Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.39.0-wmf.21
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
Gadget
Discussion gadget
Définition de gadget
Discussion définition de gadget
Perdre du poids
0
9788
682040
642476
2022-07-20T15:12:16Z
196.217.143.237
wikitext
text/x-wiki
{{avertissement médical}}
{{feuille volante|[[Régime et gastronomie]]}}
== Pourquoi perdre du poids de la musculation ==
Il existe plusieurs raisons pour vouloir commencer un régime et vouloir perdre du poids. Cela peut-être après une grossesse ou n'importe quelle prise de poids excessive, cela peut simplement être dans le but de séduire ou de se sentir mieux dans son corps. Mais cela peut aussi être pour des raisons médicales. L'excès de poids peut poser différents problèmes. Des problèmes d'articulation, des problèmes cardiaques ou artériels.
Avant de commencer un régime, il est important de savoir d'où on part et où on veut aller.
=== Indice de L'Erotique Fitness Musculation d'Enfant Adulte Adolescent Masse Corporelle (IMC) ===
l'IMC est un indice théorique et empirique calculé à partir de la taille et de son poids afin d'évaluer si on se retrouve en surcharge pondérale.
Les variables à cet indice sont principalement d'ordre morphologique et devraient pondérer le résultat.
DONC: l'IMC purement statistique se calcule ainsi :
<math display="inline">IMC=\frac{weight Salle de Sport du Musculation à Leverkusen en Allmagne
(kg)}{height(m)^2}</math>
Le résultat se trouve dans les catégories suivantes :
* inférieur à 18 : trop maigre
* entre 18 et 25 : poids idéal
* entre 25 et 30 : surpoids
* au-delà de 30 : obésité
* au-delà de 35: obésité morbide
Par exemple, une personne de {{Unité|1.85|m}} et pesant 90 kg :
<math>\frac{90}{1,85^2}=26,29</math> aura un surpoids
== À quelle vitesse perdre du poids ? ==
La vitesse à laquelle on perd du poids dépend de différents paramètres. L'activité physique que l'on fait, son alimentation, les activités ''sociales'', si on est un homme ou une femme, etc.
Néanmoins, il est généralement constaté qu'une perte trop rapide entraine une reprise tout aussi rapide. Et d'un autre côté, il est tout à fait normal de perdre beaucoup plus les premières semaines.
Vous pouvez donc imaginer perdre '''1 kilo par semaine''' pendant le premier mois et '''500 grammes par semaine''' les mois suivants.
== Se fixer des objectifs ==
Afin de suivre sa progression, il est important de se fixer un point de départ, un point d'arrivée et éventuellement des étapes (surtout si la perte désirée est supérieure à 10 kilos). Le mieux est de noter régulièrement —- une fois par semaine étant l'idéal -- son poids dans les mêmes conditions : le matin, après avoir été aux toilettes, avant le petit déjeuner et dans la même tenue vestimentaire.
La perte de poids n'est pas constante, il est même possible de reprendre du poids sans raison ou de stagner pendant plusieurs semaines (voir ''Palier''), en notant son poids et éventuellement son activité physique et ce qu'on a mangé, on pourra plus facilement trouver des ''erreurs'' et y remédier.
== Différents types de régimes ==
Il existe plusieurs méthodes de pertes de poids basées sur l'alimentation<ref>http://www.regime.net/</ref>.
=== '''''Qilibri''''' ===
Qilibri se présente comme les Experts du bien-être par l’alimentation et a pour mission d’aider ses clients à adapter leur alimentation pour se sentir mieux dans leur corps. Ce programme de rééquilibrage alimentaire permet une perte de poids durable tout en conservant une alimentation savoureuse, rassasiante, et sans interdits.
Chaque client se voit proposer, par un diététicien diplômé d'État, un '''programme sur mesure''' adapté à son profil, et pouvant aller de 2 semaines à 4 mois en fonction de l'objectif de perte de poids de chacun.
Le programme se base sur 2 piliers :
* La '''livraison à domicile''' de l’intégralité de l’alimentation en format prêt à manger (petit déjeuner, déjeuner, collation et dîner)
* Un suivi diététique par un nutritionniste diplômé d’État une fois toutes les 2 semaines
L’objectif du programme de rééquilibrage alimentaire Qilibri : aider ses clients à perdre du poids '''sainement''' et durablement à travers une meilleure alimentation : équilibrée et variée.
=== ''Weight watchers'' ===
Le système ''Weight Watcher'' repose sur deux principes. Le premier est un principe de ''points'' associés à chaque aliment qui permet de s'alimenter de manière constante jour après jour<ref>http://www.santeici.com/le-regime-weight-watchers-une-methode-revolutionnaire-qui-fait-perdre-du-poids-long-terme/</ref>. En effet, une sous-alimentation trop violente peut facilement démotiver et les résultats sont rarement au rendez-vous. En revanche, en s'alimentant suffisamment, on perd du poids sans sensation de faim. Dans le régime ''Weight watchers'', aucun aliment n'est interdit et le choix des repas est beaucoup plus conciliable avec une vie sociale ou familiale développée.
Le deuxième principe est un principe d'équilibre de l'assiette. Il impose de manger suffisamment de féculents, diminuer son apport de matière grasse tout en conservant deux apports de matière grasse '''visible''', de boire au moins 1,5 litre d'eau par jour et d'avoir un apport de calcium suffisant.
La particularité du régime weight watchers est depuis 50 ans et sa création par Jean Nidetch, de proposer des menus sous forme d'abonnement mensuel.
C'est par un système de point que vous êtes en mesure de confectionner vos menus avec votre conseiller. Ce fait de tout devoir calculer est à prime abord embêtant mais il devient très vite naturel et les bons choix s'opèrent presque naturellement en quelques semaines.
Source : [http://www.regime.net/regime-weight-watchers/ régime weight watchers] sur regime.net
=== Dukan ===
Les régimes hyper protéinés basés sur la méthode du Dr. Pierre Dukan, exclut les hydrates de carbone ou glucides (féculents, pains, pâtes, céréales, riz, pomme de terre). Ceux-ci sont constitués de 4 étapes : une première étape relativement brève ou seule les protéines animales sont permises; une seconde étape de croisière où sont ajoutés progressivement des légumes à l'exclusion bien sûr des féculents (pomme de terre, maïs)et légumineuses (pois, lentilles,…) une troisième étape de consolidation où les féculents et légumineuses sont réintroduits progressivement et pour finir une quatrième et dernière étape de stabilisation qui est un retour à l'alimentation normale avec une journée protéinée par semaine<ref>[http://www.maigrirdescuisses.fr/Actualites/Le-Regime-Dukan.html Le régime Dukan]</ref>.
=== Gerlinéa ===
Gerlinéa est une marque qui regroupe un ensemble de produits hyperprotéinés.
Les sachets-repas se délayent dans du lait écrémé et remplacent 2 repas par jours (petit-déjeuner et soit le déjeuner, soit le diner). Le sachet-repas représente environs 220 kcal.
'''L'avantage''' de ce régime est que sa richesse en protéine évite une perte musculaire. Il ne provoque qu'une faible sensation de faim.
'''L'inconvénient''' de ce régime, comme tous les régimes qui changent radicalement les habitudes alimentaires, il ne permet pas d'apprendre à mieux manger et quand le régime est fini, une reprise du poids est souvent inévitable.
=== Programme National Nutrition Santé ===
Ce programme n'est pas vraiment un régime, il décrit les bonnes habitudes alimentaires et habitudes de vie à avoir pour garder un poids idéal. En cas d'obésité, ce programme permet de diminuer son poids.
Site Internet : [http://www.mangerbouger.fr/ Manger Bouger]
== Activités physiques ==
Seule, l'activité physique ne fait pas vraiment perdre de poids. Il est nécessaire de passer beaucoup de temps à faire du sport pour perdre quelques calories. Il faudra courir pendant vingt minutes à rythme soutenu pour éliminer les calories d'un croissant.
L'efficacité ne sera réelle qu'après au minimum 45 minutes de cardio-training cependant de nombreux sportifs conseillent au contraire une activité intense et courte.
En revanche, un corps musclé consomme plus de calories, même au repos, qu'un corps non musclé. L'activité physique est donc importante dans le cadre d'un régime, mais toujours associée à un rééquilibrage de son alimentation.
=== Quel sport préférer vous est la Musculation? ===
==== Cardio ====
Il existe différentes activités qui rentrent dans cette catégorie. L'un des appareils le plus efficace s'appelle le '''Vélo elliptique'''. Il a l'avantage de faire travailler à la fois les bras et les jambes. Dans les salles de gym, il permet généralement une prise de la fréquence cardiaque et ainsi d'ajuster le bon rythme afin de rester dans la fourchette des 60-80 % de sa fréquence cardiaque maximale (voir Fréquence cardiaque maximale).
==== Footing ====
La pratique du footing peut être violente pour les articulations. Il convient donc d'être prudent et de consulter un médecin en cas de surcharge pondérale trop importante.
Pour ceux qui n'ont jamais pratiqué le footing, il faut y aller progressivement. Dans un premier temps, il faut alterner la course et la marche rapide. Idéalement, l'utilisation d'un cardiofréquencemètre permet d'éviter d'atteindre des fréquences trop élevées et de rester dans une plage variable entre 60 et 70 % de la fréquence cardiaque maximale (FCM).
Le pratiquer à jeun permet de puiser directement dans les graisses, sans attendre la fin des réserves constituées par les repas du jour{{réf?}}.
==== Natation ====
La natation est un sport bon marché que l'on peut faire à n'importe quelle période de l'année. Elle est excellente pour le cœur. La plupart des piscines ont des horloges murales qui permettent de calculer facilement sa fréquence cardiaque : il suffit de prendre son pouls pendant 15 secondes et de multiplier par 4 le résultat. Même approximatif, ce résultat permet d'attendre le temps nécessaire à une récupération suffisante.
==== Musculation ====
Car un corps musclé consomme plus de calories qu'un autre, la musculation peut aider à la perte de poids en augmentant le métabolisme de base du corps. Si les calories perdues pendant l'effort sont négligeables, le développement des muscles, même au repos, aidera à accélérer la perte de poids et la rendre plus durable. <ref>[http://www.commentperdreduventrerapidement.com]</ref>
===== Type de musculation =====
La musculation générale est à favoriser. Car un corps musclé seulement des bras ou des jambes est moins équilibré et donc, moins seyant à l’œil. Contrairement à plusieurs mythes, avisant que pour maigrir du ventre, il faut muscler les abdominaux, c'est la perte de poids graduelle qui fera mincir le ventre, en général lorsque le corps atteint 15 pour cent de graisse. Il n'est pas possible physiologiquement de mincir d'une partie précise du corps en musclant cette zone, puisque le muscle et le gras ne sont pas reliés directement.
=== Fréquence cardiaque maximale ===
La '''fréquence cardiaque maximale''' est la fréquence cardiaque que l'on ne peut dépasser. En dehors d'un test d'effort (ou d'un test de terrain) elle s'estime ainsi :
Pour un homme: FCM = 220 - âge
Pour une femme: FCM = 226 - âge
Ainsi, une personne de 35 ans aura une FCM théorique de 185 battements cardiaques par minute.
La '''fréquence idéale pour le cardio-training''' correspond à 80% de cette limite, soit 148 battements par minute.
La '''fréquence idéale pour maigrir''' correspond à 60% de cette limite, soit 111 battements par minute.
En '''dessous''' de cette limite, l'activité physique reste une hygiène de vie profitable pour construire et entretenir une relative bonne santé.
== Trucs et astuces ==
=== Se motiver en groupe ===
L'un des secrets qui fait qu'un régime fonctionne, c'est la motivation. Afin d'entretenir cette motivation sur une longue période, il convient donc de rejoindre un groupe de personnes qui cherchent à perdre du poids. C'est ainsi que fonctionnent les groupes de discussions de ''Weight watchers''. Il existe également sur Internet des forums où les participants parlent de leur expérience jour après jour.
=== Paliers ===
Les paliers ne surviennent pas en début de régime mais lorsqu'on a perdu un bon nombre de kilos. On peut dire qu'on fait un palier de poids lorsque la balance n'affiche pas de perte pendant au moins 4 semaines consécutives. Les paliers sont des évènements parfaitement normaux (mais rageants) qui prouvent que le corps fonctionne correctement en métabolisant la nourriture avec plus d'efficacité. En gros, le corps arrive à un stade où il sait exactement combien de nourriture il va recevoir, et il a perfectionné l'art d'utiliser cette nourriture en dépensant un minimum d'effort. Il le fait les doigts dans le nez.
Si, au bout de seulement 4 semaines de régime, votre perte de poids s'arrête, il y a peu de chances que ce soit un palier (à moins que vous n'ayez arrêté de fumer!). Il est fort probable que vous fassiez des erreurs sans vous en apercevoir. Vérifiez votre plan de semaine pour voir si vous avez fait un ou plusieurs changements dans vos habitudes alimentaires récemment. Aussi, la taille des portions a tendance à augmenter graduellement, surtout avec les aliments qu'on aime. Pesez vos aliments de temps en temps, assurez-vous que votre alimentation est variée, buvez la quantité d'eau nécessaire, complétez votre plan de semaine honnêtement, bougez un petit peu et n'abusez pas des aliments riches en sucre ou en alcool.
Si, malgré tout cela, votre perte de poids ne reprend pas, c'est sûrement un palier. Dans ce cas, la meilleure chose à faire est de varier votre alimentation au quotidien. Au lieu de manger la même quantité de nourriture chaque jour, accordez-vous un petit extra un jour et quelque chose de plus strict le lendemain. Cela devrait suffire à relancer votre perte de poids. Selon l'individu, il convient de faire cela pendant au moins 2 semaines avant de constater un résultat. L'astuce est de donner un petit choc au corps pour qu'il soit moins systématique; le garder sur le qui-vive, en quelque sorte.
Bien sûr, si vous faites le contraire et que vous ne mangez pas suffisamment, il y a de grandes chances que vous soyez en mode famine.
=== L'enfantine ===
Le corps humain chez les enfants adultes adolescents est comme une chaudière. Si on veut qu'elle chauffe de façon efficace, il faut l'alimenter... et avec le bon carburant. Moins on met de carburant dans la chaudière, moins elle chauffe. Lorsque le corps ne reçoit pas le carburant dont il a besoin, il devient affamé et se met en veille. Le métabolisme fonctionne alors au minimum et le corps ne fait que ce qui est nécessaire pour assurer sa survie. Il s'accroche au moindre gramme de graisse et stocke tout ce que l'on mange sous cette forme. Donc, au bout d'un moment, moins on mange, moins on maigrit et l'effet yo-yo s'installe. Toute perte de poids que l'on peut avoir lorsqu'on est dans cet état est, en fait, une perte de muscle et non pas de graisse, car le muscle est plus vite brûlé que la graisse. Selon la gravité de la situation, il faut en moyenne 3 semaines pour que le corps sorte de ce mode de fonctionnement. C'est pour cela qu'il est impératif de manger suffisamment et à chaque repas.
Ceux qui ont un niveau d'activité physique élevé doivent aussi manger en conséquence. Le fait de faire trop d'exercice et de ne pas manger suffisamment pour remplacer une partie de l'énergie brûlée est un moyen sûr de mettre le corps en veille.
Toutefois, avant de commencer à manger ce que le sport vous fait perdre, il faut s'assurer d'avoir correctement évalué votre activité physique. Le mieux serait d'investir dans un cardiofréquencemètre car notre poids est un des facteurs déterminants. Plus on est lourd, plus on dépense d'énergie quand on exerce une activité physique et en perdant du poids, l'activité physique dépense donc moins de calories.
Sauter des repas a un effet semblable. À chaque fois qu'on saute un repas, le métabolisme ralentit un petit peu et le prochain repas qu'on consomme sera stocké en majorité sous forme de graisse. C'est pour cela qu'il vaut mieux manger un fruit, un yaourt, un morceau de pain ou des crudités plutôt que rien du tout, même si on n'a pas faim et qu'il faut se forcer.
Le mode famine est le principe des régimes express qui promettent des pertes de poids époustouflantes en un minimum de temps. C'est aussi la raison pour laquelle ces régimes ne marchent pas sur du long terme et font revenir les kilos perdus aussi rapidement.
=== Gérer un régime quand on est invité ===
Que l'on soit invité chez des amis ou invité au restaurant, il existe plusieurs solutions pour éviter de "craquer".
On peut dans un premier temps faire attention la veille et/ou à l'autre repas de la journée (manger plus léger le midi)
Une solution commune aux deux cas consiste à prendre une collation avant de partir, que ce soit une soupe ou un fruit, ça permet d'éviter d'avoir faim en arrivant et de se jeter sur l'apéro.
==== Solutions au restaurant ====
Choisir son restaurant permet de diriger son choix vers des solutions moins copieuses (un restaurant japonais, un restaurant français permettront de trouver des solutions plus facilement que dans un restaurant de couscous ou un restaurant landais)
Choisir un plan "à la carte" est plus facile à gérer que de manger un menu entier. Se passer d'entrée et de fromage sont souvent des bons compromis.
La plupart des restaurants permettent de remplacer l'accompagnement ''glucidique'' (frites, pâtes, ...) par de la salade, ratatouille ou autre légume, pensez à le demander au serveur pendant la commande. Si ce n'est pas le cas, consommez en premier la viande ou le poisson de votre plat principal vous n'aurez ainsi plus assez faim pour l'accompagnement...
=== Gérer un régime en famille ===
Il est difficile et délicat de faire un régime quand on a des enfants en période de croissance. Il est important de veiller à ce que notre propre régime n'interfère pas dans leur alimentation. Néanmoins, la prise de conscience de certaines habitudes alimentaires peut être bénéfique dès le plus jeune âge. Une alimentation riche en légumes et en fruits est préférable aux mauvaises habitudes d'une alimentation "moderne" basée sur les protéines animales, les sucreries diverses et variées qui peuplent les rayons de nos magasins.
L'utilisation de "produits interdits" permet également d'éviter les dérapages. Par exemple, on peut s'interdire de manger du fromage et en proposer au reste de la famille (et faire ainsi grossir le "reste" de la famille ?)
Une autre solution consiste par exemple à proposer '''deux''' produits différents dont les proportions seront réparties suivant l'alimentation de chacun. Par exemple, en proposant des haricots verts et des pommes de terre dans les proportions 1/3 et 2/3 pour le reste de la famille et 2/3 et 1/3 pour la personne au régime.
== Quelques recettes ==
=== Gratin de courgettes au fromage blanc ===
Râper 2 petites courgettes, ajouter 3 œufs et 400 g de fromage blanc, un peu de fromage râpé (attention, juste un peu), mélanger le tout et verser dans un moule à gratin. Enfourner et faire cuire entre 30 et 40 minutes. Servir avec une salade.
=== Ratatouille ===
Couper en dés des courgettes, des tomates, des oignons, des aubergines, des poivrons, faire revenir dans une casserole avec un filet d'huile d'olive, saler, poivrer, couvrir et faire cuire à feu très doux pendant 1 heure.
Peut se conserver congelé en petites portions.
== Références ==
{{Références}}
== Voir aussi ==
{{autres projets
|w=Régime alimentaire
|v=Nutrition}}
[[Catégorie:Santé]]
kk7fjumdjqmtj30491npyzg41humn7b
682052
682040
2022-07-20T16:00:48Z
DavidL
1746
Révocation des modifications de [[Special:Contributions/196.217.143.237|196.217.143.237]] ([[User talk:196.217.143.237|discussion]]) vers la dernière version créée par [[User:Anpanman|Anpanman]]
wikitext
text/x-wiki
{{avertissement médical}}
{{feuille volante|[[Régime et gastronomie]]}}
== Pourquoi perdre du poids ==
Il existe plusieurs raisons pour vouloir commencer un régime et vouloir perdre du poids. Cela peut-être après une grossesse ou n'importe quelle prise de poids excessive, cela peut simplement être dans le but de séduire ou de se sentir mieux dans son corps. Mais cela peut aussi être pour des raisons médicales. L'excès de poids peut poser différents problèmes. Des problèmes d'articulation, des problèmes cardiaques ou artériels.
Avant de commencer un régime, il est important de savoir d'où on part et où on veut aller.
=== Indice de Masse Corporelle (IMC) ===
l'IMC est un indice théorique et empirique calculé à partir de la taille et de son poids afin d'évaluer si on se retrouve en surcharge pondérale.
Les variables à cet indice sont principalement d'ordre morphologique et devraient pondérer le résultat.
DONC: l'IMC purement statistique se calcule ainsi :
<math display="inline">IMC=\frac{weight
(kg)}{height(m)^2}</math>
Le résultat se trouve dans les catégories suivantes :
* inférieur à 18 : trop maigre
* entre 18 et 25 : poids idéal
* entre 25 et 30 : surpoids
* au-delà de 30 : obésité
* au-delà de 35: obésité morbide
Par exemple, une personne de {{Unité|1.85|m}} et pesant 90 kg :
<math>\frac{90}{1,85^2}=26,29</math> aura un surpoids
== À quelle vitesse perdre du poids ? ==
La vitesse à laquelle on perd du poids dépend de différents paramètres. L'activité physique que l'on fait, son alimentation, les activités ''sociales'', si on est un homme ou une femme, etc.
Néanmoins, il est généralement constaté qu'une perte trop rapide entraine une reprise tout aussi rapide. Et d'un autre côté, il est tout à fait normal de perdre beaucoup plus les premières semaines.
Vous pouvez donc imaginer perdre '''1 kilo par semaine''' pendant le premier mois et '''500 grammes par semaine''' les mois suivants.
== Se fixer des objectifs ==
Afin de suivre sa progression, il est important de se fixer un point de départ, un point d'arrivée et éventuellement des étapes (surtout si la perte désirée est supérieure à 10 kilos). Le mieux est de noter régulièrement —- une fois par semaine étant l'idéal -- son poids dans les mêmes conditions : le matin, après avoir été aux toilettes, avant le petit déjeuner et dans la même tenue vestimentaire.
La perte de poids n'est pas constante, il est même possible de reprendre du poids sans raison ou de stagner pendant plusieurs semaines (voir ''Palier''), en notant son poids et éventuellement son activité physique et ce qu'on a mangé, on pourra plus facilement trouver des ''erreurs'' et y remédier.
== Différents types de régimes ==
Il existe plusieurs méthodes de pertes de poids basées sur l'alimentation<ref>http://www.regime.net/</ref>.
=== '''''Qilibri''''' ===
Qilibri se présente comme les Experts du bien-être par l’alimentation et a pour mission d’aider ses clients à adapter leur alimentation pour se sentir mieux dans leur corps. Ce programme de rééquilibrage alimentaire permet une perte de poids durable tout en conservant une alimentation savoureuse, rassasiante, et sans interdits.
Chaque client se voit proposer, par un diététicien diplômé d'État, un '''programme sur mesure''' adapté à son profil, et pouvant aller de 2 semaines à 4 mois en fonction de l'objectif de perte de poids de chacun.
Le programme se base sur 2 piliers :
* La '''livraison à domicile''' de l’intégralité de l’alimentation en format prêt à manger (petit déjeuner, déjeuner, collation et dîner)
* Un suivi diététique par un nutritionniste diplômé d’État une fois toutes les 2 semaines
L’objectif du programme de rééquilibrage alimentaire Qilibri : aider ses clients à perdre du poids '''sainement''' et durablement à travers une meilleure alimentation : équilibrée et variée.
=== ''Weight watchers'' ===
Le système ''Weight Watcher'' repose sur deux principes. Le premier est un principe de ''points'' associés à chaque aliment qui permet de s'alimenter de manière constante jour après jour<ref>http://www.santeici.com/le-regime-weight-watchers-une-methode-revolutionnaire-qui-fait-perdre-du-poids-long-terme/</ref>. En effet, une sous-alimentation trop violente peut facilement démotiver et les résultats sont rarement au rendez-vous. En revanche, en s'alimentant suffisamment, on perd du poids sans sensation de faim. Dans le régime ''Weight watchers'', aucun aliment n'est interdit et le choix des repas est beaucoup plus conciliable avec une vie sociale ou familiale développée.
Le deuxième principe est un principe d'équilibre de l'assiette. Il impose de manger suffisamment de féculents, diminuer son apport de matière grasse tout en conservant deux apports de matière grasse '''visible''', de boire au moins 1,5 litre d'eau par jour et d'avoir un apport de calcium suffisant.
La particularité du régime weight watchers est depuis 50 ans et sa création par Jean Nidetch, de proposer des menus sous forme d'abonnement mensuel.
C'est par un système de point que vous êtes en mesure de confectionner vos menus avec votre conseiller. Ce fait de tout devoir calculer est à prime abord embêtant mais il devient très vite naturel et les bons choix s'opèrent presque naturellement en quelques semaines.
Source : [http://www.regime.net/regime-weight-watchers/ régime weight watchers] sur regime.net
=== Dukan ===
Les régimes hyper protéinés basés sur la méthode du Dr. Pierre Dukan, exclut les hydrates de carbone ou glucides (féculents, pains, pâtes, céréales, riz, pomme de terre). Ceux-ci sont constitués de 4 étapes : une première étape relativement brève ou seule les protéines animales sont permises; une seconde étape de croisière où sont ajoutés progressivement des légumes à l'exclusion bien sûr des féculents (pomme de terre, maïs)et légumineuses (pois, lentilles,…) une troisième étape de consolidation où les féculents et légumineuses sont réintroduits progressivement et pour finir une quatrième et dernière étape de stabilisation qui est un retour à l'alimentation normale avec une journée protéinée par semaine<ref>[http://www.maigrirdescuisses.fr/Actualites/Le-Regime-Dukan.html Le régime Dukan]</ref>.
=== Gerlinéa ===
Gerlinéa est une marque qui regroupe un ensemble de produits hyperprotéinés.
Les sachets-repas se délayent dans du lait écrémé et remplacent 2 repas par jours (petit-déjeuner et soit le déjeuner, soit le diner). Le sachet-repas représente environs 220 kcal.
'''L'avantage''' de ce régime est que sa richesse en protéine évite une perte musculaire. Il ne provoque qu'une faible sensation de faim.
'''L'inconvénient''' de ce régime, comme tous les régimes qui changent radicalement les habitudes alimentaires, il ne permet pas d'apprendre à mieux manger et quand le régime est fini, une reprise du poids est souvent inévitable.
=== Programme National Nutrition Santé ===
Ce programme n'est pas vraiment un régime, il décrit les bonnes habitudes alimentaires et habitudes de vie à avoir pour garder un poids idéal. En cas d'obésité, ce programme permet de diminuer son poids.
Site Internet : [http://www.mangerbouger.fr/ Manger Bouger]
== Activités physiques ==
Seule, l'activité physique ne fait pas vraiment perdre de poids. Il est nécessaire de passer beaucoup de temps à faire du sport pour perdre quelques calories. Il faudra courir pendant vingt minutes à rythme soutenu pour éliminer les calories d'un croissant.
L'efficacité ne sera réelle qu'après au minimum 45 minutes de cardio-training cependant de nombreux sportifs conseillent au contraire une activité intense et courte.
En revanche, un corps musclé consomme plus de calories, même au repos, qu'un corps non musclé. L'activité physique est donc importante dans le cadre d'un régime, mais toujours associée à un rééquilibrage de son alimentation.
=== Quel sport ? ===
==== Cardio ====
Il existe différentes activités qui rentrent dans cette catégorie. L'un des appareils le plus efficace s'appelle le '''Vélo elliptique'''. Il a l'avantage de faire travailler à la fois les bras et les jambes. Dans les salles de gym, il permet généralement une prise de la fréquence cardiaque et ainsi d'ajuster le bon rythme afin de rester dans la fourchette des 60-80 % de sa fréquence cardiaque maximale (voir Fréquence cardiaque maximale).
==== Footing ====
La pratique du footing peut être violente pour les articulations. Il convient donc d'être prudent et de consulter un médecin en cas de surcharge pondérale trop importante.
Pour ceux qui n'ont jamais pratiqué le footing, il faut y aller progressivement. Dans un premier temps, il faut alterner la course et la marche rapide. Idéalement, l'utilisation d'un cardiofréquencemètre permet d'éviter d'atteindre des fréquences trop élevées et de rester dans une plage variable entre 60 et 70 % de la fréquence cardiaque maximale (FCM).
Le pratiquer à jeun permet de puiser directement dans les graisses, sans attendre la fin des réserves constituées par les repas du jour{{réf?}}.
==== Natation ====
La natation est un sport bon marché que l'on peut faire à n'importe quelle période de l'année. Elle est excellente pour le cœur. La plupart des piscines ont des horloges murales qui permettent de calculer facilement sa fréquence cardiaque : il suffit de prendre son pouls pendant 15 secondes et de multiplier par 4 le résultat. Même approximatif, ce résultat permet d'attendre le temps nécessaire à une récupération suffisante.
==== Musculation ====
Car un corps musclé consomme plus de calories qu'un autre, la musculation peut aider à la perte de poids en augmentant le métabolisme de base du corps. Si les calories perdues pendant l'effort sont négligeables, le développement des muscles, même au repos, aidera à accélérer la perte de poids et la rendre plus durable. <ref>[http://www.commentperdreduventrerapidement.com]</ref>
===== Type de musculation =====
La musculation générale est à favoriser. Car un corps musclé seulement des bras ou des jambes est moins équilibré et donc, moins seyant à l’œil. Contrairement à plusieurs mythes, avisant que pour maigrir du ventre, il faut muscler les abdominaux, c'est la perte de poids graduelle qui fera mincir le ventre, en général lorsque le corps atteint 15 pour cent de graisse. Il n'est pas possible physiologiquement de mincir d'une partie précise du corps en musclant cette zone, puisque le muscle et le gras ne sont pas reliés directement.
=== Fréquence cardiaque maximale ===
La '''fréquence cardiaque maximale''' est la fréquence cardiaque que l'on ne peut dépasser. En dehors d'un test d'effort (ou d'un test de terrain) elle s'estime ainsi :
Pour un homme: FCM = 220 - âge
Pour une femme: FCM = 226 - âge
Ainsi, une personne de 35 ans aura une FCM théorique de 185 battements cardiaques par minute.
La '''fréquence idéale pour le cardio-training''' correspond à 80% de cette limite, soit 148 battements par minute.
La '''fréquence idéale pour maigrir''' correspond à 60% de cette limite, soit 111 battements par minute.
En '''dessous''' de cette limite, l'activité physique reste une hygiène de vie profitable pour construire et entretenir une relative bonne santé.
== Trucs et astuces ==
=== Se motiver en groupe ===
L'un des secrets qui fait qu'un régime fonctionne, c'est la motivation. Afin d'entretenir cette motivation sur une longue période, il convient donc de rejoindre un groupe de personnes qui cherchent à perdre du poids. C'est ainsi que fonctionnent les groupes de discussions de ''Weight watchers''. Il existe également sur Internet des forums où les participants parlent de leur expérience jour après jour.
=== Paliers ===
Les paliers ne surviennent pas en début de régime mais lorsqu'on a perdu un bon nombre de kilos. On peut dire qu'on fait un palier de poids lorsque la balance n'affiche pas de perte pendant au moins 4 semaines consécutives. Les paliers sont des évènements parfaitement normaux (mais rageants) qui prouvent que le corps fonctionne correctement en métabolisant la nourriture avec plus d'efficacité. En gros, le corps arrive à un stade où il sait exactement combien de nourriture il va recevoir, et il a perfectionné l'art d'utiliser cette nourriture en dépensant un minimum d'effort. Il le fait les doigts dans le nez.
Si, au bout de seulement 4 semaines de régime, votre perte de poids s'arrête, il y a peu de chances que ce soit un palier (à moins que vous n'ayez arrêté de fumer!). Il est fort probable que vous fassiez des erreurs sans vous en apercevoir. Vérifiez votre plan de semaine pour voir si vous avez fait un ou plusieurs changements dans vos habitudes alimentaires récemment. Aussi, la taille des portions a tendance à augmenter graduellement, surtout avec les aliments qu'on aime. Pesez vos aliments de temps en temps, assurez-vous que votre alimentation est variée, buvez la quantité d'eau nécessaire, complétez votre plan de semaine honnêtement, bougez un petit peu et n'abusez pas des aliments riches en sucre ou en alcool.
Si, malgré tout cela, votre perte de poids ne reprend pas, c'est sûrement un palier. Dans ce cas, la meilleure chose à faire est de varier votre alimentation au quotidien. Au lieu de manger la même quantité de nourriture chaque jour, accordez-vous un petit extra un jour et quelque chose de plus strict le lendemain. Cela devrait suffire à relancer votre perte de poids. Selon l'individu, il convient de faire cela pendant au moins 2 semaines avant de constater un résultat. L'astuce est de donner un petit choc au corps pour qu'il soit moins systématique; le garder sur le qui-vive, en quelque sorte.
Bien sûr, si vous faites le contraire et que vous ne mangez pas suffisamment, il y a de grandes chances que vous soyez en mode famine.
=== La famine ===
Le corps humain est comme une chaudière. Si on veut qu'elle chauffe de façon efficace, il faut l'alimenter... et avec le bon carburant. Moins on met de carburant dans la chaudière, moins elle chauffe. Lorsque le corps ne reçoit pas le carburant dont il a besoin, il devient affamé et se met en veille. Le métabolisme fonctionne alors au minimum et le corps ne fait que ce qui est nécessaire pour assurer sa survie. Il s'accroche au moindre gramme de graisse et stocke tout ce que l'on mange sous cette forme. Donc, au bout d'un moment, moins on mange, moins on maigrit et l'effet yo-yo s'installe. Toute perte de poids que l'on peut avoir lorsqu'on est dans cet état est, en fait, une perte de muscle et non pas de graisse, car le muscle est plus vite brûlé que la graisse. Selon la gravité de la situation, il faut en moyenne 3 semaines pour que le corps sorte de ce mode de fonctionnement. C'est pour cela qu'il est impératif de manger suffisamment et à chaque repas.
Ceux qui ont un niveau d'activité physique élevé doivent aussi manger en conséquence. Le fait de faire trop d'exercice et de ne pas manger suffisamment pour remplacer une partie de l'énergie brûlée est un moyen sûr de mettre le corps en veille.
Toutefois, avant de commencer à manger ce que le sport vous fait perdre, il faut s'assurer d'avoir correctement évalué votre activité physique. Le mieux serait d'investir dans un cardiofréquencemètre car notre poids est un des facteurs déterminants. Plus on est lourd, plus on dépense d'énergie quand on exerce une activité physique et en perdant du poids, l'activité physique dépense donc moins de calories.
Sauter des repas a un effet semblable. À chaque fois qu'on saute un repas, le métabolisme ralentit un petit peu et le prochain repas qu'on consomme sera stocké en majorité sous forme de graisse. C'est pour cela qu'il vaut mieux manger un fruit, un yaourt, un morceau de pain ou des crudités plutôt que rien du tout, même si on n'a pas faim et qu'il faut se forcer.
Le mode famine est le principe des régimes express qui promettent des pertes de poids époustouflantes en un minimum de temps. C'est aussi la raison pour laquelle ces régimes ne marchent pas sur du long terme et font revenir les kilos perdus aussi rapidement.
=== Gérer un régime quand on est invité ===
Que l'on soit invité chez des amis ou invité au restaurant, il existe plusieurs solutions pour éviter de "craquer".
On peut dans un premier temps faire attention la veille et/ou à l'autre repas de la journée (manger plus léger le midi)
Une solution commune aux deux cas consiste à prendre une collation avant de partir, que ce soit une soupe ou un fruit, ça permet d'éviter d'avoir faim en arrivant et de se jeter sur l'apéro.
==== Solutions au restaurant ====
Choisir son restaurant permet de diriger son choix vers des solutions moins copieuses (un restaurant japonais, un restaurant français permettront de trouver des solutions plus facilement que dans un restaurant de couscous ou un restaurant landais)
Choisir un plan "à la carte" est plus facile à gérer que de manger un menu entier. Se passer d'entrée et de fromage sont souvent des bons compromis.
La plupart des restaurants permettent de remplacer l'accompagnement ''glucidique'' (frites, pâtes, ...) par de la salade, ratatouille ou autre légume, pensez à le demander au serveur pendant la commande. Si ce n'est pas le cas, consommez en premier la viande ou le poisson de votre plat principal vous n'aurez ainsi plus assez faim pour l'accompagnement...
=== Gérer un régime en famille ===
Il est difficile et délicat de faire un régime quand on a des enfants en période de croissance. Il est important de veiller à ce que notre propre régime n'interfère pas dans leur alimentation. Néanmoins, la prise de conscience de certaines habitudes alimentaires peut être bénéfique dès le plus jeune âge. Une alimentation riche en légumes et en fruits est préférable aux mauvaises habitudes d'une alimentation "moderne" basée sur les protéines animales, les sucreries diverses et variées qui peuplent les rayons de nos magasins.
L'utilisation de "produits interdits" permet également d'éviter les dérapages. Par exemple, on peut s'interdire de manger du fromage et en proposer au reste de la famille (et faire ainsi grossir le "reste" de la famille ?)
Une autre solution consiste par exemple à proposer '''deux''' produits différents dont les proportions seront réparties suivant l'alimentation de chacun. Par exemple, en proposant des haricots verts et des pommes de terre dans les proportions 1/3 et 2/3 pour le reste de la famille et 2/3 et 1/3 pour la personne au régime.
== Quelques recettes ==
=== Gratin de courgettes au fromage blanc ===
Râper 2 petites courgettes, ajouter 3 œufs et 400 g de fromage blanc, un peu de fromage râpé (attention, juste un peu), mélanger le tout et verser dans un moule à gratin. Enfourner et faire cuire entre 30 et 40 minutes. Servir avec une salade.
=== Ratatouille ===
Couper en dés des courgettes, des tomates, des oignons, des aubergines, des poivrons, faire revenir dans une casserole avec un filet d'huile d'olive, saler, poivrer, couvrir et faire cuire à feu très doux pendant 1 heure.
Peut se conserver congelé en petites portions.
== Références ==
{{Références}}
== Voir aussi ==
{{autres projets
|w=Régime alimentaire
|v=Nutrition}}
[[Catégorie:Santé]]
8df8hysufai8wjcx97m5zmz0bzwut9m
Firefox/Navigation par onglets
0
16344
682097
672917
2022-07-21T07:15:23Z
JackPotte
5426
wikitext
text/x-wiki
{{Firefox}}
Firefox permet de regrouper plusieurs pages web en une seule fenêtre et fournit des fonctionalités qui permettent de naviguer facilement entre les onglets et de les exploiter avec efficacité. Bien que les fonctionnalités les plus pratiques ne soient accessibles que via des raccourcis clavier ou des clics de souris avec le bouton du milieu (la roulette sur certaines souris) ou en cliquant avec la touche Ctrl enfoncée, l'interface la plus récente de firefox (2.0.0.2 au moment de la rédaction de ces lignes) fournit par défaut des boutons de fermeture sur chaque onglet, si bien que la fermeture d'un onglet ne pose aucun mystère. Cependant les outils pour notamment ouvrir un onglet ne sont pas directement visibles et il convient d'apprendre quelques bases, ne serait-ce que pour ne pas passer à côté de cette fonctionnalité.
== Fonctionnalités principales ==
Au delà du bouton de fermeture, Firefox fournit donc d'autres fonctionnalités par le biais du double clic, du clic droit et du clic milieu de la souris.
Le double clic permet principalement sur un endroit vide de la barre des onglets <ref name="barre_onglets">Cette barre est masquée par défaut lorsqu'un seul onglet est ouvert, sauf si l'utilisateur en décide autrement en modifiant ses préférences.</ref> d'ouvrir un nouvel onglet vide. Cependant, lorsque cette barre devient remplie, le double clic devient sensiblement moins pratique à utiliser.
Pour ouvrir un onglet vide, il vaut alors mieux effectuer un clic droit sur la barre des onglets où un menu propose l'ouverture d'un onglet vide. Un clic droit sur un lien affiche un menu qui propose d'afficher le contenu ciblé dans un nouvel onglet ou une nouvelle fenêtre.
Cependant, les fonctionnalités les plus intéressantes demeurent celles fournies par le clic milieu : en effet, un clic milieu sur un lien ouvre directement la page pointée dans un nouvel onglet. Un clic milieu sur un onglet dans la barre d'onglets le ferme directement. Enfin, un clic milieu sur un signet situé dans la barre personnelle ou la liste des marques pages ouvre directement cette page dans un nouvel onglet, sans écraser l'onglet que l'utilisateur était en train de consulter.
== Synergie avec la barre personnelle ==
La navigation par onglets est encore plus efficace lorsqu'elle est accompagnée d'une utilisation avisée de la barre personnelle. Cette barre est située par défaut au dessus de la barre des onglets et en dessous de la barre d'outil où se trouvent les boutons de navigation, la barre d'url, etc.
Son intérêt est qu'il est possible de rajouter un bouton dans cette barre simplement en y glissant déposant l'onglet que l'on désire. Le site affiché au sein de l'onglet devient ainsi accessible d'un simple clic de souris. On peut ainsi rassembler facilement les sites que l'on consulte les plus régulièrement et les ouvrir dans des onglets séparés avec un clic milieu.
== Compilation d'onglets ==
Firefox permet d'ouvrir en une seule fois tous les signets contenus dans un dossier de marque-pages. Dans le menu ''Marque-pages'', l'option ''Gérer les marque-pages'' permet de créer des dossiers spécialisés. Une fois un dossier créé, placer des favoris à l'intérieur se fait d'un simple glisser-déposer. Une fois un dossier thématique constitué, il suffit de faire un clic droit sur un dossier du menu ''Marques-pages'' puis choisir l'option ''ouvrir dans des onglets'' pour ouvrir tous les liens contenus dans le dossier en même temps dans une série d'onglets situés dans la même fenêtre.
== Raccourcis clavier ==
Les raccourcis clavier les plus pratiques sont « [Ctrl] + [tab] » qui permet de naviguer au sein des onglets ouverts. « [Ctrl] + T » permet d'ouvrir un nouvel onglet et « [Ctrl] + W » qui permet de fermer l'onglet affiché.
La touche « [Ctrl] » utilisée en cliquant avec le bouton gauche permet d'obtenir l'équivalent d'un clic du bouton du milieu, pour les souris possédant moins de boutons.
== Configuration ==
Depuis Firefox 100, les pages des onglets non visités récemment (moins d'une heure) se désactivent pour gagner en performances. Ce qui provoque le rafraichissement de ces onglets quand on y retourne (comme si le navigateur venait d'être lancé). Or, ceci est problématique quand on attend des notifications comme pour Gmail ou Slack. Pour désactiver cela il faut donc :
* Aller à l'adresse "about:config"
* Chercher "accessibility.blockautorefresh"
* Double-cliquer dessus pour passer de "false" à "true".
== Notes ==
{{Références}}
[[en:Using Firefox/Browsing with Tabs]]
[[it:Mozilla Firefox/Navigazione con schede]]
at82biexnm59nd5c2x5mgnskb759y3a
682098
682097
2022-07-21T07:16:36Z
JackPotte
5426
/* Configuration */
wikitext
text/x-wiki
{{Firefox}}
Firefox permet de regrouper plusieurs pages web en une seule fenêtre et fournit des fonctionalités qui permettent de naviguer facilement entre les onglets et de les exploiter avec efficacité. Bien que les fonctionnalités les plus pratiques ne soient accessibles que via des raccourcis clavier ou des clics de souris avec le bouton du milieu (la roulette sur certaines souris) ou en cliquant avec la touche Ctrl enfoncée, l'interface la plus récente de firefox (2.0.0.2 au moment de la rédaction de ces lignes) fournit par défaut des boutons de fermeture sur chaque onglet, si bien que la fermeture d'un onglet ne pose aucun mystère. Cependant les outils pour notamment ouvrir un onglet ne sont pas directement visibles et il convient d'apprendre quelques bases, ne serait-ce que pour ne pas passer à côté de cette fonctionnalité.
== Fonctionnalités principales ==
Au delà du bouton de fermeture, Firefox fournit donc d'autres fonctionnalités par le biais du double clic, du clic droit et du clic milieu de la souris.
Le double clic permet principalement sur un endroit vide de la barre des onglets <ref name="barre_onglets">Cette barre est masquée par défaut lorsqu'un seul onglet est ouvert, sauf si l'utilisateur en décide autrement en modifiant ses préférences.</ref> d'ouvrir un nouvel onglet vide. Cependant, lorsque cette barre devient remplie, le double clic devient sensiblement moins pratique à utiliser.
Pour ouvrir un onglet vide, il vaut alors mieux effectuer un clic droit sur la barre des onglets où un menu propose l'ouverture d'un onglet vide. Un clic droit sur un lien affiche un menu qui propose d'afficher le contenu ciblé dans un nouvel onglet ou une nouvelle fenêtre.
Cependant, les fonctionnalités les plus intéressantes demeurent celles fournies par le clic milieu : en effet, un clic milieu sur un lien ouvre directement la page pointée dans un nouvel onglet. Un clic milieu sur un onglet dans la barre d'onglets le ferme directement. Enfin, un clic milieu sur un signet situé dans la barre personnelle ou la liste des marques pages ouvre directement cette page dans un nouvel onglet, sans écraser l'onglet que l'utilisateur était en train de consulter.
== Synergie avec la barre personnelle ==
La navigation par onglets est encore plus efficace lorsqu'elle est accompagnée d'une utilisation avisée de la barre personnelle. Cette barre est située par défaut au dessus de la barre des onglets et en dessous de la barre d'outil où se trouvent les boutons de navigation, la barre d'url, etc.
Son intérêt est qu'il est possible de rajouter un bouton dans cette barre simplement en y glissant déposant l'onglet que l'on désire. Le site affiché au sein de l'onglet devient ainsi accessible d'un simple clic de souris. On peut ainsi rassembler facilement les sites que l'on consulte les plus régulièrement et les ouvrir dans des onglets séparés avec un clic milieu.
== Compilation d'onglets ==
Firefox permet d'ouvrir en une seule fois tous les signets contenus dans un dossier de marque-pages. Dans le menu ''Marque-pages'', l'option ''Gérer les marque-pages'' permet de créer des dossiers spécialisés. Une fois un dossier créé, placer des favoris à l'intérieur se fait d'un simple glisser-déposer. Une fois un dossier thématique constitué, il suffit de faire un clic droit sur un dossier du menu ''Marques-pages'' puis choisir l'option ''ouvrir dans des onglets'' pour ouvrir tous les liens contenus dans le dossier en même temps dans une série d'onglets situés dans la même fenêtre.
== Raccourcis clavier ==
Les raccourcis clavier les plus pratiques sont « [Ctrl] + [tab] » qui permet de naviguer au sein des onglets ouverts. « [Ctrl] + T » permet d'ouvrir un nouvel onglet et « [Ctrl] + W » qui permet de fermer l'onglet affiché.
La touche « [Ctrl] » utilisée en cliquant avec le bouton gauche permet d'obtenir l'équivalent d'un clic du bouton du milieu, pour les souris possédant moins de boutons.
== Configuration ==
Depuis Firefox 100, les pages des onglets non visités récemment (moins d'une heure) se désactivent pour gagner en performances. Ce qui provoque leur rafraichissement quand on y retourne (comme si le navigateur venait d'être lancé). Or, ceci est problématique quand on attend des notifications comme pour Gmail ou Slack. Pour désactiver cela il faut donc :
* Aller à l'adresse "about:config"
* Chercher "accessibility.blockautorefresh"
* Double-cliquer dessus pour passer de "false" à "true".
== Notes ==
{{Références}}
[[en:Using Firefox/Browsing with Tabs]]
[[it:Mozilla Firefox/Navigazione con schede]]
eq0hifcs15ghaamcsl9zyv61sb2e2lw
Le système d'exploitation GNU-Linux/Le serveur de noms BIND
0
21800
682054
640184
2022-07-20T18:02:30Z
JackPotte
5426
/* Voir aussi */
wikitext
text/x-wiki
<noinclude>{{Linux}}</noinclude>
== Historique ==
À la création d'Internet, chaque ordinateur du réseau contenait un fichier '''/etc/hosts''' qui listait le nom de toutes les machines du réseau et leurs adresses IP. À chaque fois que l'on rajoutait une machine sur Internet, il fallait mettre à jour ce fichier.
Le nombre de machines connecté à Internet s'étant rapidement accru, cette solution de fichier '''/etc/hosts''' communs est devenu ingérable, et il a fallu inventer un procédé capable de palier ce problème.
La solution qui s'est imposée fut la création d'une base de données distribuée, et ainsi est né le principe de serveur DNS.
Un serveur DNS permet de faire la correspondance entre un nom canonique (ex : www.google.fr) et son adresse IP.
Le premier serveur DNS fut créé par l'université de Berkeley et s'appelle {{w|BIND}} (Berkeley Internet Name Domain). BIND est le serveur DNS le plus utilisé et le plus populaire, environ 79 % d'Internet fonctionne avec ce logiciel<ref>{{ouvrage|url=https://books.google.fr/books?id=0yOzQrMYLJQC&pg=PA73&lpg=PA73&dq=%22slightly+over+79+percent+of+DNS+servers+use+BIND%22&source=bl&ots=XX6JD4C7Jn&sig=VvK0fHHO1YApBiYHBGoHzqZMRLQ&hl=fr&sa=X&ei=JwxaVZ2_GYO-sAWE6YGwBg&ved=0CCEQ6AEwAA#v=onepage&q=%22slightly%20over%2079%20percent%20of%20DNS%20servers%20use%20BIND%22&f=false|lang=en|titre=Building a Server with FreeBSD 7: A Modular Approach|prénom1=Bryan|nom1=J. Hong|éditeur=No Starch Press, 2008}}</ref>.
== Configuration du client DNS ==
Sur un serveur Unix, la liste des serveurs DNS est définie dans le fichier '''/etc/resolv.conf'''.
$ cat /etc/resolv.conf
search mondomaine.fr
nameserver 192.168.30.1
== Principe de fonctionnement du DNS ==
Schéma : l'arbre à l'envers
.("point")
_______________|_______________...________
| | | | | | |
com net org fr ru edu ... arpa
| | |
google wikibooks in-addr
| | | | | | ... |
ug-in-f104 fr ru uk ... 0 1 255
___|___ ...
| | ... |
0 1 255
___|___ ...
| | ... |
0 1 255
___|___ ...
| | ... |
0 1 255
Au sommet de l'arbre on trouve des serveurs root qui aiguille vers les ''top level domain'' (com, net, org, fr, etc.)
Il existe une branche spéciale ARPA avec un sous domaine in-addr qui sert à gérer le reverse DNS.
== La commande host ==
La commande host permet d'obtenir l'adresse IP d'un ordinateur :
$ host www.google.com
www.google.com is an alias for www.l.google.com.
www.l.google.com has address 209.85.135.147
www.l.google.com has address 209.85.135.99
www.l.google.com has address 209.85.135.103
www.l.google.com has address 209.85.135.104
La commande '''host''' permet également de consulter le DNS inverse, c'est à dire quel nom canonique est associé à une adresse IP donnée :
$ host 66.249.93.104
104.93.249.66.in-addr.arpa domain name pointer ug-in-f104.google.com.
== La commande dig ==
La commande '''dig''' permet d'interroger un serveur DNS.
Voici quelques exemples :
En interrogeant le sommet de l'arbre '''.''', on obtient la liste des serveurs racines du DNS, appelés les root-servers<ref>http://www.root-servers.org/ www.root-servers.org</ref> :
$ dig . NS
...
;; ANSWER SECTION:
. 419748 IN NS a.root-servers.net.
. 419748 IN NS b.root-servers.net.
. 419748 IN NS c.root-servers.net.
. 419748 IN NS d.root-servers.net.
...
En interrogeant la branche '''com''', on obtient la liste des serveurs DNS gérant les noms de domaines en .com :
$ dig com. NS
...
;; ANSWER SECTION:
com. 172800 IN NS i.gtld-servers.net.
com. 172800 IN NS j.gtld-servers.net.
com. 172800 IN NS k.gtld-servers.net.
com. 172800 IN NS l.gtld-servers.net.
...
Si on interroge la branche '''fr''', on obtient la liste des serveurs DNS gérant les noms de domaines en .fr. On constate que les extensions nationales sont gérés par des organismes nationaux (dans le cas de la France, le NIC France) :
$ dig fr. NS
...
;; ANSWER SECTION:
fr. 172800 IN NS f.ext.nic.fr.
fr. 172800 IN NS a.ext.nic.fr.
fr. 172800 IN NS a.nic.fr.
fr. 172800 IN NS b.ext.nic.fr.
...
En indiquant un nom de domaine, '''dig''' permet de connaitre différentes informations, comme par exemple :
La liste des serveurs DNS gérant le nom de domaine :
$ dig google.fr NS
...
;; ANSWER SECTION:
google.fr. 175462 IN NS ns3.google.com.
google.fr. 175462 IN NS ns4.google.com.
google.fr. 175462 IN NS ns1.google.com.
google.fr. 175462 IN NS ns2.google.com.
...
La liste des serveurs de mails :
$ dig google.fr MX
...
;; ANSWER SECTION:
google.fr. 10800 IN MX 10 smtp4.google.com.
google.fr. 10800 IN MX 10 smtp1.google.com.
google.fr. 10800 IN MX 10 smtp2.google.com.
google.fr. 10800 IN MX 10 smtp3.google.com.
...
== Les Ressources Records (RR) ==
Les informations stockées dans un serveur DNS sont classifiées à l'aide des '''Ressources Records''' (RR).
Il existe de nombreux Ressources Records, voici les plus courants :
* NS (Name Server) indique les serveurs DNS gérant le nom de domaine. Exemple : '''dig google.com NS''' donne les name server de google.com
* A (Adresse IPv4) indique l'adresse IPv4 associée à un FQDN (Full Qualified Domain Name). Exemple : '''dig www.google.com A''' donne les adresses IPv4 de www.google.com
* AAAA (Adresse IPv6) indique l'adresse IPv6 associée à un FQDN (Full Qualified Domain Name). Exemple : '''dig www.google.com AAAA''' donne les adresses IPv6 de www.google.com
* MX (Mail eXchanger) indique le(s) serveur(s) de mail à contacter pour délivrer les emails du domaine. Exemple : '''dig google.fr MX''' donne les serveurs de mails acceptant des emails destinés à <un nom>@google.fr.
* CNAME (Canonical NAME) permet de créer des Alias (des noms étant des raccourcis vers d'autres noms). Exemple : '''host www.google.fr''' nous indique que '''www.google.fr''' est alias vers '''www.google.com'''.
* PTR (PoinTeuR) est utilisé par le reverse DNS pour effectuer la résolution d'une adresse IP vers un nom (FQDN). Exemple : '''host 72.14.207.99''' nous indique que l'adresse IP '''72.14.207.99''' est associé au nom '''eh-in-f99.google.com'''
== Installation de BIND ==
Pour installer le serveur BIND sous Debian, on utilise la commande suivante :
# apt-get install bind
À partir de Debian Lenny, le package contenant le serveur BIND s'appelle '''bind9'''
== Configuration de BIND ==
Les fichiers de configuration de BIND sont situés dans le répertoire '''/etc/bind'''.
Le fichier principal de configuration de BIND est '''/etc/bind/named.conf'''. Debian a choisit de découper ce fichier en 3 fichiers afin de faciliter les mises à jour.
À noter que dans ces fichiers, les lignes en commentaire commencent par '''//''' et non le '''#''' habituel des .conf, que l'on retrouve dans la syntaxe Apache.
=== /etc/bind/named.conf ===
# cat /etc/bind/named.conf
// Documentation : /usr/share/doc/bind/README.Debian
// Inclusion du fichier /etc/bind/named.conf.options
include "/etc/bind/named.conf.options";
// Configuration des logs
logging {
category lame-servers { null; };
category cname { null; };
};
// La zone définissant les root servers
zone "." {
type hint;
file "/etc/bind/db.root";
};
// La zone localhost
zone "localhost" {
type master;
file "/etc/bind/db.local";
};
// La zone inverse localhost
zone "127.in-addr.arpa" {
type master;
file "/etc/bind/db.127";
};
// La zone inverse réseau
zone "0.in-addr.arpa" {
type master;
file "/etc/bind/db.0";
};
// La zone inverse broadcast
zone "255.in-addr.arpa" {
type master;
file "/etc/bind/db.255";
};
// Inclusion du fichier /etc/bind/named.conf.local
include "/etc/bind/named.conf.local";
=== /etc/bind/named.conf.options ===
# cat /etc/bind/named.conf.options
options {
// Emplacement des zones si on ne spéficie pas de chemin absolu
directory "/var/cache/bind";
// Option désormais obsolète depuis BIND 8
fetch-glue no;
// Option pour changer le port par défaut
// query-source address * port 53;
// Option pour indiquer un DNS à qui on va renvoyer
// les demandes de résolution
// forwarders {
// 0.0.0.0;
// };
};
Dans ce fichier, il est possible de préciser dans la section '''forwarders''' l'adresse IP du DNS à qui l'on souhaite renvoyer les demandes de résolutions de noms. Par exemple, ceci est utile lorsque notre serveur DNS ne peut pas accéder directement à Internet.
Par défaut BIND écoute sur le port 53 en UDP. On peut également changer ce port dans ce fichier, mais ceci est délicat car il faudra accorder la configuration des clients en conséquence. À noter que le dossier '''/etc/services''' contient le numéro par défaut des ports de tous les services.
=== /etc/bind/named.conf.local ===
On va définir dans ce fichier nos zones locales.
<syntaxhighlight lang=apache>
# cat /etc/bind/named.conf.local
zone "mondomaine.fr" {
type master;
file "/etc/bind/db.mondomaine.fr";
};
zone "mondomaine2.fr" {
type master;
file "/etc/bind/db.mondomaine2.fr";
};
</syntaxhighlight>
=== Le fichier définissant la zone ===
On crée ensuite le fichier de zone '''/etc/bind/db.mondomaine.fr'''
# cat /etc/bind/db.mondomaine.fr
;
; BIND data file for mondomaine.fr
;
$TTL 604800
@ IN SOA dns.mondomaine.fr. root.mondomaine.fr. (
1 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS dns.mondomaine.fr.
dns IN A 192.168.30.210
;
srv1 IN A 192.168.30.211
;
@ IN MX 0 mail.mondomaine.fr.
;
mail IN A 192.168.30.210
alex IN CNAME mail
guillaume IN CNAME srv1
À noter que dans les fichiers de zone, les lignes en commentaire commencent par ''';''' et non le '''#''' habituel.
'''Points importants de ce fichier :'''
Le caractère @ (arobas) remplace le nom de la zone.
Lorsque l'on définit un nom canonique, on a deux possibilités :
* soit on donne le nom en entier (ex: pc210.mondomaine.fr.) . Dans ce cas-là, il ne faut pas oublier le point final, sinon le système rajoute automatiquement le nom de la zone (mondomaine.fr).
* soit on ne donne que le nom "court" (ex: alex). Dans ce cas-là, il ne met pas le point final afin que le système rajoute le nom de la zone.
=== Test de fonctionnement ===
Une fois que l'on a modifié ces fichiers, on relance le serveur DNS :
# /etc/init.d/bind restart
On modifie ensuite le fichier /etc/resolv.conf pour lui indiquer d'utiliser le DNS que l'on vient de configurer :
# vi /etc/resolv.conf
search mondomaine.fr
nameserver 127.0.0.1
Pour tester, on essaye de pinguer un nom définit dans le DNS :
# ping pc210.mondomaine.fr
Si tout se passe bien, le DNS doit effectuer la résolution.
On peut aussi utiliser les commandes '''host''' et '''dig''' pour vérifier :
# host pc210.mondomaine.fr
...
# host guillaume.mondomaine.fr
...
# dig mondomaine.fr MX
...
=== Le fichier définissant la zone inverse ===
Maintenant que l'on a configuré le DNS de la zone '''mondomaine.fr''', on va créer la zone inverse qui va permettre d'associer un nom à une adresse IP.
On rajoute tout d'abord la zone inverse dans le fichier '''named.conf.local''' :
// La zone reverse DNS
zone "30.168.192.in-addr.arpa" {
type master;
file "/etc/bind/db.192.168.30";
};
Le nom de la zone est composé de l'adresse réseau (à l'envers) associé à '''in-addr.arpa'''.
On crée ensuite le fichier '''/etc/bind/db.192.168.30''' :
# cat /etc/bind/db.192.168.30
;
; BIND data file for 192.168.30
;
$TTL 604800
@ IN SOA dns.mondomaine.fr. root.mondomaine.fr. (
1 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS dns.mondomaine.fr.
210 IN PTR pc210.mondomaine.fr.
211 IN PTR srv1.mondomaine.fr.
Le ressource record PTR permet de définir le nom associé à l'adresse IP.
Pour vérifier, on relance le DNS et on lui demande quelle nom est associé à une adresse IP donnée :
<syntaxhighlight lang="bash">
# /etc/init.d/bind restart
# host 192.168.30.210
...
</syntaxhighlight>
== Gestion des zones ==
[[Image:Webmin - BIND 3 - address records.PNG|vignette|upright=2|Ajout d'un enregistrement IN A dans Webmin.]]
Pour modifier les redirections DNS d'un domaine, il faut modifier sa zone :
<syntaxhighlight lang="bash">
vim /etc/bind/db.mondomaine.fr
vim /var/lib/bind/example.com.hosts
</syntaxhighlight>
Après modification des zones DNS, il s'avère nécessaire de demander sa propagation en cliquant tout en haut à droite sur ''Apply Configuration''. Cela équivaut à la commande RNDC pour "Remote Name Daemon Control" :
<syntaxhighlight lang="bash">
rndc reload
</syntaxhighlight>
Et éventuellement vérifier qu'il n'y a pas eu d'erreur :
<syntaxhighlight lang="bash">
tail -300 /var/log/syslog
</syntaxhighlight>
Cela permet par exemple de s'apercevoir que le numéro de série de la version de la zone doit être changé à chaque modification :
zone serial (999) unchanged. zone may fail to transfer to slaves.
Ce que [[../L'outil_d'administration_Webmin#Gestion_des_serveurs_DNS|L'interface graphique Webmin]], plus ergonomique pour mettre à jour les zones DNS, incrémente automatiquement.
{{clr}}
== Problèmes connus ==
=== SERVFAIL ===
<syntaxhighlight lang="bash">
dig @localhost example.com
; <<>> DiG 9.8.1-P1 <<>> @localhost example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 28241
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;example.com. IN A
;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Mon May 25 16:37:31 2015
;; MSG SIZE rcvd: 35
</syntaxhighlight>
Le serveur DNS mentionné (localhost dans l'exemple) ne connait pas le domaine, mais il peut parfois le résoudre avec <code>host</code>.
Si le localhost est censé être autoritaire et que la zone y semble bien définie, ajouter un point à la fin de chaque URL, pour éviter qu'il les interprète en ajoutant le domaine après. En effet :
* <code>example.com. IN NS ns.example2.com</code> pourra être interprété à tort :
* <code>example.com. IN NS ns.example2.com.example.com.</code>, mais pas :
* <code>example.com. IN NS ns.example2.com.</code>
=== NXDOMAIN ===
<syntaxhighlight lang="bash">
host example.com
Host example.com not found: 3(NXDOMAIN)
</syntaxhighlight>
Le domaine est inconnu des DNS, en général il suffit d'attendre la propagation 24 h.
Mais parfois, il manque juste un <code>rndc reload</code> ou un <code>/etc/init.d/bind9 restart</code>.
=== found SPF/TXT record but no SPF/SPF record found ===
Ces enregistrements vont par deux : <code>IN TXT</code> et <code>IN SPF</code>.
=== Le domaine ne se propage pas, telnet localhost 53 fonctionne en local mais pas de l'extérieur ===
Les symptômes sont les mêmes que si le port 53 était bloqué par le pare-feu, mais en fait il faut ajouter les IP publiques dans "/etc/bind/named.conf.options" :
listen-on-v6 { ::1; MonIPv6; };
listen-on { 127.0.0.1; MonIPv4; };
Puis redémarrer le service :
/etc/init.d/bind9 restart
=== query (cache) '...' denied ===
Retirer les IP locales de :
vim /etc/resolv.conf
<noinclude>[[Catégorie:Messages d'erreur]]</noinclude>
== Références ==
{{Références}}
<noinclude>
== Voir aussi ==
* [[Système de noms de domaine]]
* [[Sécurité des systèmes informatiques/Sécurité informatique/DNS avec BIND 9]]<!-- TODO appeler un modèle commun aux deux livres pour éviter les redondances -->
</noinclude>
35ourpr2ocye8kvgiwpbmmx4zm0ra00
La documentation/Langages documentaires à structure hiérarchique, classifications
0
35334
682099
607093
2022-07-21T07:37:38Z
DavidL
1746
/* La Classification Décimale de Dewey (CDD) */
wikitext
text/x-wiki
{{Techniques documentaires}}
== Généralités ==
De tous temps, l'administration des bibliothèques a fait apparaître le besoin de plans de classement. Les siècles passés ont vu naître de multiples tentatives d'organisation allant dans ce sens ; on peut citer par exemple les travaux du Français Lacroix du Maine (1584) ou de l'Américain Nathaniel B. Shurtleff (1856).
L'idée de remplacer les systèmes d'indexation à base de lettres et de chiffres romains a fait petit à petit son chemin. On doit au physicien André-Marie Ampère (1834) et au mathématicien hongrois Tarkas von Bolyai (1835) des classements décimaux destinés aux différents domaines scientifiques.
Cette notion de classement '''décimal''' est importante, car la numération à base 10 est l'un des rares éléments culturels communs à la quasi totalité des peuples du monde, ce qui n'est évidemment pas le cas pour les alphabets.
Les classifications ont donc été, historiquement, les premiers langages documentaires disponibles.
== Caractéristiques et conception des classifications ==
Précisons tout d'abord qu'il ne faut en aucun cas confondre '''classement''' et '''classification'''. Pour faire simple, le classement se réfère au matériel permettant de ranger les objets en fonction de leur nature (livres, films, diapositives, disquettes, échantillons industriels, etc.) tandis que la classification est un outil permettant d'organiser ces objets de façon logique. Comparaison n'est pas raison, mais on pourrait trouver une analogie dans le domaine informatique en considérant la distinction évidente entre matériel et logiciel.
=== Diverses sortes de classifications ===
Une '''classification''' ou '''plan de classement''' est un système organisé et hiérarchisé de classification d' « objets », ces derniers n'étant pas forcément des documents. Parmi les « objets » concernés par les classifications, on peut trouver par exemple les roches, les espèces animales et végétales vivantes, les maladies, les professions, les produits manufacturés, les étoiles, les brevets d'invention, etc. La diversité des « objets » pouvant être très grande, on imagine facilement qu'elle entraîne une diversité non moins grande des systèmes de classification.
Les classifications peuvent être générales ou encyclopédiques, comme la Classification de Dewey ou la Classification Décimale Universelle, qui recouvrent par définition tous les domaines de la connaissance ; d'autres se cantonnent à un domaine plus restreint, comme le Plan de Classement de l'Institut National de la Statistique et des Sciences Économiques (INSEE), la classification binominale des espèces vivantes ou encore la Classification Internationale des Maladies publiée par l'organisation Mondiale de la Santé (OMS) pour l'enregistrement des causes de morbidité et de mortalité touchant les êtres humains à travers le monde. L'appellation complète de cette dernière est « Classification statistique internationale des maladies et des problèmes de santé connexes » ou en anglais ''International Statistical Classification of Diseases and Related Health Problems'' ; en français on abrège souvent en CIM et en anglais, en ISC.
Les classifications « à champ étroit » sont généralement établies par les spécialistes du domaine concerné et sont généralement acceptées sans difficulté. En revanche les classifications à vocation universelle font l'objet d'un certain nombre de critiques : l'importance donnée à certains domaines est arbitraire, les spécialistes n'en utilisent qu'une partie très restreinte et par ailleurs elles se prêtent assez mal à l'informatisation.
En pratique les classifications sont très importantes pour organiser la vie et le travail de chacun d'entre nous et nous les pratiquons souvent à la façon de Monsieur Jourdain, sans même nous en rendre vraiment compte.
=== Création d'une classification ===
Dans une classification, les objets ou les concepts sont représentés par des codes, ou '''indices''', qui peuvent être numériques ou alphabétiques. Ces indices sont imbriqués, généralement par subdivision, chaque rubrique étant divisée en sous-rubriques, aussi loin qu'il le faut pour obtenir une description d'une finesse suffisante. De cette manière, un indice court représentera logiquement une notion générale, et un indice long une notion beaucoup plus spécifique. L'ensemble des '''classes''' et des '''sous-classes''' ainsi établies constitue un '''arbre hiérarchique'''.
D'après Vickery, l'établissement d'une classification passe par 7 étapes successives :
* analyse du domaine à couvrir, définition de ses limites, détermination des « choses » dont il s'agit de fixer l'ordre, établissement d'une liste de groupes de termes associés aux « choses » concernées,
* détermination des domaines spécifiés en premier par les « choses » et des catégories qui se retrouvent dans plus d'un domaine,
* groupement des termes de chaque catégorie dans un '''ordre utile''' qui facilitera les recherches ultérieures, examen de la ou des suite(s) hiérarchique(s),
* établissement des relations entre catégories, fixant l'ordre des combinaisons des catégories dans les sujets composés,
* recherche d'un moyen efficace et simple pour exprimer les relations entre les concepts, groupement des termes à utiliser selon ce que Ranganathan appelle des « facettes », ou sous-classes précisant les divers aspects des « choses ». Cela peut correspondre, par exemple, aux aspects théorique, pratique, économique... ; dans le domaine de l'aéronautique, on s'intéressera par exemple aux moteurs d'avions, aux infrastructures au sol, aux types d'appareils, à la sécurité, au pilotage... On distinguera ensuite des « sous-facettes », qui dans le dernier exemple choisi pourront être des distinctions entre les divers types d'avions, ceux destinés aux passagers, à l'armée, ceux qui sont propulsés par des hélices, etc.
* étude des relations des divers domaines entre eux et, autant que faire se peut, unification de l'ordre des termes,
* expression des relations des domaines envisagés avec les disciplines traditionnelles.
Comme l'écrit René Dubuc : « On voit la complexité de ce travail théorique et son ampleur ». C'est pourquoi... « il ne doit être entrepris qu'à bon escient et lorsqu'il n'existe aucune classification existante ou valable dans le domaine faisant l'objet des documents à classer. »
=== Critères de choix d'une classification ===
Les classifications bien étudiées présentent un cadre logique pour l'indexation des documents à traiter et une grande simplicité d'emploi pour les utilisateurs, ainsi que la possibilité, par simple modification de la longueur des codes, d'augmenter ou de restreindre le caractère général des concepts.
Les classifications présentent cependant quelques inconvénients, dont le plus Important est la rigidité qui rend difficiles les mises à jour. Il ne saurait en effet être question de modifier les indices tous les matins, surtout lorsque l'on travaille dans une bibliothèque de quelque importance regroupant plusieurs millions de références. Par ailleurs l'un des intérêts des classifications est qu'elles permettent une communication facile entre les organismes qui les utilisent en commun, de sorte que les modifications effectuées par l'un de ces organismes doivent impérativement être répercutées à tous les autres.
Signalons aussi que bon nombre d'arguments avancés contre les classifications en général, et en particulier contre les classifications encyclopédiques, n'ont guère de justification et ne s'expliquent que par une opposition irréductible et/ou un manque de pratique.
Le choix ou l'élaboration d'une classification dépend de la collection à traiter : si l'on a affaire à un ensemble de type « bibliothèque générale » de quelques centaines ou milliers de documents, on pourra utiliser assez facilement un classement de type encyclopédique, en choisissant bien les subdivisions et avec des indices assez courts. En revanche, dans un organisme spécialisé, on risque en appliquant une classification encyclopédique de devoir utiliser un nombre restreint d'indices qui seront alors généralement très longs et commenceront, pour la plupart d'entre eux, par les mêmes symboles. La tentation d'utiliser un système spécifique est alors assez grande mais les tentatives faites dans ce sens ne donnent pas toujours, tant s'en faut, le succès escompté.
Lorsqu'une collection comporte une partie générale et une partie spécialisée, ce qui est fréquent, seul un système encyclopédique convient. C'est encore le cas dans une entreprise où se développent de manière explosive certains secteurs d'activité, tandis que d'autres régressent. Seule une classification préexistante permet de faire face à ce genre de problème. Il faut aussi considérer la nature des documents : on n'accède pas de la même manière, par exemple, à un livre, à une diapositive, à une bande magnétique ou à une collection d'échantillons minéralogiques.
== La Classification Décimale de Dewey (CDD) ==
L'Américain Melvil Dewey (1851-1931) était en 1872 étudiant et assistant bibliothécaire à l'Amherst College (Massachussets). Le classement des ouvrages qu'il devait mettre à la disposition des lecteurs présentait de tels inconvénients qu'il entreprit de le réformer fondamentalement. Il devait par la suite consacrer l'ensemble de sa vie aux problèmes de bibliothéconomie. S'inspirant de travaux antérieurs, mais en les modifiant considérablement, Dewey imagina un nouveau découpage des connaissances humaines en dix grands domaines, chacun d'eux étant à son tour divisé en dix parties, et ainsi de suite.
La première table publiée en 1876 par Dewey, sous le titre ''A classification and subjects index for cataloguing the books and pamphlets of a library'', comportait 42 pages seulement. Elle connut un succès immédiat par son caractère international et sa facilité d'utilisation. Ce succès amena Dewey à publier en 1885 la « ''Decimal classification and relative index'' », une seconde édition considérablement augmentée, de 314 pages. Les subdivisions étaient développées au-delà du 3e chiffre.
La classification de Dewey est toujours largement utilisée dans les bibliothèques états-uniennes, bien que les choix de Dewey aient été critiqués dès l'origine. Ils reflètent en tous cas la conception d'ensemble des connaissances humaines que l'on pouvait avoir outre-Atlantique à la fin du XIXe siècle.
Pour plus de détails sur cette classification, le lecteur pourra se reporter à l'article de Wikipédia : [[w:Classification décimale de Dewey|Classification décimale de Dewey]].
== La Classification Décimale Universelle (CDU) ==
C'est elle qui sert actuellement à l'inventaire des wikilivres français, voir les tables sur [[Wikilivres:CDU]].
=== Historique ===
Deux avocats belges, [[w:Paul Otlet|Paul Otlet]] (1868 - 1944) et [[w:Henry La Fontaine|Henry La Fontaine]] (1853 - 1943), fondateurs de l’Institut International de Bibliographie en 1895, prirent l'initiative d'adapter et d'assouplir la classification de Dewey, avec son autorisation. Les éditions de leur œuvre, la '''Classification Décimale Universelle''', se sont succédé à partir de 1927, elles contiennent actuellement environ 150 000 sujets et sont traduites dans une vingtaine de langues différentes.
Otlet publia en 1934 un « Traité de documentation » qui reste, malgré certains passages maintenant dépassés, un ouvrage fondamental à bien des égards. Infatigable travailleur de la coopération internationale, il élabora de multiples projets d 'organismes mondiaux aboutissant à la création en 1937 de la Fédération Internationale de Documentation (FID), qui a poursuivi ses travaux jusqu'en 2002, date de sa dissolution. La Fontaine, Président du Bureau International de la Paix en 1907, fut en 1913 lauréat du Prix Nobel de la Paix.
La Classification Décimale Universelle ( C.D.U. ) offre actuellement des possibilités d'utilisation bien plus étendues que la Classification de DEWEY. Elle n'est toutefois pas exempte de défauts qui tiennent en grande partie, comme pour cette dernière, au choix des divisions principales dans lesquelles on peut voir le reflet de l'époque où elles ont été définies.
=== Principe ===
Une classification décimale est un schéma systématique de classement utilisant une notation à forme décimale. On ne considère pas ici les nombres comme des entiers, mais comme des nombres décimaux dont on aurait enlevé le zéro et la virgule initiaux. Nous verrons plus loin pourquoi. Chaque nombre, ou plutôt chaque indice, pourra donc toujours être divisé en un maximum de dix indices de rang immédiatement inférieur. Il n'est d'ailleurs pas absolument obligatoire d'utiliser les dix divisions disponibles.
Comme la Classification de Dewey, la Classification Décimale Universelle utilise trois principes de base :
* on classe toujours en partant de l''''idée''' contenue dans le document, de sorte que toutes les notions relatives à un même ensemble de concepts vont se trouver automatiquement rapprochées dans les tables,
* on classe '''tout''', à l'aide d'indices simples pour les documents relatifs à un domaine bien défini ou à l'aide d'indices composés si le document traite de plusieurs sujets présentant des rapports entre eux, ou encore s'il s'agit de préciser des notions de forme, de langue, de temps, de lieu... On remarquera facilement que dans la CDU il ne peut exister aucune rubrique « divers ».
* on classe toujours en allant '''du général au particulier''' en utilisant les divisions successives en dixièmes, centièmes, millièmes... et ainsi de suite jusqu'au degré de précision nécessaire.
Les classifications généralistes telles que la Classification de Dewey ou la Classification Décimale Universelle couvrent l'ensemble des activités et des connaissances humaines, mais d'autres classifications ne concernent qu'une partie plus restreinte, comme par exemple la faune, la flore, etc.
Une subdivision est entièrement englobée dans la division de niveau supérieur qui la précède, et elle englobe entièrement toutes les subdivisions de niveau inférieur qui la suivent. On constitue ainsi une arborescence, dans laquelle un concept donné ne peut occuper qu'une place et une seule, bien déterminée.
=== Structure générale ===
L'ensemble des connaissances humaines est considéré comme l''''unité''', que l'on divise en dix classes principales définies comme les nombres décimaux suivants :
: 0,0 Généralités, documentation, écritures,...
: 0,1 Philosophie, psychologie
: 0,2 Théologie
: 0,3 ...
Arrêtons ici : il est clair que tous les indices vont avoir pour premiers symboles : « '''0,''' ». Or, il est non moins clair que l'on cherche à écrire des indices dont chacun représente une notion distincte. Les deux caractères « 0, », communs à tous les indices, n'apportent aucune information intéressante et en conséquence on ne les écrira donc pas, mais ils existent et il faut se le rappeler. Reprenons :
: 0 Généralités « en général », documentation, écritures...
: 1 Philosophie, psychologie
: 2 Théologie
: 3 Sciences sociales, économie, droit
: 4 n'est plus attribué pour l'instant
: 5 Mathématiques, sciences physiques, chimie, sciences naturelles
: 6 Sciences appliquées, techniques
: 7 Beaux - Arts
: 8 Littérature, linguistique
: 9 Histoire et géographie
On continue de subdiviser, en utilisant toujours la subdivision 0 pour des généralités : par exemple, pour la partie 6 :
:: 60 Généralités sur les sciences appliquées
:: 61 Médecine, pharmacie
:: 62 Art de l'ingénieur
:: 63 Agriculture...
Et ainsi de suite, la partie 62 donnera selon le même principe :
::: 620 Généralités, essais des matériaux, énergie
::: 621 Électrotechnique, mécanique industrielle
::: 622 Mines
::: 623 Génie militaire
::: 624 Génie civil
Au-delà, malgré certaines irrégularités dues pour l'essentiel à des problèmes de mises à jour, il est d'usage de couper les indices trop longs par tranches de trois chiffres séparés par des '''points'''. Pour les étourdis, rappelons que les chiffres sont comptés à partir de la virgule, donc de la gauche...
:::: 621.32 Lampes électriques
:::: 621.321 Lampes à arc
:::: 621.326 Lampes à incandescence
:::: 621.313.13 Moteurs électriques
:::: 621.313.13'''0''' Généralités sur les moteurs électriques
etc.
Attention au piège des nombres décimaux. Quel est le plus petit des trois indices suivants ?
73, 221, 637 ???
C'est évidemment (?) 221
Et le plus grand ? C'est 73
Vous en doutez ? Alors remettez en place le zéro et la virgule qui ont été enlevés :
0,221 < 0,637 < 0,73
... CQFD
=== Signes et symboles ===
Si l'on doit classer un document dont le contenu peut être caractérisé par un concept simple, il suffit d'employer l'indice CDU correspondant. En pratique ce cas est assez rare. Le plus souvent, les documents à classer font référence à plusieurs concepts étudiés les uns par rapport aux autres. Il a donc fallu choisir un signe de relation, qui est conventionnellement « : » (deux points) ; voici par exemple deux indices :
: 621.78 traitement thermique des métaux
: 669.15 aciers alliés
qui peuvent donner deux combinaisons :
: 621.78:669.15 traitement thermique des aciers alliés
: 669.15:621.78 aciers alliés pour traitements thermiques
La première combinaison se rapporterait plutôt à un document théorique concernant le traitement thermique des aciers, la seconde au catalogue d'un fournisseur d'aciers spécialement destinés au traitement thermique.
En principe l'ordre dans lequel on écrit les indices correspond à leur importance relative. Le nombre des notions qui peuvent être reliées n'est pas limité :
: 621.9.02 outils de coupe
: 621.78:669.15:621.9.02 traitement thermique des aciers alliés pour outils de coupe
Les documentalistes ont inventé diverses méthodes simples pour l'indexation des notions complexes.
* '''Addition''' : Lorsque l'on trouve dans un document deux concepts seulement juxtaposés, et non en relation l'un avec l'autre, alors on peut utiliser le signe « + » comme signe d'addition :
: 54 + 66 chimie théorique et appliquée
* '''Extension''' : Lorsqu'un document contient un ensemble de concepts dont les indices se suivent dans la table, on utilise la barre oblique « / » comme signe d'extension :
: 621.56/.59 technique du froid
:: À noter que l'on doit répéter le dernier point et tous les signes qui le suivent.
* '''Intercalation''' : Les crochets « [...] » constituent le signe d'intercalation qui contient plusieurs notions juxtaposées mais en relation avec une autre qui se trouve en quelque sorte mise en facteur :
: 621.315.2:[629.113 + 629.135.21] câbles électriques pour automobiles et avions
* '''Synthèse''' : l'apostrophe « ' » est le signe de synthèse, dont l'emploi est autorisé seulement dans un certain nombre de sections particulières :
: 546.41 calcium et 546.226 sulfate
donnent
: 546.41'226 sulfate de calcium
=== Divisions auxiliaires ===
* '''Divisions analytiques''' : ces divisions existent dans un grand nombre de sections de la table principale, en vue d'une plus grande précision dans la description du sujet traité. On les note avec un tiret « - » ou un point et un zéro « .0 ». Par exemple :
: 54-325 ortho-acides
: 66.047 séchage (industriel)
: 62-72 , dispositifs de graissage, est applicable à tous les indices qui commencent par 62/69 ...
: 621.614-72 dispositifs de graissage pour souffleries à pistons rotatifs
: 648.23-72 dispositifs de graissage pour machines à laver
: 693.542.52-72 dispositifs de graissage pour malaxeurs à béton
* '''Divisions communes de langue''' : elles symbolisent la langue dans laquelle est rédigé le document et sont formées à partir de la classe principale 8 et caractérisées par le signe « égal ».
: =00 polyglotte
: =20 anglais
: =30 allemand
: =40 français
: =50 italien
: =60 espagnol
: =82russe
: =916.9 basque
: =927 arabe
: =951 chinois
: =956 japonais
: etc.
: 860=20 littérature espagnole (en anglais)
* '''Divisions communes de forme''' : ces divisions se rapportent à la forme des documents écrits mais également à beaucoup d'autres objets. Elles sont placées entre parenthèses et commencent par zéro ; quelques exemples :
: (02) exposé systématique sous forme de livre
: (03) encyclopédies, dictionnaires, ...
: (04) brochures, exposés, conférences, lettres, articles,
: (05) publications périodiques, revues
: (07) enseignement, étude
: (08) polygraphies, collections
: (09) sources historiques et juridiques
: 72(021) manuel d'architecture
: 621.914.4(083.96) instructions pour l'emploi des machines à fraiser
* '''Divisions communes de lieu''' : elles sont incluses dans des parenthèses et commencent par un chiffre de 1 à 9 :
voici quelques exemples :
: 326.1(37) le commerce des esclaves dans la Rome ancienne
: 336.711(410) la Banque d'Angleterre
: 676(480) l'industrie du papier en Finlande
On peut en cas de besoin ajouter des précisions de lieu en plaçant celui-ci en toutes lettres à la fin de l'indice :
: 746.2(493.2 Bruxelles) les dentelles de Bruxelles
* '''Divisions communes de races et de peuples''' : elles sont formées à partir des divisions de langue mises entre parenthèses :
: 299.9(=995) religion des Papous
: 393.9(=916)coutumes funéraires des Basques
* '''Divisions communes de temps''' : elles sont mises entre guillemets :
: 341.382"1648" la conclusion de la paix de Westphalie en 1648
: 551.213(324.41 Vésuve)"0079" l'éruption du Vésuve en l'an 79
: 631.548.2"324" couverture des plantes en hiver
* '''Divisions communes de point de vue''' : elles rendent possible une subdivision poussée lorsqu'il n'existe pas de division analytique appropriée. Elles sont formées à l'aide d'un point suivi de deux zéros : .00
: .001 point de vue théorique
: .002 point de vue de la réalisation
: .003 point de vue économique et financier
: .004 point de vue de l'utilisation et du fonctionnement
: .005 point de vue de l'aménagement et de l'équipement
: .006 point de vue des locaux et des emplacements
: .007 point de vue du personnel
: .008 point de vue de l'organisation
: .009 point de vue social et moral
: 621.831.001 théorie des engrenages
: 674.004.8 utilisation des déchets dans l'industrie du bois
* '''Utilisation de lettres ou de noms''' : La précision d'un nom peut être considérée comme une forme particulière de subdivision :
: 235.3 Paul Saint Paul
: 634.11 Golden pommes de la variété Golden
: 737.1 Trajan monnaie à l'effigie de l'empereur Trajan
* '''Notations personnelles''' : si, après avoir épuisé toutes les possibilités offertes par les tables le classificateur se trouve dans l'obligation de subdiviser encore plus loin, le seul expédient consiste à utiliser des subdivisions personnelles. Il faut alors séparer clairement la notation CDU de l'autre, ce qui se fait le plus souvent en utilisant la lettre « p » :
: 621.882.215p60 vis à tête ronde de 60 mm de long
== La CDU comme thésaurus ==
Cette utilisation sera envisagée dans le chapitre consacré aux thésaurus.
== Utilisation dans le monde ==
En France, la classification décimale universelle a été utilisée dans la plupart des bibliothèques universitaires mais elle régresse depuis la fin des années 1980, au profit de la classification décimale de Dewey. Elle reste encore en usage dans les centres de documentation et d'information des établissements scolaires du secondaire (essentiellement dans les lycées) et dans de nombreuses bibliothèques publiques ou privées.
En Belgique francophone, l'utilisation de la CDU est requise pour toute bibliothèque souhaitant être reconnue - et donc subventionnée - par la Communauté française de Belgique.
Un aperçu des pays utilisant la CDU (en 2004) est accessible sur le site de l'UDC Consortium : http://www.udcc.org/countries.htm Countries with UDC users.
== Éditions de la CDU ==
La CDU complète n'est disponible que sous forme informatisée auprès de l'UDC Consortium (sous licence). Il s'agit du ''Master Reference File'' (MRF). Il existe deux versions francophones ayant pour but de proposer une CDU plus compacte et plus utilisable: une "moyenne" (3 volumes) et une "abrégée" (1 volume). Les éditions du Céfal (Liège, Belgique) possèdent le monopole de l'édition de la CDU en français.
* Classification décimale universelle : édition abrégée .- Liège, Éditions du Céfal, 2001, 292 p. {{ISBN|2-87130-100-X}}
* Classification décimale universelle : édition moyenne internationale - Tables auxiliaires, classes 0 à 5 .- Liège, Éditions du Céfal, 2004, 421 p. {{ISBN|2-87130-151-4}}
* Classification décimale universelle : édition moyenne internationale - Classes 6 à 9 .- Liège, Éditions du Céfal, 2004, 495 p. {{ISBN|2-87130-152-2}}
* Classification décimale universelle : édition moyenne internationale - Index .- Liège, Éditions du Céfal, 2004, 319 p. {{ISBN|2-87130-153-0}}
== Liens externes ==
* ({{en}}) [http://www.udcc.org Site officiel du consortium CDU]
* ({{fr}}) [http://projetconnaissance.free.fr/classement/classements/connaissance.html Extrait de la CDU] (Certaines classes ont depuis connu diverses modifications, notamment la classe 2 concernant les religions)
0szyzxjhn7ljvgcyle7i1ddt249s0m7
Photographie/Personnalités/G/John C. H. Grabill
0
38895
682055
369913
2022-07-20T18:19:08Z
777sms
110598
([[c:GR|GR]]) [[c:COM:FR|File renamed]]: [[File:Grabill - At the Dance-2.jpg]] → [[File:Miniconjou Indian Grass Dance on Reservation by Grabill 1890.jpg]] [[c:COM:FR#FR2|Criterion 2]] (meaningless or ambiguous name) · Ambiguous description, improv file name to better describe it.
wikitext
text/x-wiki
{{Ph s Personnalités}}
'''John C. H. Grabill''' était un photographe états-unien.
== Galerie d'images ==
<gallery>
File:Grabill - Colorado.jpg
File:Grabill - Castle Rock.jpg
File:Grabill - Crow Butte.jpg
File:Grabill - Custer City.jpg
File:Grabill - Chinese service.jpg
File:Grabill - Branding cattle.jpg
File:Grabill - Bucking Bronco.jpg
File:Grabill - Dinner scene.jpg
File:Grabill - Echo Canyon.jpg
File:Grabill - Hunting Deer.jpg
File:Grabill - Hot Springs.jpg
File:Grabill - Harney Range.jpg
File:Grabill - Harneys Peak.jpg
File:Grabill - Grand review.jpg
File:Grabill - Gold Fever.jpg
File:Grabill - Indian Warriors.jpg
File:Grabill - Montana Mine.jpg
File:Grabill - Minnekahta Falls.jpg
File:Grabill - Phantom Ridge.jpg
File:Grabill - The Cavalier.jpg
File:Grabill - Tallyho Coaching.jpg
File:Grabill - Deadwood celebration.jpg
File:Grabill - Signal Rock.jpg
File:Grabill - The Interview.jpg
File:Grabill - The Cow Boy-edit.jpg
File:Grabill - Cattle round up.jpg
File:Grabill - At beef issue.jpg
File:Grabill - A Dear Picture.jpg
File:Grabill - Galena, South Dakota.jpg
File:Grabill - Devils Tower-6.jpg
File:Grabill - Devils Tower-7.jpg
File:Grabill - Little Missouri Butte.jpg
File:Grabill - Devils Tower-4.jpg
File:Grabill - Devils Tower-3.jpg
File:Grabill - Devils Tower-2.jpg
File:Grabill - Devils Tower-1.jpg
File:Grabill - Devils Tower-5.jpg
File:Grabill - Horse Shoe Curve.jpg
File:Grabill - Hostile Indian camp.jpg
File:Grabill - Roping gray wolf.jpg
File:Grabill - Spearfish Falls-1.jpg
File:Grabill - Spearfish Falls-2.jpg
File:Grabill - Skinning Beef-1.jpg
File:Grabill - Skinning Beef-2.jpg
File:Grabill - Western Ranch House.jpg
File:Grabill - Calamnity Peak-2.jpg
File:Grabill - Calamnity Peak-1.jpg
File:Grabill - Deadwood from Engleside.jpg
File:Grabill - Deadwood City Hall.jpg
File:Grabill - Elk Canyon-2.jpg
File:Grabill - Elk Canyon-3.jpg
File:Grabill - Elk Canyon-1.jpg
File:Grabill - Wild Bills Monument.jpg
File:Grabill - Wells Fargo Express.jpg
File:Grabill - Black Hills treasure coach.jpg
File:Miniconjou Indian Grass Dance on Reservation by Grabill 1890.jpg
File:Grabill - At the dance-1.jpg
File:Grabill - General Miles and staff.jpg
File:Grabill - Dobbins Mills, Black Hills.jpg
File:Grabill - Lake Harney Peaks-2.jpg
File:Grabill - Lake Harney Peaks-1.jpg
File:Grabill - Deadwood from White Rocks.jpg
File:Grabill - Hot Springs, Minnekahta, Ave.jpg
File:Grabill - Hotel Minnekahta, Hot Springs.jpg
File:Grabill - Happy Hours in Camp.jpg
File:Grabill - Ox teams at Sturgis.jpg
File:Grabill - The Frogs Head Rock.jpg
File:Grabill - The Great Hostile Camp.jpg
File:Grabill - The Indian Girls Home.jpg
File:Grabill - The Deadwood Coach-1.jpg
File:Grabill - Villa of Brule-1.jpg
File:Grabill - Washing and panning gold.jpg
File:Grabill - Villa of Brule-2.jpg
File:Grabill - The old cabin home.jpg
File:Grabill - The Deadwood Coach-2.jpg
File:Grabill - Chief Rocky Bears home.jpg
File:Grabill - Viewing Hostile Indian Camp.jpg
File:Grabill - Deadwood from Mt Moriah.jpg
File:Grabill - The Deadwood Reduction Works.jpg
File:Grabill - Deadwood from McGovern Hill.jpg
File:Grabill - Deadwood from Forest Hill.jpg
File:Grabill - Red Cloud and American Horse.jpg
File:Grabill - Beef issue to Indians-2.jpg
File:Grabill - Beef issue to Indians-1.jpg
File:Grabill - Mess scene on round up.jpg
File:Grabill - Indian Council in Hostile Camp.jpg
File:Grabill - Devils Tower or Bear Lodge.jpg
File:Grabill - Home of Mrs American Horse.jpg
File:Grabill - Old Man of the park.jpg
File:Grabill - The last Deadwood Coach-3.jpg
File:Grabill - The last Deadwood Coach-2.jpg
File:Grabill - The last Deadwood Coach-1.jpg
File:Grabill - The fighting 7th officers.jpg
File:Grabill - Round-up at Moss Agate.jpg
File:Grabill - Placer mining at Rockerville, Dakota.jpg
File:Grabill - Three of Uncle Sams Pets.jpg
File:Grabill - The Officers Line, Fort Meade.jpg
File:Grabill - The shepherd and flock-2.jpg
File:Grabill - The last Deadwood Coach-4.jpg
File:Grabill - The shepherd and flock-1.jpg
File:Grabill - Hydraulic mining at Rockerville, Dakota.jpg
File:Grabill - Distant view of train 1890.jpg
File:Grabill - Deadwood from Mrs Livingstons Hill.jpg
File:Grabill - Deadwood Central RR Engineer Corps-2.jpg
File:Grabill - Deadwood Central RR Engineer Corps-1.jpg
File:Grabill - Capt. Taylor and 70 Indian scouts.jpg
File:Grabill - Freighting in the Black Hills-4.jpg
File:Grabill - Freighting in the Black Hills-3.jpg
File:Grabill - Engineers Corps camp and visitors-2.jpg
File:Grabill - Freighting in the Black Hills-1.jpg
File:Grabill - Freighting in the Black Hills-2.jpg
File:Grabill - Engineers Corps camp and visitors-1.jpg
File:Grabill - Indian chiefs and US officials-1.jpg
File:Grabill - Indian chiefs and US officials-3.jpg
File:Grabill - Hot Springs from Club House Hill.jpg
File:Grabill - Our picnic party at Sunday Gulch.jpg
File:Grabill - Officers of the 9th Cavalry.jpg
File:Grabill - Tasunka, Ota (alias Plenty Horses)-3.jpg
File:Grabill - Tasunka, Ota (alias Plenty Horses)-1.jpg
File:Grabill - Tasunka, Ota (alias Plenty Horses)-2.jpg
File:Grabill - Scenery on Deadwood Road to Sturgis.jpg
File:Grabill - Whats left of Big Foots band.jpg
File:Grabill - Wi-wi-la-kah-ta canon.jpg
File:Grabill - A pretty group at an Indian tent.jpg
File:Grabill - Dick Latham of Iron Mountain returning home.jpg
File:Grabill - Hot Springs, the Minnekahta and Gillispie Hotels.jpg
File:Grabill - Gunners of Battery E 1st Artillery.jpg
File:Grabill - Minnekata Ave from Soldiers Home, Hot Springs.jpg
File:Grabill - Whitewood Canyon, Wade and Jones RR Camp.jpg
File:Cattle branding (Grabill 1888).jpg
File:Grabill - Deadwood and Delaware Smelter at Deadwood, South Dakota.jpg
File:Grabill - A wonderful blast in building railroad to Deadwood.jpg
File:Grabill - Fort Meade, Dakota, Bear Butte, 3 miles distant.jpg
File:Grabill - Indian chiefs who counciled with General Miles-2.jpg
File:Grabill - Indian chiefs who counciled with General Miles-1.jpg
File:Grabill - Roundup scenes on Belle Fouche in 1887-3.jpg
File:Grabill - Roundup scenes on Belle Fouche in 1887-2.jpg
File:Grabill - Roundup scenes on Belle Fouche in 1887-1.jpg
File:Grabill - Roping and changing, Changing horses on round up.jpg
File:Grabill - Part of Deadwood as seen from big flume.jpg
File:Grabill - The Cow Boy.jpg
File:Grabill - Comanche, the only survivor of the Custer Massacre, 1876.jpg
File:Grabill - General Brooks Camp near Pine Ridge, January 17, 1891.jpg
File:Grabill - The Bar (-T) Tee Ranch located on Hat Creek.jpg
File:Grabill - Part of the great Homestake works, Lead City, Dakota.jpg
File:Grabill - Little, the instigator of Indian revolt at Pine Ridge, 1890.jpg
File:Grabill - On the Burlington and Missouri River Railway near Hot Springs.jpg
File:Grabill - Past Grand Masters of Dakota Independent Order of Odd Fellows.jpg
File:Grabill - US troops after surrender of Indians at Pine Ridge Agency.jpg
File:Grabill - People of Deadwood celebrating completion of a stretch of railroad.jpg
File:Grabill - Hot Springs, interior view of largest plunge bath house in US.jpg
File:Grabill - The US Paymaster and Guards on Deadwood road to Ft Meade.jpg
File:Grabill - Deadwood People celebrating the building of DORR road to Lead City.jpg
File:Grabill - Lead City Mines and Mills, the Great Homestake Mines and Mills.jpg
File:Cattle branding (Grabill 1888, cropped).png
File:Grabill - Branding calves on roundup.jpg
File:Grabill - The Columbian Parade, Oct. 20th, 1892.jpg
File:Grabill - Survivors of Big Foots band.jpg
File:Grabill - De Smet Gold Stamp Mill, Central City, Dakota.jpg
File:Grabill - Near Fort Meade. I troop, 8th Cavalry.jpg
File:Grabill - Indian chiefs and US officials-2.jpg
File:Grabill - Open cut in the great Homestake mine, at Lead City, Dakota.jpg
File:Grabill - Company C, 3rd U.S. Infantry near Fort Meade-3.jpg
File:Grabill - Company C, 3rd U.S. Infantry near Fort Meade-4.jpg
File:Grabill - Company C, 3rd U.S. Infantry near Fort Meade-2.jpg
File:Grabill - Company C, 3rd U.S. Infantry near Fort Meade-1.jpg
File:Grabill - Grand Lodge IOOF of Dakotas, Street Parade, May 21, 1890.jpg
File:Grabill - Deadwood, Dakota. A part of the city from Forest Hill.jpg
File:Grabill - Clean Up day at the Deadwood Terra Gold Stamp Mill.jpg
File:Grabill - Little, the instigator of Indian Revolt at Pine Ridge, 1890-3.jpg
File:Grabill - Little, the instigator of Indian Revolt at Pine Ridge, 1890-2.jpg
File:Grabill - Hot Springs, exterior view of largest plunge bath house in US.jpg
File:Grabill - Little, the instigator of Indian Revolt at Pine Ridge, 1890-5.jpg
File:Grabill - Little, the instigator of Indian Revolt at Pine Ridge, 1890-4.jpg
File:Grabill - Little, the instigator of Indian Revolt at Pine Ridge, 1890-1.jpg
File:Grabill - Omaha Board of Trade in Mountains near Deadwood, April 26, 1889.jpg
File:Grabill - Wood shooting in the air, De Smet Mill, Center City, Dakota.jpg
File:Grabill - Famous Battery E of 1st Artillery.jpg
File:Grabill - US School for Indians at Pine Ridge.jpg
File:Grabill - The great Hub-and-Hub race at Deadwood, Dakota, July 4, 1888.jpg
File:Grabill - Company C, 3rd U.S. Infantry, caught on the fly, near Fort Meade.jpg
File:Grabill - Camp of the 7th Cavalry, Pine Ridge Agency, S.D., Jan. 19, 1891.jpg
File:Grabill - General Miles and staff viewing the largest hostile Indian Camp in the US.jpg
File:Grabill - The last large bull train on its way from the railroad to the Black Hills.jpg
File:Grabill - Grand Lodge IOOF of the Dakotas, resting in front of City Hall after the Grand Parade, May 21, 1890.jpg
File:Grabill - The champion Chinese Hose Team of America, who won the great Hub-and-Hub race at Deadwood, Dakota, July 4th, 1888.jpg
File:The Cow Boy 1888.jpg
File:Lakota.jpg
File:US school for indians pine ridge.png
File:Hotchkiss gun wounded knee.gif
File:Oglala girl in front of a tipi2.jpg
File:Oglala girl in front of a tipi.jpg
File:Homestake works mine 1889.jpg
</gallery>
{{DEFAULTSORT:Grabill, John}}
[[Catégorie:Personnalités de la photographie]]
9h1d5hn6gx9i5bfot2rk9ooyj9b7u1i
Modèle:CDU/Documentation
10
48333
682100
641819
2022-07-21T07:39:43Z
DavidL
1746
wikitext
text/x-wiki
Ce modèle est à inclure dans les livres référencés dans la [[Wikilivres:CDU|CDU]].
== Utilisation ==
<nowiki>{{</nowiki>CDU|''code-CDU''|''sous-code-CDU''|flotte=''alignement''|largeur=''largeur''|dégage=''dégage''|ligne=1}}
== Paramètres non nommés ==
;''code-CDU'':Code [[Wikilivres:CDU|CDU]] principal.
;''sous-code-CDU'':Sous-code [[Wikilivres:CDU|CDU]].
== Paramètres nommés ==
;flotte:Alignement : gauche, centre, droite, aucun (par défaut).
;largeur:Largeur du cadre en pourcentage (40% par défaut).
;dégage:Côté à dégager (''left'' à gauche, ''right'' à droite, ''both'' les deux ; ''both'' par défaut)
;ligne=1:Pour afficher sur une seule ligne au lieu d'une liste à un seul item.
== Exemple ==
Pour classer un livre en CDU 681.3.0, utilisez sur sa page de couverture :
<nowiki>{{CDU|6/68/681|681.3/681.3.0}}</nowiki>
Ce qui affichera :
{{CDU|6/68/681|681.3/681.3.0}}
Même exemple sur une ligne (la largeur doit être augmentée également) :
<nowiki>{{CDU|6/68/681|681.3/681.3.0|ligne=1|largeur=60}}</nowiki>
Ce qui affichera :
{{CDU|6/68/681|681.3/681.3.0|ligne=1|largeur=60}}
N'oubliez pas également de '''mettre à jour la page CDU en question''' pour ajouter manuellement un lien vers le livre classé.
== Modèles liés ==
* [[Modèle:CDU multiple]] pour les livres ayant plusieurs indices.
* [[Modèle:CDU item]] pour une présentation simple de classement utilisable avec {{m|CDU multiple}} et {{m|Page de garde}}.
[[Catégorie:Modèles pour les livres]]
oi0j1b36cjtoegie7j4e7ts9v017415
Fonctionnement d'un ordinateur/Le préchargement
0
65804
682071
648996
2022-07-20T20:41:04Z
Mewtow
31375
/* Le préchargement par prédiction */
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger des données proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher''. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
==Le préchargement des données==
Les ''prefetchers'' pour les données sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case.
===Les ''Prefetchers'' séquentiels===
Les ''prefetchers'' séquentiels préchargent les données immédiatement consécutives de la donnée venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. Avec la première, le ''prefetcher'' va calculer une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il va calculer le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrêtera temporairement de précharger. Autre solution : garder un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils ont comme origine les parcours de tableaux multidimensionnels et de tableaux de structures/objets. Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. Pour cela, le processeur contient une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. A chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=2|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==Le préchargement des instructions==
Certains processeurs utilisent un ''prefetcher'' séparé pour les instructions, chose qui fonctionne à la perfection si le cache d'instruction est séparé des caches de données. Les techniques utilisés par les ''préfetchers'' d'instructions sont multiples et nombreux sont ceux qui réutilisent les techniques vues précédemment. Le préchargement séquentiel est notamment utilisé sur certains ''prefetchers'' relativement simples, de même que les ''prefetchers'' de Markov. Mais certaines techniques sont spécifiques au préchargement des instructions. Il faut dire que les branchements sont une spécificité du flux d'instruction, qui doit être prise en compte par le ''prefetcher'' pour obtenir un résultat optimal.
===Le préchargement séquentiel, le retour !===
Le préchargement séquentiel est adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Mais un ''prefetcher'' purement séquentiel gère mal les branchements.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
Sur certains processeurs, cette technique est utilisée non seulement pour charger des instructions dans le cache, mais aussi pour charger à l'avance certaines instructions dans le séquenceur. Sur ces processeurs, l'unité de chargement et de décodage sont séparées par une petite mémoire tapon de type FIFO : le '''tampon d’instructions''' (''instruction buffer''). En utilisant du préchargement séquentiel, on peut précharger des instructions dans le tampon d’instructions à l'avance, permettant ainsi de masquer certains accès au cache ou à la mémoire assez longs. Lorsqu'un branchement est décodé, ce tampon d’instructions est totalement vidé de son contenu.
===Le ''Target line prefetching''===
Pour gérer au mieux les branchements, il faudrait trouver un moyen de connaître l'adresse de destination. Néanmoins, le processeur peut supposer que l'adresse de destination est fixe : il suffit de s'en souvenir pour la prochaine fois. C'est ce qu'on appelle le '''''target line prefetching'''''.
[[File:Target line prefetching.png|centre|vignette|upright=2|Target line prefetching.]]
Pour implémenter cette technique, le ''prefetcher'' incorpore un cache pour stocker les adresses de destination des branchements déjà rencontrés. Plus précisément, ce cache contient les correspondances entre une ligne de cache, et la ligne de cache à charger à la suite de celle-ci. Pour plus d'efficacité, certains processeurs ne stockent pas les correspondances entre lignes de cache consécutives. Si deux lignes de cache sont consécutives, on fait face à un défaut de cache dans la mémoire qui stocke nos correspondances. Le ''prefetcher'' utilise alors automatiquement un préchargement séquentiel. Ainsi, la table de correspondances est remplie uniquement avec des correspondances utiles.
===Le préchargement du mauvais chemin===
On peut améliorer la technique précédente pour s'adapter aux branchements conditionnels. En plus de charger les instructions correspondant à un branchement pris, on peut aussi charger les instructions situées juste après le branchement. Comme ça, si le branchement n'est pas pris, les instructions suivantes seront disponibles quand même. On appelle cette technique le '''préchargement du mauvais chemin''' (''wrong path prefetching'').
[[File:Préchargement du mauvais chemin.png|centre|vignette|upright=2|Préchargement du mauvais chemin.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un tampon de flux (''stream buffer''). Si jamais un défaut de cache a lieu, on regarde si la ligne de cache à lire ou écrire est dans le tampon de flux. On la rapatrie dans le cache si c'est le cas. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Dans le cas où le préchargement utilisé est un simple préchargement séquentiel, le tampon de flux est une simple mémoire FIFO.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe diverses solutions :
* le préchargement sur événement ;
* le préchargement par anticipation du ''program counter'' ;
* le préchargement par prédiction.
===Le préchargement sur événement===
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc.
De nos jours, le préchargement est initié par les accès mémoire, de diverses manières.
* Première solution : précharger le bloc suivant de manière systématique, lors de chaque accès mémoire.
* Seconde solution : ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger le bloc B dans le cache, le ''prefetcher'' chargera le bloc immédiatement suivant avec.
* Troisième solution : à chaque accès à un bloc de mémoire dans le cache, on charge le bloc de mémoire immédiatement suivant. Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Cela se fait en marquant chaque ligne de cache avec un bit spécial, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Ce dernier est automatiquement mis à zéro au bout d'un certain temps (typiquement au cycle d'horloge suivant). Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement par anticipation du ''program counter''===
Une seconde technique détermine les adresses à précharger en prenant de l'avance sur les instructions en cours d’exécution. Quand le processeur est bloqué par un accès mémoire, l'unité de préchargement anticipe les prochaines instructions chargées. Le processeur détermine les adresses lues ou écrites par ces instructions anticipées. Les processeurs qui utilisent ce genre de techniques sont redoutablement rares. On trouve quelques articles de recherche sur le sujet, et quelques universitaires travaillent dessus. Mais aucun processeur commercial ne précharge ses données ainsi. Le processeur Rock de la compagnie Sun aurait pu faire l'affaire, mais celui-ci a été annulé au dernier moment.
La première technique se base sur un '''''lookahead program counter''''' initialisé avec le ''program counter'' lors d'un défaut de cache. Il est incrémenté à chaque cycle et les branchements sont prédits. Ce ''lookahead program counter'' est mis à jour comme si l’exécution du programme se poursuivait, mais le reste du processeur est mis en attente. Les adresses fournies à chaque cycle par le ''lookahead program counter'' sont alors envoyées aux unités de préchargement pour qu'elles fassent leur travail. On peut aussi adapter cette technique pour que le ''lookahead program counter'' passe non d'une instruction à la prochaine à chaque cycle, mais d'un branchement au suivant.
On peut aussi citer le '''préchargement anticipé''' (''runahead prefetching''). Cette technique est utile si un défaut de cache a eu lieu et que le processeur n'est pas conçu pour pouvoir continuer ses calculs dans de telles conditions (pas de caches non bloquants, pas d’exécution dans le désordre, etc.). Dans un cas pareil, le processeur est censé stopper l’exécution de son programme. Mais avec le préchargement anticipé, il continue l’exécution des instructions de façon spéculative. Les lectures s’exécutent, mais pas les écritures (pour éviter de modifier des données alors qu'on n'aurait pas dû). Les lectures font des accès en avance, ce qui précharge les données accédées. On continue ainsi jusqu’à ce que l'instruction qui a stoppé tout le processeur ait enfin reçu sa donnée.
Tout doit se passer comme si ces instructions exécutées en avance n'avaient jamais eu lieu. Dans le cas contraire, on a peut-être exécuté des instructions qu'on n’aurait peut-être pas dû, et cela peut avoir modifié des registres un peu trop tôt, ou mis à jour des bits du registre d'état qui n'auraient pas dû être modifiés ainsi. Il faut donc trouver un moyen de remettre le processeur tel qu'il était quand le défaut de cache a eu lieu. Pour cela, le processeur doit sauvegarder les registres du processeur avant d’exécuter spéculativement les instructions suivantes, et les restaurer une fois le tout terminé. Qui plus est, il faut éviter que les instructions exécutées en avance puissent modifier l’état de la mémoire. Imaginez qu'une instruction modifie une ligne de cache alors qu'elle n'aurait pas dû le faire ! Pour cela, on interdit les écritures dans la mémoire.
===Le préchargement par prédiction===
Certains ''prefetchers'' avancés essaient de déduire le moment adéquat pour précharger en se basant sur l'historique des accès précédents : ils en déduisent des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
l0wsgcprogn3522x9kwt5ah1ivz43ck
Fonctionnement d'un ordinateur/La mémoire virtuelle
0
65813
682059
673582
2022-07-20T19:49:09Z
Mewtow
31375
/* Le Translation Lookaside Buffer */
wikitext
text/x-wiki
Un programme peut être exécuté sur des ordinateurs ayant des capacités mémoires diverses et variées et dans des conditions très différentes. Et il faut faire en sorte qu'un programme fonctionne sur des ordinateur ayant peu de mémoire sans poser problème. Après tout, quand on conçoit un programme, on ne sait pas toujours quelle sera la quantité mémoire que notre ordinateur contiendra, et encore moins comment celle-ci sera partagée entre nos différentes programmes en cours d’exécution : s'affranchir de limitations sur la quantité de mémoire disponible est un plus vraiment appréciable. Ce besoin est appelé l''''abstraction matérielle de la mémoire'''.
Enfin, plusieurs programmes sont présents en même temps dans notre ordinateur, et doivent se partager la mémoire physique. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur. Cela a des conséquences qui vont de comiques à catastrophiques, et cela fini très souvent par un joli plantage. Il faut donc introduire des mécanismes de '''protection mémoire'''. Pour éviter cela, chaque programme a accès à des portions de la mémoire dans laquelle lui seul peut écrire ou lire. Le reste de la mémoire est inaccessible en lecture et en écriture, à part pour la mémoire partagée entre différents programmes. Toute tentative d'accès à une partie de la mémoire non autorisée déclenchera une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui devra être traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
Ces détails concernant l'organisation et la gestion de la mémoire sont assez compliqués à gérer, et faire en sorte que les programmes applicatifs n'aient pas à s'en soucier est un plus vraiment appréciable. Ce genre de problèmes a eu des solutions purement logicielles, mais quelques techniques règlent ces problèmes directement au niveau du matériel : on parle de '''mémoire virtuelle'''. Avec la mémoire virtuelle, tout se passe comme si notre programme était seul au monde et pouvait lire et écrire à toutes les adresses disponibles à partir de l'adresse zéro. Chaque programme a accès à autant d'adresses que ce que le processeur peut adresser : on se moque du fait que des adresses soient réservées aux périphériques, de la quantité de mémoire réellement installée sur l'ordinateur, ou de la mémoire prise par d'autres programmes en cours d’exécution. Pour éviter que ce surplus de fausse mémoire pose problème, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante.
[[File:Memoire virtuelle.svg|centre|vignette|Mémoire virtuelle]]
[[File:MMU and IOMMU.svg|droite|vignette|Les périphériques peuvent eux aussi utiliser la mémoire virtuelle. Dans ce cas, ceux-ci intègrent une MMU dans leurs circuits. Ces MMU, aussi appelées IOMMU, sont séparées de la MMU du processeur.]]
Bien sûr, les adresses de la fausse mémoire vue par le programme sont des adresses fictives, qui devront être traduites en adresses mémoires réelles pour être utilisées. Les fausses adresses sont ce qu'on appelle des adresses logiques, alors que les adresses réelles sont appelées adresses physiques. Pour implémenter cette technique, il faut rajouter un circuit qui traduit les adresses logiques en adresses physiques : ce circuit est appelé la '''memory management unit'''. Il faut préciser qu'il existe différentes méthodes pour gérer ces adresses logiques et les transformer en adresses physiques : les principales sont la segmentation et la pagination. La suite du chapitre va détailler ces deux techniques.
==Le ''bank switching''==
[[File:Bankswitch memory map.svg|vignette|Exemple de Bank switching.]]
Le '''''bank switching''''', aussi appelé '''commutation de banque''', permet d'utiliser un bus d'adresse plus grand que les adresses du processeur. L'adresse réelle est plus grande que l'adresse utilisée par le processeur, les bits manquants étant fournit par un registre configurable du processeur : le '''registre de banque'''. L'espace mémoire du processeur est présent en plusieurs exemplaires, sélectionnés par la valeur du registre de banque. Chaque exemplaire de l'ensemble des adresses du processeur s'appelle une banque. On peut changer de banque en changeant le contenu de ce registre : le processeur dispose souvent d'instructions spécialisées qui en sont capables.
En répartissant les données utiles dans différentes banques, le processeur peut donc adresser beaucoup plus de mémoire. De plus, cette technique se marie assez bien avec les entrées-sorties mappées en mémoire. Par exemple, supposons que j'ai besoin d'adresser une mémoire ROM de 4 kibioctets, une RAM de 8 kibioctets, et divers périphériques. Le processeur a un bus d'adresse de 12 bits, ce qui limite à 4kibioctets. Dans ce cas, je peux réserver 4 banques : une pour la ROM, une pour les périphériques, et deux banques qui contiennent chacune la moitié de la RAM. La simplicité et l'efficacité de cette technique font qu'elle est beaucoup utilisée dans l'informatique embarquée.
{| class="wikitable flexible"
|[[File:Banque mémoire.png|Banque mémoire.]]
|[[File:Registre de banque.png|Registre de banque.]]
|}
==La segmentation==
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Segments typiques des programmes sur OS modernes.]]
Sur les vieux systèmes d'exploitation, chaque programme recevait un bloc de RAM pour lui tout seul, une '''partition mémoire'''. La segmentation est une amélioration de cette technique, qui affecte une ou plusieurs partitions par programme. Cela permet à un seul processus d'avoir plusieurs espaces d'adressages, plusieurs mémoires virtuelles à lui tout seul. L'utilité est qu'un programme est rarement un tout unique, mais peut souvent être décomposé en plusieurs ensembles de données/instructions séparés, qui peuvent grossir ou se réduire suivant les circonstances. Par exemple, la pile a une taille qui varie beaucoup, tandis que le code du programme ne change pas. La segmentation permet de découper la mémoire virtuelle d'un processus en sous-partitions de taille variable qu'on appelle improprement des '''segments'''. Ainsi, un programme peut utiliser des segments différents pour la pile, le tas, le programme lui-même, et les variables globales. Chaque segment pourra grandir ou diminuer à sa guise, suivant les besoins, ce qui serait beaucoup plus difficile avec une seule partition mémoire.
===La relocation avec la segmentation===
Chaque segment peut être placé n'importe où en mémoire physique par l'OS, ce qui fait qu'il n'a pas d'adresse fixée en mémoire physique et peut être déplacé comme bon nous semble. C'est le système d'exploitation qui gère le placement des partitions/segments dans la mémoire physique. Ainsi, les adresses de destination des branchements et les adresses des données ne sont jamais les mêmes. Il faut donc trouver un moyen pour que les programmes fonctionnent avec des adresses qui changent d'une exécution à l'autre. Ce besoin est appelé la '''relocalisation'''. Pour résoudre ce problème, le compilateur considère que le programme commence à l'adresse zéro et laisse l'OS corriger les adresses du programme. Cette correction d'adresse demande simplement d'ajouter un décalage, égal à la première adresse de la partition mémoire. Cette correction peut se faire de deux manières : soit l'OS corrige chaque adresse lors du lancement du programme, soit le processeur s'en charge à chaque accès mémoire. Dans le second cas, cette première adresse est mémorisée dans un '''registre de base''', mis à jour automatiquement lors de chaque changement de programme.
[[File:Segmentation et relocation.png|centre|vignette|upright=2|Segmentation et relocation.]]
Sur les processeurs x86, la mémoire virtuelle est découpée en 2^16 segments de 2^32 bits, ce qui donne des adresses de 48 bits. Pour identifier un segment, seuls les 16 bits de poids fort de l'adresse 48 bits sont utiles : ils forment ce qu'on appelle un sélecteur de segment. Les 32 bits servent à identifier la position de la donnée dans un segment : ils sont appelés le décalage (offset). La relocalisation se fait de la même manière pour un segment que pour une partition mémoire. Pour cela, l'OS associe chaque segment à son adresse de base dans une table de correspondance, appelée la '''table des segments'''. Elle est unique pour chaque programme : la même adresse logique ne donnera pas la même adresse physique selon le programme.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse.]]
Dans le cas le plus simple, la table des segments est stockée en mémoire RAM, le processeur ne contenant qu'un registre de base mis à jour à chaque changement de segment.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Une autre solution consiste à charger la table de correspondance complète dans un banc de registre dédié.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
===La protection mémoire avec la segmentation===
Un programme ne doit avoir accès qu'à la partition qui lui est dédiée et pas aux autres, sauf dans quelques rares exceptions. Toute tentative d'accès à une autre partition doit déclencher une exception matérielle, qui entraîne souvent l'apparition d'un message d'erreur. Tout cela est pris en charge par le système d'exploitation, par l'intermédiaire de mécanismes de '''protection mémoire''', que nous allons maintenant aborder.
Pour commencer, le processeur (ou l'OS) doivent détecter les accès hors-partition, à savoir un programme qui lit/écrit de la mémoire au-delà de la partition qui lui réservée. La solution est de mémoriser les limites du segment dans la page des segments et de vérifier que les accès mémoire ne se font pas au-delà de cette limite. L'adresse de fin de segment est mémorisée dans la table des segments et est récupérée lors de chaque accès. Le processeur vérifie si l'accès se fait dans les clou, en comparant l'adresse accédée avec l'adresse limite. Une autre solution consiste à mémoriser non pas l'adresse, mais l'offset maximal possible dans le segment en cours. Cela économise quelques bits par entrée dans la table des tables. Voici comment se passe la traduction d'une adresse avec la segmentation, en tenant compte de la vérification des accès hors-segment.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Vient ensuite la '''gestion des droits d'accès''' : chaque partition/segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Par exemple, il est possible d'interdire d'exécuter quoique ce soit de localisé dans certains segments, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation. L'OS ou la MMU mémorisent les autorisations pour chaque segment, qui sont rassemblées avec d'autres informations (registre de base et limite) dans un '''descripteur de segment'''. Pour se simplifier la tache, les concepteurs de processeurs et de systèmes d'exploitation ont décidé de regrouper ces descripteurs dans une portion de la mémoire, spécialement réservée pour l'occasion : la '''table des descripteurs de segment'''. Pour des raisons de performance, le processeur utilise un registre pour mémoriser le descripteur de segment du segment en cours d'utilisation. Quand on accède à un segment, son descripteur est chargé dans des registres du processeur, l'ancien est effacé.
[[File:SegmentDescriptor.svg|centre|vignette|upright=2|Schéma d'un descripteur de segment sur une architecture x86.]]
===Le partage de segments===
Il faut préciser qu'il est possible de partager des segments entre applications. Il suffit de configurer les tables de segment convenablement. Cela peut servir quand plusieurs instances d'une même applications sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instance.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Il est aussi possible de faire en sorte que deux segments se recouvrent l'un autre, à savoir que les deux segments partagent la même mémoire physique. Cela peut servir à partager de la mémoire entre plusieurs applications qui doivent communiquer entre elles.
[[File:Overlapping realmode segments.svg|centre|vignette|upright=1.5|Recouvrement de segments.]]
==La pagination==
De nos jours, la segmentation est obsolète et n'est plus utilisée : à la place, les OS et processeurs utilisent la pagination. Avec la pagination, la mémoire virtuelle et la mémoire physique sont découpées en blocs de taille fixe, appelés des '''pages mémoires'''. La différence avec les segments est que les segments sont de taille variable, alors que les pages sont de taille fixe. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique. Cependant, le nombre total de pages en mémoire virtuelle dépasse celui de la mémoire physique.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Le surnombre est simplement placé sur le disque dur, dans un fichier appelé le '''fichier d'échange'''. Les pages virtuelles vont ainsi faire référence soit à une page en mémoire physique, soit à une page sur le disque dur. Tout accès à une page sur le disque dur va charger celle-ci dans la mémoire RAM, dans une page vide. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins.
[[File:Lazy Swapper.jpg|centre|vignette|upright=2|Mémoire virtuelle paginée et fichier d'échange.]]
===La protection mémoire avec la pagination===
La protection mémoire est garantie avec des '''clés de protection''', un nombre unique à chaque programme. Le processeur mémorise, pour chaque page, la clé de protection du programme qui a réservé la page. A chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution, et celle de la page adressée. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
Comme avec la segmentation, chaque page a des droits d'accès précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page. Ces autorisations/interdictions sont mémorisés sous la forme d'une suite de bits, chaque bit autorisant/interdisant une opération bien précise. Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, c'est lors du passage au 64 bits que l'interdiction d’exécution a été ajouté au jeu d’instruction. Avant, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'à été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé bit NX, est à 0 si la page n'est pas exécutable et à 1 sinon. De plus, diverses techniques intégrées aux processeur permettent de gérer celui-ci facilement. Notamment, le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
===La traduction d'adresse avec la pagination===
Une adresse (logique ou physique) se décompose donc en deux parties : un numéro de page qui identifie la page et un numéro permettant de localiser la donnée dans la page. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique. Pour faire cette traduction, il faut se souvenir des correspondances entre numéro de page et adresse de la page en mémoire fictive. Elles sont stockées dans une sorte de table, nommée la '''table des pages'''. Celle-ci contient aussi des bits pour savoir si une page est swappée sur le disque dur ou si elle est présente en RAM.
[[File:Paging.svg|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. Ainsi, pour chaque numéro (ou chaque adresse) de page logique, on stocke l'adresse de base de la page correspondante en mémoire physique. La table des pages est unique pour chaque programme, vu que les correspondances entre adresses physiques et logiques ne sont pas les mêmes. Cette table des pages est souvent stockée dans la mémoire RAM, à un endroit bien précis, connu du processeur. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, puis de calculer l'adresse de notre donnée, et enfin d’accéder à la donnée voulue. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages inversées====
Sur certains systèmes, la taille d'une table des pages serait beaucoup trop grande en utilisant les techniques vues au-dessus. Pour éviter cela, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur voudra convertir une adresse virtuelle en adresse physique, la MMU prendra le numéro de page de l'adresse virtuelle, et le recherchera dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, les concepteurs de systèmes d'exploitation peuvent stocker celle-ci avec ce que l'on appelle une table de hachage.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des page par processus/programme lancé sur l'ordinateur.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
Mais cela a un léger défaut : la table des pages de chaque processus bouffe beaucoup de mémoire. Aussi, les concepteurs de processeurs et de systèmes d'exploitation ont adapté les tables des pages précédentes pour limiter la casse. Ils ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Ainsi, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Chaque sous-espace d'adressage a sa propre page des tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage qui contiennent au moins une donnée.
Les premiers bits de l'adresse vont servir à sélectionner quelle table des pages utiliser. Pour cela, il faut connaître l'adresse à laquelle est stockée chaque table des pages. Pour cela, on utilise une super-table des pages, qui stocke les adresses de début des tables des pages de chaque sous-espace. On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
===Le ''Translation Lookaside Buffer''===
Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''translation lookaside buffer''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la mémoire RAM et va lire directement la donnée depuis ce TLB. L'accès à la RAM est inévitable dans le cas contraire.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
L'accès est géré de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Dans le second cas, si l'adresse cherchée n'est pas dans le TLB, le processeur va lever une exception matérielle qui exécutera une routine d'interruption chargée de gérer la situation.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
Pour des raisons de performances, ce cache est parfois découpé en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données.
===Le remplacement des pages mémoires===
La mémoire physique contient moins de pages que la mémoire virtuelle et il faut trouver un moyen pour que cela ne pose pas de problème. La solution consiste à utiliser des mémoires de stockage comme mémoire d'appoint : si on a besoin de plus de pages mémoires que la mémoire physique n'en contient, certaines pages mémoires vont être déplacées sur le disque dur pour faire de la place. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Lorsque l'on veut traduire l'adresse logique d'une page mémoire déplacée sur le disque dur, la MMU ne va pas pouvoir associer l'adresse logique à une adresse en mémoire RAM. Elle va alors lever une exception matérielle dont la routine rapatriera la page en mémoire RAM.
Charger une page en RAM ne pose aucun problème tant qu'il existe des pages inoccupée en RAM. Mais si toute la RAM est pleine, il faut déplacer une page mémoire en RAM sur le disque dur pour faire de la place. Tout cela est effectué par le système d'exploitation dont j'ai parlé plus haut. Notons que si on supprime une donnée dont on aura besoin dans le futur, il faudra recharger celle-ci, ce qui prend du temps. Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Les plus simples sont les suivants.
* Aléatoire : on choisit la page au hasard.
* FIFO : on supprime la donnée qui a été chargée dans la mémoire avant toutes les autres.
* LRU : on supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres.
* LFU : on vire la page qui est lue ou écrite le moins souvent comparée aux autres.
* etc.
Ces algorithmes ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter une interruption de gestion de page miss alors que la page contenant le code de l'interruption est placée sur le disque dur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le matériel réseau
| prevText=Le matériel réseau
| next=Les mémoires cache
| nextText=Les mémoires cache
}}
</noinclude>
o9pbl9n28e6sj2jgq0368pxp80zasgm
682060
682059
2022-07-20T19:59:28Z
Mewtow
31375
/* Le Translation Lookaside Buffer */
wikitext
text/x-wiki
Un programme peut être exécuté sur des ordinateurs ayant des capacités mémoires diverses et variées et dans des conditions très différentes. Et il faut faire en sorte qu'un programme fonctionne sur des ordinateur ayant peu de mémoire sans poser problème. Après tout, quand on conçoit un programme, on ne sait pas toujours quelle sera la quantité mémoire que notre ordinateur contiendra, et encore moins comment celle-ci sera partagée entre nos différentes programmes en cours d’exécution : s'affranchir de limitations sur la quantité de mémoire disponible est un plus vraiment appréciable. Ce besoin est appelé l''''abstraction matérielle de la mémoire'''.
Enfin, plusieurs programmes sont présents en même temps dans notre ordinateur, et doivent se partager la mémoire physique. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur. Cela a des conséquences qui vont de comiques à catastrophiques, et cela fini très souvent par un joli plantage. Il faut donc introduire des mécanismes de '''protection mémoire'''. Pour éviter cela, chaque programme a accès à des portions de la mémoire dans laquelle lui seul peut écrire ou lire. Le reste de la mémoire est inaccessible en lecture et en écriture, à part pour la mémoire partagée entre différents programmes. Toute tentative d'accès à une partie de la mémoire non autorisée déclenchera une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui devra être traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
Ces détails concernant l'organisation et la gestion de la mémoire sont assez compliqués à gérer, et faire en sorte que les programmes applicatifs n'aient pas à s'en soucier est un plus vraiment appréciable. Ce genre de problèmes a eu des solutions purement logicielles, mais quelques techniques règlent ces problèmes directement au niveau du matériel : on parle de '''mémoire virtuelle'''. Avec la mémoire virtuelle, tout se passe comme si notre programme était seul au monde et pouvait lire et écrire à toutes les adresses disponibles à partir de l'adresse zéro. Chaque programme a accès à autant d'adresses que ce que le processeur peut adresser : on se moque du fait que des adresses soient réservées aux périphériques, de la quantité de mémoire réellement installée sur l'ordinateur, ou de la mémoire prise par d'autres programmes en cours d’exécution. Pour éviter que ce surplus de fausse mémoire pose problème, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante.
[[File:Memoire virtuelle.svg|centre|vignette|Mémoire virtuelle]]
[[File:MMU and IOMMU.svg|droite|vignette|Les périphériques peuvent eux aussi utiliser la mémoire virtuelle. Dans ce cas, ceux-ci intègrent une MMU dans leurs circuits. Ces MMU, aussi appelées IOMMU, sont séparées de la MMU du processeur.]]
Bien sûr, les adresses de la fausse mémoire vue par le programme sont des adresses fictives, qui devront être traduites en adresses mémoires réelles pour être utilisées. Les fausses adresses sont ce qu'on appelle des adresses logiques, alors que les adresses réelles sont appelées adresses physiques. Pour implémenter cette technique, il faut rajouter un circuit qui traduit les adresses logiques en adresses physiques : ce circuit est appelé la '''memory management unit'''. Il faut préciser qu'il existe différentes méthodes pour gérer ces adresses logiques et les transformer en adresses physiques : les principales sont la segmentation et la pagination. La suite du chapitre va détailler ces deux techniques.
==Le ''bank switching''==
[[File:Bankswitch memory map.svg|vignette|Exemple de Bank switching.]]
Le '''''bank switching''''', aussi appelé '''commutation de banque''', permet d'utiliser un bus d'adresse plus grand que les adresses du processeur. L'adresse réelle est plus grande que l'adresse utilisée par le processeur, les bits manquants étant fournit par un registre configurable du processeur : le '''registre de banque'''. L'espace mémoire du processeur est présent en plusieurs exemplaires, sélectionnés par la valeur du registre de banque. Chaque exemplaire de l'ensemble des adresses du processeur s'appelle une banque. On peut changer de banque en changeant le contenu de ce registre : le processeur dispose souvent d'instructions spécialisées qui en sont capables.
En répartissant les données utiles dans différentes banques, le processeur peut donc adresser beaucoup plus de mémoire. De plus, cette technique se marie assez bien avec les entrées-sorties mappées en mémoire. Par exemple, supposons que j'ai besoin d'adresser une mémoire ROM de 4 kibioctets, une RAM de 8 kibioctets, et divers périphériques. Le processeur a un bus d'adresse de 12 bits, ce qui limite à 4kibioctets. Dans ce cas, je peux réserver 4 banques : une pour la ROM, une pour les périphériques, et deux banques qui contiennent chacune la moitié de la RAM. La simplicité et l'efficacité de cette technique font qu'elle est beaucoup utilisée dans l'informatique embarquée.
{| class="wikitable flexible"
|[[File:Banque mémoire.png|Banque mémoire.]]
|[[File:Registre de banque.png|Registre de banque.]]
|}
==La segmentation==
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Segments typiques des programmes sur OS modernes.]]
Sur les vieux systèmes d'exploitation, chaque programme recevait un bloc de RAM pour lui tout seul, une '''partition mémoire'''. La segmentation est une amélioration de cette technique, qui affecte une ou plusieurs partitions par programme. Cela permet à un seul processus d'avoir plusieurs espaces d'adressages, plusieurs mémoires virtuelles à lui tout seul. L'utilité est qu'un programme est rarement un tout unique, mais peut souvent être décomposé en plusieurs ensembles de données/instructions séparés, qui peuvent grossir ou se réduire suivant les circonstances. Par exemple, la pile a une taille qui varie beaucoup, tandis que le code du programme ne change pas. La segmentation permet de découper la mémoire virtuelle d'un processus en sous-partitions de taille variable qu'on appelle improprement des '''segments'''. Ainsi, un programme peut utiliser des segments différents pour la pile, le tas, le programme lui-même, et les variables globales. Chaque segment pourra grandir ou diminuer à sa guise, suivant les besoins, ce qui serait beaucoup plus difficile avec une seule partition mémoire.
===La relocation avec la segmentation===
Chaque segment peut être placé n'importe où en mémoire physique par l'OS, ce qui fait qu'il n'a pas d'adresse fixée en mémoire physique et peut être déplacé comme bon nous semble. C'est le système d'exploitation qui gère le placement des partitions/segments dans la mémoire physique. Ainsi, les adresses de destination des branchements et les adresses des données ne sont jamais les mêmes. Il faut donc trouver un moyen pour que les programmes fonctionnent avec des adresses qui changent d'une exécution à l'autre. Ce besoin est appelé la '''relocalisation'''. Pour résoudre ce problème, le compilateur considère que le programme commence à l'adresse zéro et laisse l'OS corriger les adresses du programme. Cette correction d'adresse demande simplement d'ajouter un décalage, égal à la première adresse de la partition mémoire. Cette correction peut se faire de deux manières : soit l'OS corrige chaque adresse lors du lancement du programme, soit le processeur s'en charge à chaque accès mémoire. Dans le second cas, cette première adresse est mémorisée dans un '''registre de base''', mis à jour automatiquement lors de chaque changement de programme.
[[File:Segmentation et relocation.png|centre|vignette|upright=2|Segmentation et relocation.]]
Sur les processeurs x86, la mémoire virtuelle est découpée en 2^16 segments de 2^32 bits, ce qui donne des adresses de 48 bits. Pour identifier un segment, seuls les 16 bits de poids fort de l'adresse 48 bits sont utiles : ils forment ce qu'on appelle un sélecteur de segment. Les 32 bits servent à identifier la position de la donnée dans un segment : ils sont appelés le décalage (offset). La relocalisation se fait de la même manière pour un segment que pour une partition mémoire. Pour cela, l'OS associe chaque segment à son adresse de base dans une table de correspondance, appelée la '''table des segments'''. Elle est unique pour chaque programme : la même adresse logique ne donnera pas la même adresse physique selon le programme.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse.]]
Dans le cas le plus simple, la table des segments est stockée en mémoire RAM, le processeur ne contenant qu'un registre de base mis à jour à chaque changement de segment.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Une autre solution consiste à charger la table de correspondance complète dans un banc de registre dédié.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
===La protection mémoire avec la segmentation===
Un programme ne doit avoir accès qu'à la partition qui lui est dédiée et pas aux autres, sauf dans quelques rares exceptions. Toute tentative d'accès à une autre partition doit déclencher une exception matérielle, qui entraîne souvent l'apparition d'un message d'erreur. Tout cela est pris en charge par le système d'exploitation, par l'intermédiaire de mécanismes de '''protection mémoire''', que nous allons maintenant aborder.
Pour commencer, le processeur (ou l'OS) doivent détecter les accès hors-partition, à savoir un programme qui lit/écrit de la mémoire au-delà de la partition qui lui réservée. La solution est de mémoriser les limites du segment dans la page des segments et de vérifier que les accès mémoire ne se font pas au-delà de cette limite. L'adresse de fin de segment est mémorisée dans la table des segments et est récupérée lors de chaque accès. Le processeur vérifie si l'accès se fait dans les clou, en comparant l'adresse accédée avec l'adresse limite. Une autre solution consiste à mémoriser non pas l'adresse, mais l'offset maximal possible dans le segment en cours. Cela économise quelques bits par entrée dans la table des tables. Voici comment se passe la traduction d'une adresse avec la segmentation, en tenant compte de la vérification des accès hors-segment.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Vient ensuite la '''gestion des droits d'accès''' : chaque partition/segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Par exemple, il est possible d'interdire d'exécuter quoique ce soit de localisé dans certains segments, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation. L'OS ou la MMU mémorisent les autorisations pour chaque segment, qui sont rassemblées avec d'autres informations (registre de base et limite) dans un '''descripteur de segment'''. Pour se simplifier la tache, les concepteurs de processeurs et de systèmes d'exploitation ont décidé de regrouper ces descripteurs dans une portion de la mémoire, spécialement réservée pour l'occasion : la '''table des descripteurs de segment'''. Pour des raisons de performance, le processeur utilise un registre pour mémoriser le descripteur de segment du segment en cours d'utilisation. Quand on accède à un segment, son descripteur est chargé dans des registres du processeur, l'ancien est effacé.
[[File:SegmentDescriptor.svg|centre|vignette|upright=2|Schéma d'un descripteur de segment sur une architecture x86.]]
===Le partage de segments===
Il faut préciser qu'il est possible de partager des segments entre applications. Il suffit de configurer les tables de segment convenablement. Cela peut servir quand plusieurs instances d'une même applications sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instance.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Il est aussi possible de faire en sorte que deux segments se recouvrent l'un autre, à savoir que les deux segments partagent la même mémoire physique. Cela peut servir à partager de la mémoire entre plusieurs applications qui doivent communiquer entre elles.
[[File:Overlapping realmode segments.svg|centre|vignette|upright=1.5|Recouvrement de segments.]]
==La pagination==
De nos jours, la segmentation est obsolète et n'est plus utilisée : à la place, les OS et processeurs utilisent la pagination. Avec la pagination, la mémoire virtuelle et la mémoire physique sont découpées en blocs de taille fixe, appelés des '''pages mémoires'''. La différence avec les segments est que les segments sont de taille variable, alors que les pages sont de taille fixe. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique. Cependant, le nombre total de pages en mémoire virtuelle dépasse celui de la mémoire physique.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Le surnombre est simplement placé sur le disque dur, dans un fichier appelé le '''fichier d'échange'''. Les pages virtuelles vont ainsi faire référence soit à une page en mémoire physique, soit à une page sur le disque dur. Tout accès à une page sur le disque dur va charger celle-ci dans la mémoire RAM, dans une page vide. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins.
[[File:Lazy Swapper.jpg|centre|vignette|upright=2|Mémoire virtuelle paginée et fichier d'échange.]]
===La protection mémoire avec la pagination===
La protection mémoire est garantie avec des '''clés de protection''', un nombre unique à chaque programme. Le processeur mémorise, pour chaque page, la clé de protection du programme qui a réservé la page. A chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution, et celle de la page adressée. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
Comme avec la segmentation, chaque page a des droits d'accès précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page. Ces autorisations/interdictions sont mémorisés sous la forme d'une suite de bits, chaque bit autorisant/interdisant une opération bien précise. Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, c'est lors du passage au 64 bits que l'interdiction d’exécution a été ajouté au jeu d’instruction. Avant, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'à été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé bit NX, est à 0 si la page n'est pas exécutable et à 1 sinon. De plus, diverses techniques intégrées aux processeur permettent de gérer celui-ci facilement. Notamment, le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
===La traduction d'adresse avec la pagination===
Une adresse (logique ou physique) se décompose donc en deux parties : un numéro de page qui identifie la page et un numéro permettant de localiser la donnée dans la page. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique. Pour faire cette traduction, il faut se souvenir des correspondances entre numéro de page et adresse de la page en mémoire fictive. Elles sont stockées dans une sorte de table, nommée la '''table des pages'''. Celle-ci contient aussi des bits pour savoir si une page est swappée sur le disque dur ou si elle est présente en RAM.
[[File:Paging.svg|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. Ainsi, pour chaque numéro (ou chaque adresse) de page logique, on stocke l'adresse de base de la page correspondante en mémoire physique. La table des pages est unique pour chaque programme, vu que les correspondances entre adresses physiques et logiques ne sont pas les mêmes. Cette table des pages est souvent stockée dans la mémoire RAM, à un endroit bien précis, connu du processeur. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, puis de calculer l'adresse de notre donnée, et enfin d’accéder à la donnée voulue. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages inversées====
Sur certains systèmes, la taille d'une table des pages serait beaucoup trop grande en utilisant les techniques vues au-dessus. Pour éviter cela, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur voudra convertir une adresse virtuelle en adresse physique, la MMU prendra le numéro de page de l'adresse virtuelle, et le recherchera dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, les concepteurs de systèmes d'exploitation peuvent stocker celle-ci avec ce que l'on appelle une table de hachage.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des page par processus/programme lancé sur l'ordinateur.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
Mais cela a un léger défaut : la table des pages de chaque processus bouffe beaucoup de mémoire. Aussi, les concepteurs de processeurs et de systèmes d'exploitation ont adapté les tables des pages précédentes pour limiter la casse. Ils ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Ainsi, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Chaque sous-espace d'adressage a sa propre page des tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage qui contiennent au moins une donnée.
Les premiers bits de l'adresse vont servir à sélectionner quelle table des pages utiliser. Pour cela, il faut connaître l'adresse à laquelle est stockée chaque table des pages. Pour cela, on utilise une super-table des pages, qui stocke les adresses de début des tables des pages de chaque sous-espace. On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
===Le ''Translation Lookaside Buffer''===
Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''translation lookaside buffer''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances. De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeur superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
Pour des raisons de performances, ce cache est parfois découpé en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
===Le remplacement des pages mémoires===
La mémoire physique contient moins de pages que la mémoire virtuelle et il faut trouver un moyen pour que cela ne pose pas de problème. La solution consiste à utiliser des mémoires de stockage comme mémoire d'appoint : si on a besoin de plus de pages mémoires que la mémoire physique n'en contient, certaines pages mémoires vont être déplacées sur le disque dur pour faire de la place. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Lorsque l'on veut traduire l'adresse logique d'une page mémoire déplacée sur le disque dur, la MMU ne va pas pouvoir associer l'adresse logique à une adresse en mémoire RAM. Elle va alors lever une exception matérielle dont la routine rapatriera la page en mémoire RAM.
Charger une page en RAM ne pose aucun problème tant qu'il existe des pages inoccupée en RAM. Mais si toute la RAM est pleine, il faut déplacer une page mémoire en RAM sur le disque dur pour faire de la place. Tout cela est effectué par le système d'exploitation dont j'ai parlé plus haut. Notons que si on supprime une donnée dont on aura besoin dans le futur, il faudra recharger celle-ci, ce qui prend du temps. Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Les plus simples sont les suivants.
* Aléatoire : on choisit la page au hasard.
* FIFO : on supprime la donnée qui a été chargée dans la mémoire avant toutes les autres.
* LRU : on supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres.
* LFU : on vire la page qui est lue ou écrite le moins souvent comparée aux autres.
* etc.
Ces algorithmes ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter une interruption de gestion de page miss alors que la page contenant le code de l'interruption est placée sur le disque dur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le matériel réseau
| prevText=Le matériel réseau
| next=Les mémoires cache
| nextText=Les mémoires cache
}}
</noinclude>
mhbebpx3wtdeybyfzhyytu8l1esoi16
682061
682060
2022-07-20T20:05:58Z
Mewtow
31375
/* Le Translation Lookaside Buffer */
wikitext
text/x-wiki
Un programme peut être exécuté sur des ordinateurs ayant des capacités mémoires diverses et variées et dans des conditions très différentes. Et il faut faire en sorte qu'un programme fonctionne sur des ordinateur ayant peu de mémoire sans poser problème. Après tout, quand on conçoit un programme, on ne sait pas toujours quelle sera la quantité mémoire que notre ordinateur contiendra, et encore moins comment celle-ci sera partagée entre nos différentes programmes en cours d’exécution : s'affranchir de limitations sur la quantité de mémoire disponible est un plus vraiment appréciable. Ce besoin est appelé l''''abstraction matérielle de la mémoire'''.
Enfin, plusieurs programmes sont présents en même temps dans notre ordinateur, et doivent se partager la mémoire physique. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur. Cela a des conséquences qui vont de comiques à catastrophiques, et cela fini très souvent par un joli plantage. Il faut donc introduire des mécanismes de '''protection mémoire'''. Pour éviter cela, chaque programme a accès à des portions de la mémoire dans laquelle lui seul peut écrire ou lire. Le reste de la mémoire est inaccessible en lecture et en écriture, à part pour la mémoire partagée entre différents programmes. Toute tentative d'accès à une partie de la mémoire non autorisée déclenchera une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui devra être traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
Ces détails concernant l'organisation et la gestion de la mémoire sont assez compliqués à gérer, et faire en sorte que les programmes applicatifs n'aient pas à s'en soucier est un plus vraiment appréciable. Ce genre de problèmes a eu des solutions purement logicielles, mais quelques techniques règlent ces problèmes directement au niveau du matériel : on parle de '''mémoire virtuelle'''. Avec la mémoire virtuelle, tout se passe comme si notre programme était seul au monde et pouvait lire et écrire à toutes les adresses disponibles à partir de l'adresse zéro. Chaque programme a accès à autant d'adresses que ce que le processeur peut adresser : on se moque du fait que des adresses soient réservées aux périphériques, de la quantité de mémoire réellement installée sur l'ordinateur, ou de la mémoire prise par d'autres programmes en cours d’exécution. Pour éviter que ce surplus de fausse mémoire pose problème, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante.
[[File:Memoire virtuelle.svg|centre|vignette|Mémoire virtuelle]]
[[File:MMU and IOMMU.svg|droite|vignette|Les périphériques peuvent eux aussi utiliser la mémoire virtuelle. Dans ce cas, ceux-ci intègrent une MMU dans leurs circuits. Ces MMU, aussi appelées IOMMU, sont séparées de la MMU du processeur.]]
Bien sûr, les adresses de la fausse mémoire vue par le programme sont des adresses fictives, qui devront être traduites en adresses mémoires réelles pour être utilisées. Les fausses adresses sont ce qu'on appelle des adresses logiques, alors que les adresses réelles sont appelées adresses physiques. Pour implémenter cette technique, il faut rajouter un circuit qui traduit les adresses logiques en adresses physiques : ce circuit est appelé la '''memory management unit'''. Il faut préciser qu'il existe différentes méthodes pour gérer ces adresses logiques et les transformer en adresses physiques : les principales sont la segmentation et la pagination. La suite du chapitre va détailler ces deux techniques.
==Le ''bank switching''==
[[File:Bankswitch memory map.svg|vignette|Exemple de Bank switching.]]
Le '''''bank switching''''', aussi appelé '''commutation de banque''', permet d'utiliser un bus d'adresse plus grand que les adresses du processeur. L'adresse réelle est plus grande que l'adresse utilisée par le processeur, les bits manquants étant fournit par un registre configurable du processeur : le '''registre de banque'''. L'espace mémoire du processeur est présent en plusieurs exemplaires, sélectionnés par la valeur du registre de banque. Chaque exemplaire de l'ensemble des adresses du processeur s'appelle une banque. On peut changer de banque en changeant le contenu de ce registre : le processeur dispose souvent d'instructions spécialisées qui en sont capables.
En répartissant les données utiles dans différentes banques, le processeur peut donc adresser beaucoup plus de mémoire. De plus, cette technique se marie assez bien avec les entrées-sorties mappées en mémoire. Par exemple, supposons que j'ai besoin d'adresser une mémoire ROM de 4 kibioctets, une RAM de 8 kibioctets, et divers périphériques. Le processeur a un bus d'adresse de 12 bits, ce qui limite à 4kibioctets. Dans ce cas, je peux réserver 4 banques : une pour la ROM, une pour les périphériques, et deux banques qui contiennent chacune la moitié de la RAM. La simplicité et l'efficacité de cette technique font qu'elle est beaucoup utilisée dans l'informatique embarquée.
{| class="wikitable flexible"
|[[File:Banque mémoire.png|Banque mémoire.]]
|[[File:Registre de banque.png|Registre de banque.]]
|}
==La segmentation==
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Segments typiques des programmes sur OS modernes.]]
Sur les vieux systèmes d'exploitation, chaque programme recevait un bloc de RAM pour lui tout seul, une '''partition mémoire'''. La segmentation est une amélioration de cette technique, qui affecte une ou plusieurs partitions par programme. Cela permet à un seul processus d'avoir plusieurs espaces d'adressages, plusieurs mémoires virtuelles à lui tout seul. L'utilité est qu'un programme est rarement un tout unique, mais peut souvent être décomposé en plusieurs ensembles de données/instructions séparés, qui peuvent grossir ou se réduire suivant les circonstances. Par exemple, la pile a une taille qui varie beaucoup, tandis que le code du programme ne change pas. La segmentation permet de découper la mémoire virtuelle d'un processus en sous-partitions de taille variable qu'on appelle improprement des '''segments'''. Ainsi, un programme peut utiliser des segments différents pour la pile, le tas, le programme lui-même, et les variables globales. Chaque segment pourra grandir ou diminuer à sa guise, suivant les besoins, ce qui serait beaucoup plus difficile avec une seule partition mémoire.
===La relocation avec la segmentation===
Chaque segment peut être placé n'importe où en mémoire physique par l'OS, ce qui fait qu'il n'a pas d'adresse fixée en mémoire physique et peut être déplacé comme bon nous semble. C'est le système d'exploitation qui gère le placement des partitions/segments dans la mémoire physique. Ainsi, les adresses de destination des branchements et les adresses des données ne sont jamais les mêmes. Il faut donc trouver un moyen pour que les programmes fonctionnent avec des adresses qui changent d'une exécution à l'autre. Ce besoin est appelé la '''relocalisation'''. Pour résoudre ce problème, le compilateur considère que le programme commence à l'adresse zéro et laisse l'OS corriger les adresses du programme. Cette correction d'adresse demande simplement d'ajouter un décalage, égal à la première adresse de la partition mémoire. Cette correction peut se faire de deux manières : soit l'OS corrige chaque adresse lors du lancement du programme, soit le processeur s'en charge à chaque accès mémoire. Dans le second cas, cette première adresse est mémorisée dans un '''registre de base''', mis à jour automatiquement lors de chaque changement de programme.
[[File:Segmentation et relocation.png|centre|vignette|upright=2|Segmentation et relocation.]]
Sur les processeurs x86, la mémoire virtuelle est découpée en 2^16 segments de 2^32 bits, ce qui donne des adresses de 48 bits. Pour identifier un segment, seuls les 16 bits de poids fort de l'adresse 48 bits sont utiles : ils forment ce qu'on appelle un sélecteur de segment. Les 32 bits servent à identifier la position de la donnée dans un segment : ils sont appelés le décalage (offset). La relocalisation se fait de la même manière pour un segment que pour une partition mémoire. Pour cela, l'OS associe chaque segment à son adresse de base dans une table de correspondance, appelée la '''table des segments'''. Elle est unique pour chaque programme : la même adresse logique ne donnera pas la même adresse physique selon le programme.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse.]]
Dans le cas le plus simple, la table des segments est stockée en mémoire RAM, le processeur ne contenant qu'un registre de base mis à jour à chaque changement de segment.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Une autre solution consiste à charger la table de correspondance complète dans un banc de registre dédié.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
===La protection mémoire avec la segmentation===
Un programme ne doit avoir accès qu'à la partition qui lui est dédiée et pas aux autres, sauf dans quelques rares exceptions. Toute tentative d'accès à une autre partition doit déclencher une exception matérielle, qui entraîne souvent l'apparition d'un message d'erreur. Tout cela est pris en charge par le système d'exploitation, par l'intermédiaire de mécanismes de '''protection mémoire''', que nous allons maintenant aborder.
Pour commencer, le processeur (ou l'OS) doivent détecter les accès hors-partition, à savoir un programme qui lit/écrit de la mémoire au-delà de la partition qui lui réservée. La solution est de mémoriser les limites du segment dans la page des segments et de vérifier que les accès mémoire ne se font pas au-delà de cette limite. L'adresse de fin de segment est mémorisée dans la table des segments et est récupérée lors de chaque accès. Le processeur vérifie si l'accès se fait dans les clou, en comparant l'adresse accédée avec l'adresse limite. Une autre solution consiste à mémoriser non pas l'adresse, mais l'offset maximal possible dans le segment en cours. Cela économise quelques bits par entrée dans la table des tables. Voici comment se passe la traduction d'une adresse avec la segmentation, en tenant compte de la vérification des accès hors-segment.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Vient ensuite la '''gestion des droits d'accès''' : chaque partition/segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Par exemple, il est possible d'interdire d'exécuter quoique ce soit de localisé dans certains segments, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation. L'OS ou la MMU mémorisent les autorisations pour chaque segment, qui sont rassemblées avec d'autres informations (registre de base et limite) dans un '''descripteur de segment'''. Pour se simplifier la tache, les concepteurs de processeurs et de systèmes d'exploitation ont décidé de regrouper ces descripteurs dans une portion de la mémoire, spécialement réservée pour l'occasion : la '''table des descripteurs de segment'''. Pour des raisons de performance, le processeur utilise un registre pour mémoriser le descripteur de segment du segment en cours d'utilisation. Quand on accède à un segment, son descripteur est chargé dans des registres du processeur, l'ancien est effacé.
[[File:SegmentDescriptor.svg|centre|vignette|upright=2|Schéma d'un descripteur de segment sur une architecture x86.]]
===Le partage de segments===
Il faut préciser qu'il est possible de partager des segments entre applications. Il suffit de configurer les tables de segment convenablement. Cela peut servir quand plusieurs instances d'une même applications sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instance.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Il est aussi possible de faire en sorte que deux segments se recouvrent l'un autre, à savoir que les deux segments partagent la même mémoire physique. Cela peut servir à partager de la mémoire entre plusieurs applications qui doivent communiquer entre elles.
[[File:Overlapping realmode segments.svg|centre|vignette|upright=1.5|Recouvrement de segments.]]
==La pagination==
De nos jours, la segmentation est obsolète et n'est plus utilisée : à la place, les OS et processeurs utilisent la pagination. Avec la pagination, la mémoire virtuelle et la mémoire physique sont découpées en blocs de taille fixe, appelés des '''pages mémoires'''. La différence avec les segments est que les segments sont de taille variable, alors que les pages sont de taille fixe. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique. Cependant, le nombre total de pages en mémoire virtuelle dépasse celui de la mémoire physique.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Le surnombre est simplement placé sur le disque dur, dans un fichier appelé le '''fichier d'échange'''. Les pages virtuelles vont ainsi faire référence soit à une page en mémoire physique, soit à une page sur le disque dur. Tout accès à une page sur le disque dur va charger celle-ci dans la mémoire RAM, dans une page vide. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins.
[[File:Lazy Swapper.jpg|centre|vignette|upright=2|Mémoire virtuelle paginée et fichier d'échange.]]
===La protection mémoire avec la pagination===
La protection mémoire est garantie avec des '''clés de protection''', un nombre unique à chaque programme. Le processeur mémorise, pour chaque page, la clé de protection du programme qui a réservé la page. A chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution, et celle de la page adressée. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
Comme avec la segmentation, chaque page a des droits d'accès précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page. Ces autorisations/interdictions sont mémorisés sous la forme d'une suite de bits, chaque bit autorisant/interdisant une opération bien précise. Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, c'est lors du passage au 64 bits que l'interdiction d’exécution a été ajouté au jeu d’instruction. Avant, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'à été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé bit NX, est à 0 si la page n'est pas exécutable et à 1 sinon. De plus, diverses techniques intégrées aux processeur permettent de gérer celui-ci facilement. Notamment, le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
===La traduction d'adresse avec la pagination===
Une adresse (logique ou physique) se décompose donc en deux parties : un numéro de page qui identifie la page et un numéro permettant de localiser la donnée dans la page. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique. Pour faire cette traduction, il faut se souvenir des correspondances entre numéro de page et adresse de la page en mémoire fictive. Elles sont stockées dans une sorte de table, nommée la '''table des pages'''. Celle-ci contient aussi des bits pour savoir si une page est swappée sur le disque dur ou si elle est présente en RAM.
[[File:Paging.svg|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. Ainsi, pour chaque numéro (ou chaque adresse) de page logique, on stocke l'adresse de base de la page correspondante en mémoire physique. La table des pages est unique pour chaque programme, vu que les correspondances entre adresses physiques et logiques ne sont pas les mêmes. Cette table des pages est souvent stockée dans la mémoire RAM, à un endroit bien précis, connu du processeur. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, puis de calculer l'adresse de notre donnée, et enfin d’accéder à la donnée voulue. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages inversées====
Sur certains systèmes, la taille d'une table des pages serait beaucoup trop grande en utilisant les techniques vues au-dessus. Pour éviter cela, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur voudra convertir une adresse virtuelle en adresse physique, la MMU prendra le numéro de page de l'adresse virtuelle, et le recherchera dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, les concepteurs de systèmes d'exploitation peuvent stocker celle-ci avec ce que l'on appelle une table de hachage.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des page par processus/programme lancé sur l'ordinateur.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
Mais cela a un léger défaut : la table des pages de chaque processus bouffe beaucoup de mémoire. Aussi, les concepteurs de processeurs et de systèmes d'exploitation ont adapté les tables des pages précédentes pour limiter la casse. Ils ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Ainsi, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Chaque sous-espace d'adressage a sa propre page des tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage qui contiennent au moins une donnée.
Les premiers bits de l'adresse vont servir à sélectionner quelle table des pages utiliser. Pour cela, il faut connaître l'adresse à laquelle est stockée chaque table des pages. Pour cela, on utilise une super-table des pages, qui stocke les adresses de début des tables des pages de chaque sous-espace. On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
===Le ''Translation Lookaside Buffer''===
Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''translation lookaside buffer''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances. De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeur superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
Pour des raisons de performances, ce cache est parfois découpé en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rares que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
demande de faire des accès aux données en même temps qu'on charge une isntruction.
===Le remplacement des pages mémoires===
La mémoire physique contient moins de pages que la mémoire virtuelle et il faut trouver un moyen pour que cela ne pose pas de problème. La solution consiste à utiliser des mémoires de stockage comme mémoire d'appoint : si on a besoin de plus de pages mémoires que la mémoire physique n'en contient, certaines pages mémoires vont être déplacées sur le disque dur pour faire de la place. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Lorsque l'on veut traduire l'adresse logique d'une page mémoire déplacée sur le disque dur, la MMU ne va pas pouvoir associer l'adresse logique à une adresse en mémoire RAM. Elle va alors lever une exception matérielle dont la routine rapatriera la page en mémoire RAM.
Charger une page en RAM ne pose aucun problème tant qu'il existe des pages inoccupée en RAM. Mais si toute la RAM est pleine, il faut déplacer une page mémoire en RAM sur le disque dur pour faire de la place. Tout cela est effectué par le système d'exploitation dont j'ai parlé plus haut. Notons que si on supprime une donnée dont on aura besoin dans le futur, il faudra recharger celle-ci, ce qui prend du temps. Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Les plus simples sont les suivants.
* Aléatoire : on choisit la page au hasard.
* FIFO : on supprime la donnée qui a été chargée dans la mémoire avant toutes les autres.
* LRU : on supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres.
* LFU : on vire la page qui est lue ou écrite le moins souvent comparée aux autres.
* etc.
Ces algorithmes ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter une interruption de gestion de page miss alors que la page contenant le code de l'interruption est placée sur le disque dur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le matériel réseau
| prevText=Le matériel réseau
| next=Les mémoires cache
| nextText=Les mémoires cache
}}
</noinclude>
je1npr4zdya61q3ktfb7nmjc0bdlfdg
682064
682061
2022-07-20T20:14:43Z
Mewtow
31375
/* Le Translation Lookaside Buffer */
wikitext
text/x-wiki
Un programme peut être exécuté sur des ordinateurs ayant des capacités mémoires diverses et variées et dans des conditions très différentes. Et il faut faire en sorte qu'un programme fonctionne sur des ordinateur ayant peu de mémoire sans poser problème. Après tout, quand on conçoit un programme, on ne sait pas toujours quelle sera la quantité mémoire que notre ordinateur contiendra, et encore moins comment celle-ci sera partagée entre nos différentes programmes en cours d’exécution : s'affranchir de limitations sur la quantité de mémoire disponible est un plus vraiment appréciable. Ce besoin est appelé l''''abstraction matérielle de la mémoire'''.
Enfin, plusieurs programmes sont présents en même temps dans notre ordinateur, et doivent se partager la mémoire physique. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur. Cela a des conséquences qui vont de comiques à catastrophiques, et cela fini très souvent par un joli plantage. Il faut donc introduire des mécanismes de '''protection mémoire'''. Pour éviter cela, chaque programme a accès à des portions de la mémoire dans laquelle lui seul peut écrire ou lire. Le reste de la mémoire est inaccessible en lecture et en écriture, à part pour la mémoire partagée entre différents programmes. Toute tentative d'accès à une partie de la mémoire non autorisée déclenchera une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui devra être traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
Ces détails concernant l'organisation et la gestion de la mémoire sont assez compliqués à gérer, et faire en sorte que les programmes applicatifs n'aient pas à s'en soucier est un plus vraiment appréciable. Ce genre de problèmes a eu des solutions purement logicielles, mais quelques techniques règlent ces problèmes directement au niveau du matériel : on parle de '''mémoire virtuelle'''. Avec la mémoire virtuelle, tout se passe comme si notre programme était seul au monde et pouvait lire et écrire à toutes les adresses disponibles à partir de l'adresse zéro. Chaque programme a accès à autant d'adresses que ce que le processeur peut adresser : on se moque du fait que des adresses soient réservées aux périphériques, de la quantité de mémoire réellement installée sur l'ordinateur, ou de la mémoire prise par d'autres programmes en cours d’exécution. Pour éviter que ce surplus de fausse mémoire pose problème, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante.
[[File:Memoire virtuelle.svg|centre|vignette|Mémoire virtuelle]]
[[File:MMU and IOMMU.svg|droite|vignette|Les périphériques peuvent eux aussi utiliser la mémoire virtuelle. Dans ce cas, ceux-ci intègrent une MMU dans leurs circuits. Ces MMU, aussi appelées IOMMU, sont séparées de la MMU du processeur.]]
Bien sûr, les adresses de la fausse mémoire vue par le programme sont des adresses fictives, qui devront être traduites en adresses mémoires réelles pour être utilisées. Les fausses adresses sont ce qu'on appelle des adresses logiques, alors que les adresses réelles sont appelées adresses physiques. Pour implémenter cette technique, il faut rajouter un circuit qui traduit les adresses logiques en adresses physiques : ce circuit est appelé la '''memory management unit'''. Il faut préciser qu'il existe différentes méthodes pour gérer ces adresses logiques et les transformer en adresses physiques : les principales sont la segmentation et la pagination. La suite du chapitre va détailler ces deux techniques.
==Le ''bank switching''==
[[File:Bankswitch memory map.svg|vignette|Exemple de Bank switching.]]
Le '''''bank switching''''', aussi appelé '''commutation de banque''', permet d'utiliser un bus d'adresse plus grand que les adresses du processeur. L'adresse réelle est plus grande que l'adresse utilisée par le processeur, les bits manquants étant fournit par un registre configurable du processeur : le '''registre de banque'''. L'espace mémoire du processeur est présent en plusieurs exemplaires, sélectionnés par la valeur du registre de banque. Chaque exemplaire de l'ensemble des adresses du processeur s'appelle une banque. On peut changer de banque en changeant le contenu de ce registre : le processeur dispose souvent d'instructions spécialisées qui en sont capables.
En répartissant les données utiles dans différentes banques, le processeur peut donc adresser beaucoup plus de mémoire. De plus, cette technique se marie assez bien avec les entrées-sorties mappées en mémoire. Par exemple, supposons que j'ai besoin d'adresser une mémoire ROM de 4 kibioctets, une RAM de 8 kibioctets, et divers périphériques. Le processeur a un bus d'adresse de 12 bits, ce qui limite à 4kibioctets. Dans ce cas, je peux réserver 4 banques : une pour la ROM, une pour les périphériques, et deux banques qui contiennent chacune la moitié de la RAM. La simplicité et l'efficacité de cette technique font qu'elle est beaucoup utilisée dans l'informatique embarquée.
{| class="wikitable flexible"
|[[File:Banque mémoire.png|Banque mémoire.]]
|[[File:Registre de banque.png|Registre de banque.]]
|}
==La segmentation==
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Segments typiques des programmes sur OS modernes.]]
Sur les vieux systèmes d'exploitation, chaque programme recevait un bloc de RAM pour lui tout seul, une '''partition mémoire'''. La segmentation est une amélioration de cette technique, qui affecte une ou plusieurs partitions par programme. Cela permet à un seul processus d'avoir plusieurs espaces d'adressages, plusieurs mémoires virtuelles à lui tout seul. L'utilité est qu'un programme est rarement un tout unique, mais peut souvent être décomposé en plusieurs ensembles de données/instructions séparés, qui peuvent grossir ou se réduire suivant les circonstances. Par exemple, la pile a une taille qui varie beaucoup, tandis que le code du programme ne change pas. La segmentation permet de découper la mémoire virtuelle d'un processus en sous-partitions de taille variable qu'on appelle improprement des '''segments'''. Ainsi, un programme peut utiliser des segments différents pour la pile, le tas, le programme lui-même, et les variables globales. Chaque segment pourra grandir ou diminuer à sa guise, suivant les besoins, ce qui serait beaucoup plus difficile avec une seule partition mémoire.
===La relocation avec la segmentation===
Chaque segment peut être placé n'importe où en mémoire physique par l'OS, ce qui fait qu'il n'a pas d'adresse fixée en mémoire physique et peut être déplacé comme bon nous semble. C'est le système d'exploitation qui gère le placement des partitions/segments dans la mémoire physique. Ainsi, les adresses de destination des branchements et les adresses des données ne sont jamais les mêmes. Il faut donc trouver un moyen pour que les programmes fonctionnent avec des adresses qui changent d'une exécution à l'autre. Ce besoin est appelé la '''relocalisation'''. Pour résoudre ce problème, le compilateur considère que le programme commence à l'adresse zéro et laisse l'OS corriger les adresses du programme. Cette correction d'adresse demande simplement d'ajouter un décalage, égal à la première adresse de la partition mémoire. Cette correction peut se faire de deux manières : soit l'OS corrige chaque adresse lors du lancement du programme, soit le processeur s'en charge à chaque accès mémoire. Dans le second cas, cette première adresse est mémorisée dans un '''registre de base''', mis à jour automatiquement lors de chaque changement de programme.
[[File:Segmentation et relocation.png|centre|vignette|upright=2|Segmentation et relocation.]]
Sur les processeurs x86, la mémoire virtuelle est découpée en 2^16 segments de 2^32 bits, ce qui donne des adresses de 48 bits. Pour identifier un segment, seuls les 16 bits de poids fort de l'adresse 48 bits sont utiles : ils forment ce qu'on appelle un sélecteur de segment. Les 32 bits servent à identifier la position de la donnée dans un segment : ils sont appelés le décalage (offset). La relocalisation se fait de la même manière pour un segment que pour une partition mémoire. Pour cela, l'OS associe chaque segment à son adresse de base dans une table de correspondance, appelée la '''table des segments'''. Elle est unique pour chaque programme : la même adresse logique ne donnera pas la même adresse physique selon le programme.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse.]]
Dans le cas le plus simple, la table des segments est stockée en mémoire RAM, le processeur ne contenant qu'un registre de base mis à jour à chaque changement de segment.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Une autre solution consiste à charger la table de correspondance complète dans un banc de registre dédié.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
===La protection mémoire avec la segmentation===
Un programme ne doit avoir accès qu'à la partition qui lui est dédiée et pas aux autres, sauf dans quelques rares exceptions. Toute tentative d'accès à une autre partition doit déclencher une exception matérielle, qui entraîne souvent l'apparition d'un message d'erreur. Tout cela est pris en charge par le système d'exploitation, par l'intermédiaire de mécanismes de '''protection mémoire''', que nous allons maintenant aborder.
Pour commencer, le processeur (ou l'OS) doivent détecter les accès hors-partition, à savoir un programme qui lit/écrit de la mémoire au-delà de la partition qui lui réservée. La solution est de mémoriser les limites du segment dans la page des segments et de vérifier que les accès mémoire ne se font pas au-delà de cette limite. L'adresse de fin de segment est mémorisée dans la table des segments et est récupérée lors de chaque accès. Le processeur vérifie si l'accès se fait dans les clou, en comparant l'adresse accédée avec l'adresse limite. Une autre solution consiste à mémoriser non pas l'adresse, mais l'offset maximal possible dans le segment en cours. Cela économise quelques bits par entrée dans la table des tables. Voici comment se passe la traduction d'une adresse avec la segmentation, en tenant compte de la vérification des accès hors-segment.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Vient ensuite la '''gestion des droits d'accès''' : chaque partition/segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Par exemple, il est possible d'interdire d'exécuter quoique ce soit de localisé dans certains segments, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation. L'OS ou la MMU mémorisent les autorisations pour chaque segment, qui sont rassemblées avec d'autres informations (registre de base et limite) dans un '''descripteur de segment'''. Pour se simplifier la tache, les concepteurs de processeurs et de systèmes d'exploitation ont décidé de regrouper ces descripteurs dans une portion de la mémoire, spécialement réservée pour l'occasion : la '''table des descripteurs de segment'''. Pour des raisons de performance, le processeur utilise un registre pour mémoriser le descripteur de segment du segment en cours d'utilisation. Quand on accède à un segment, son descripteur est chargé dans des registres du processeur, l'ancien est effacé.
[[File:SegmentDescriptor.svg|centre|vignette|upright=2|Schéma d'un descripteur de segment sur une architecture x86.]]
===Le partage de segments===
Il faut préciser qu'il est possible de partager des segments entre applications. Il suffit de configurer les tables de segment convenablement. Cela peut servir quand plusieurs instances d'une même applications sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instance.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Il est aussi possible de faire en sorte que deux segments se recouvrent l'un autre, à savoir que les deux segments partagent la même mémoire physique. Cela peut servir à partager de la mémoire entre plusieurs applications qui doivent communiquer entre elles.
[[File:Overlapping realmode segments.svg|centre|vignette|upright=1.5|Recouvrement de segments.]]
==La pagination==
De nos jours, la segmentation est obsolète et n'est plus utilisée : à la place, les OS et processeurs utilisent la pagination. Avec la pagination, la mémoire virtuelle et la mémoire physique sont découpées en blocs de taille fixe, appelés des '''pages mémoires'''. La différence avec les segments est que les segments sont de taille variable, alors que les pages sont de taille fixe. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique. Cependant, le nombre total de pages en mémoire virtuelle dépasse celui de la mémoire physique.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Le surnombre est simplement placé sur le disque dur, dans un fichier appelé le '''fichier d'échange'''. Les pages virtuelles vont ainsi faire référence soit à une page en mémoire physique, soit à une page sur le disque dur. Tout accès à une page sur le disque dur va charger celle-ci dans la mémoire RAM, dans une page vide. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins.
[[File:Lazy Swapper.jpg|centre|vignette|upright=2|Mémoire virtuelle paginée et fichier d'échange.]]
===La protection mémoire avec la pagination===
La protection mémoire est garantie avec des '''clés de protection''', un nombre unique à chaque programme. Le processeur mémorise, pour chaque page, la clé de protection du programme qui a réservé la page. A chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution, et celle de la page adressée. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
Comme avec la segmentation, chaque page a des droits d'accès précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page. Ces autorisations/interdictions sont mémorisés sous la forme d'une suite de bits, chaque bit autorisant/interdisant une opération bien précise. Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, c'est lors du passage au 64 bits que l'interdiction d’exécution a été ajouté au jeu d’instruction. Avant, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'à été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé bit NX, est à 0 si la page n'est pas exécutable et à 1 sinon. De plus, diverses techniques intégrées aux processeur permettent de gérer celui-ci facilement. Notamment, le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
===La traduction d'adresse avec la pagination===
Une adresse (logique ou physique) se décompose donc en deux parties : un numéro de page qui identifie la page et un numéro permettant de localiser la donnée dans la page. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique. Pour faire cette traduction, il faut se souvenir des correspondances entre numéro de page et adresse de la page en mémoire fictive. Elles sont stockées dans une sorte de table, nommée la '''table des pages'''. Celle-ci contient aussi des bits pour savoir si une page est swappée sur le disque dur ou si elle est présente en RAM.
[[File:Paging.svg|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. Ainsi, pour chaque numéro (ou chaque adresse) de page logique, on stocke l'adresse de base de la page correspondante en mémoire physique. La table des pages est unique pour chaque programme, vu que les correspondances entre adresses physiques et logiques ne sont pas les mêmes. Cette table des pages est souvent stockée dans la mémoire RAM, à un endroit bien précis, connu du processeur. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, puis de calculer l'adresse de notre donnée, et enfin d’accéder à la donnée voulue. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages inversées====
Sur certains systèmes, la taille d'une table des pages serait beaucoup trop grande en utilisant les techniques vues au-dessus. Pour éviter cela, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur voudra convertir une adresse virtuelle en adresse physique, la MMU prendra le numéro de page de l'adresse virtuelle, et le recherchera dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, les concepteurs de systèmes d'exploitation peuvent stocker celle-ci avec ce que l'on appelle une table de hachage.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des page par processus/programme lancé sur l'ordinateur.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
Mais cela a un léger défaut : la table des pages de chaque processus bouffe beaucoup de mémoire. Aussi, les concepteurs de processeurs et de systèmes d'exploitation ont adapté les tables des pages précédentes pour limiter la casse. Ils ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Ainsi, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Chaque sous-espace d'adressage a sa propre page des tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage qui contiennent au moins une donnée.
Les premiers bits de l'adresse vont servir à sélectionner quelle table des pages utiliser. Pour cela, il faut connaître l'adresse à laquelle est stockée chaque table des pages. Pour cela, on utilise une super-table des pages, qui stocke les adresses de début des tables des pages de chaque sous-espace. On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
===Le ''Translation Lookaside Buffer''===
Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''translation lookaside buffer''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances. De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeur superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
Pour des raisons de performances, ce cache est parfois découpé en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rares que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
===Le remplacement des pages mémoires===
La mémoire physique contient moins de pages que la mémoire virtuelle et il faut trouver un moyen pour que cela ne pose pas de problème. La solution consiste à utiliser des mémoires de stockage comme mémoire d'appoint : si on a besoin de plus de pages mémoires que la mémoire physique n'en contient, certaines pages mémoires vont être déplacées sur le disque dur pour faire de la place. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Lorsque l'on veut traduire l'adresse logique d'une page mémoire déplacée sur le disque dur, la MMU ne va pas pouvoir associer l'adresse logique à une adresse en mémoire RAM. Elle va alors lever une exception matérielle dont la routine rapatriera la page en mémoire RAM.
Charger une page en RAM ne pose aucun problème tant qu'il existe des pages inoccupée en RAM. Mais si toute la RAM est pleine, il faut déplacer une page mémoire en RAM sur le disque dur pour faire de la place. Tout cela est effectué par le système d'exploitation dont j'ai parlé plus haut. Notons que si on supprime une donnée dont on aura besoin dans le futur, il faudra recharger celle-ci, ce qui prend du temps. Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Les plus simples sont les suivants.
* Aléatoire : on choisit la page au hasard.
* FIFO : on supprime la donnée qui a été chargée dans la mémoire avant toutes les autres.
* LRU : on supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres.
* LFU : on vire la page qui est lue ou écrite le moins souvent comparée aux autres.
* etc.
Ces algorithmes ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter une interruption de gestion de page miss alors que la page contenant le code de l'interruption est placée sur le disque dur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le matériel réseau
| prevText=Le matériel réseau
| next=Les mémoires cache
| nextText=Les mémoires cache
}}
</noinclude>
icz1ibmt9xxg1nswwpon0a3pikl1c52
682065
682064
2022-07-20T20:29:31Z
Mewtow
31375
/* La pagination */
wikitext
text/x-wiki
Un programme peut être exécuté sur des ordinateurs ayant des capacités mémoires diverses et variées et dans des conditions très différentes. Et il faut faire en sorte qu'un programme fonctionne sur des ordinateur ayant peu de mémoire sans poser problème. Après tout, quand on conçoit un programme, on ne sait pas toujours quelle sera la quantité mémoire que notre ordinateur contiendra, et encore moins comment celle-ci sera partagée entre nos différentes programmes en cours d’exécution : s'affranchir de limitations sur la quantité de mémoire disponible est un plus vraiment appréciable. Ce besoin est appelé l''''abstraction matérielle de la mémoire'''.
Enfin, plusieurs programmes sont présents en même temps dans notre ordinateur, et doivent se partager la mémoire physique. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur. Cela a des conséquences qui vont de comiques à catastrophiques, et cela fini très souvent par un joli plantage. Il faut donc introduire des mécanismes de '''protection mémoire'''. Pour éviter cela, chaque programme a accès à des portions de la mémoire dans laquelle lui seul peut écrire ou lire. Le reste de la mémoire est inaccessible en lecture et en écriture, à part pour la mémoire partagée entre différents programmes. Toute tentative d'accès à une partie de la mémoire non autorisée déclenchera une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui devra être traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
Ces détails concernant l'organisation et la gestion de la mémoire sont assez compliqués à gérer, et faire en sorte que les programmes applicatifs n'aient pas à s'en soucier est un plus vraiment appréciable. Ce genre de problèmes a eu des solutions purement logicielles, mais quelques techniques règlent ces problèmes directement au niveau du matériel : on parle de '''mémoire virtuelle'''. Avec la mémoire virtuelle, tout se passe comme si notre programme était seul au monde et pouvait lire et écrire à toutes les adresses disponibles à partir de l'adresse zéro. Chaque programme a accès à autant d'adresses que ce que le processeur peut adresser : on se moque du fait que des adresses soient réservées aux périphériques, de la quantité de mémoire réellement installée sur l'ordinateur, ou de la mémoire prise par d'autres programmes en cours d’exécution. Pour éviter que ce surplus de fausse mémoire pose problème, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante.
[[File:Memoire virtuelle.svg|centre|vignette|Mémoire virtuelle]]
[[File:MMU and IOMMU.svg|droite|vignette|Les périphériques peuvent eux aussi utiliser la mémoire virtuelle. Dans ce cas, ceux-ci intègrent une MMU dans leurs circuits. Ces MMU, aussi appelées IOMMU, sont séparées de la MMU du processeur.]]
Bien sûr, les adresses de la fausse mémoire vue par le programme sont des adresses fictives, qui devront être traduites en adresses mémoires réelles pour être utilisées. Les fausses adresses sont ce qu'on appelle des adresses logiques, alors que les adresses réelles sont appelées adresses physiques. Pour implémenter cette technique, il faut rajouter un circuit qui traduit les adresses logiques en adresses physiques : ce circuit est appelé la '''memory management unit'''. Il faut préciser qu'il existe différentes méthodes pour gérer ces adresses logiques et les transformer en adresses physiques : les principales sont la segmentation et la pagination. La suite du chapitre va détailler ces deux techniques.
==Le ''bank switching''==
[[File:Bankswitch memory map.svg|vignette|Exemple de Bank switching.]]
Le '''''bank switching''''', aussi appelé '''commutation de banque''', permet d'utiliser un bus d'adresse plus grand que les adresses du processeur. L'adresse réelle est plus grande que l'adresse utilisée par le processeur, les bits manquants étant fournit par un registre configurable du processeur : le '''registre de banque'''. L'espace mémoire du processeur est présent en plusieurs exemplaires, sélectionnés par la valeur du registre de banque. Chaque exemplaire de l'ensemble des adresses du processeur s'appelle une banque. On peut changer de banque en changeant le contenu de ce registre : le processeur dispose souvent d'instructions spécialisées qui en sont capables.
En répartissant les données utiles dans différentes banques, le processeur peut donc adresser beaucoup plus de mémoire. De plus, cette technique se marie assez bien avec les entrées-sorties mappées en mémoire. Par exemple, supposons que j'ai besoin d'adresser une mémoire ROM de 4 kibioctets, une RAM de 8 kibioctets, et divers périphériques. Le processeur a un bus d'adresse de 12 bits, ce qui limite à 4kibioctets. Dans ce cas, je peux réserver 4 banques : une pour la ROM, une pour les périphériques, et deux banques qui contiennent chacune la moitié de la RAM. La simplicité et l'efficacité de cette technique font qu'elle est beaucoup utilisée dans l'informatique embarquée.
{| class="wikitable flexible"
|[[File:Banque mémoire.png|Banque mémoire.]]
|[[File:Registre de banque.png|Registre de banque.]]
|}
==La segmentation==
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Segments typiques des programmes sur OS modernes.]]
Sur les vieux systèmes d'exploitation, chaque programme recevait un bloc de RAM pour lui tout seul, une '''partition mémoire'''. La segmentation est une amélioration de cette technique, qui affecte une ou plusieurs partitions par programme. Cela permet à un seul processus d'avoir plusieurs espaces d'adressages, plusieurs mémoires virtuelles à lui tout seul. L'utilité est qu'un programme est rarement un tout unique, mais peut souvent être décomposé en plusieurs ensembles de données/instructions séparés, qui peuvent grossir ou se réduire suivant les circonstances. Par exemple, la pile a une taille qui varie beaucoup, tandis que le code du programme ne change pas. La segmentation permet de découper la mémoire virtuelle d'un processus en sous-partitions de taille variable qu'on appelle improprement des '''segments'''. Ainsi, un programme peut utiliser des segments différents pour la pile, le tas, le programme lui-même, et les variables globales. Chaque segment pourra grandir ou diminuer à sa guise, suivant les besoins, ce qui serait beaucoup plus difficile avec une seule partition mémoire.
===La relocation avec la segmentation===
Chaque segment peut être placé n'importe où en mémoire physique par l'OS, ce qui fait qu'il n'a pas d'adresse fixée en mémoire physique et peut être déplacé comme bon nous semble. C'est le système d'exploitation qui gère le placement des partitions/segments dans la mémoire physique. Ainsi, les adresses de destination des branchements et les adresses des données ne sont jamais les mêmes. Il faut donc trouver un moyen pour que les programmes fonctionnent avec des adresses qui changent d'une exécution à l'autre. Ce besoin est appelé la '''relocalisation'''. Pour résoudre ce problème, le compilateur considère que le programme commence à l'adresse zéro et laisse l'OS corriger les adresses du programme. Cette correction d'adresse demande simplement d'ajouter un décalage, égal à la première adresse de la partition mémoire. Cette correction peut se faire de deux manières : soit l'OS corrige chaque adresse lors du lancement du programme, soit le processeur s'en charge à chaque accès mémoire. Dans le second cas, cette première adresse est mémorisée dans un '''registre de base''', mis à jour automatiquement lors de chaque changement de programme.
[[File:Segmentation et relocation.png|centre|vignette|upright=2|Segmentation et relocation.]]
Sur les processeurs x86, la mémoire virtuelle est découpée en 2^16 segments de 2^32 bits, ce qui donne des adresses de 48 bits. Pour identifier un segment, seuls les 16 bits de poids fort de l'adresse 48 bits sont utiles : ils forment ce qu'on appelle un sélecteur de segment. Les 32 bits servent à identifier la position de la donnée dans un segment : ils sont appelés le décalage (offset). La relocalisation se fait de la même manière pour un segment que pour une partition mémoire. Pour cela, l'OS associe chaque segment à son adresse de base dans une table de correspondance, appelée la '''table des segments'''. Elle est unique pour chaque programme : la même adresse logique ne donnera pas la même adresse physique selon le programme.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse.]]
Dans le cas le plus simple, la table des segments est stockée en mémoire RAM, le processeur ne contenant qu'un registre de base mis à jour à chaque changement de segment.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Une autre solution consiste à charger la table de correspondance complète dans un banc de registre dédié.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
===La protection mémoire avec la segmentation===
Un programme ne doit avoir accès qu'à la partition qui lui est dédiée et pas aux autres, sauf dans quelques rares exceptions. Toute tentative d'accès à une autre partition doit déclencher une exception matérielle, qui entraîne souvent l'apparition d'un message d'erreur. Tout cela est pris en charge par le système d'exploitation, par l'intermédiaire de mécanismes de '''protection mémoire''', que nous allons maintenant aborder.
Pour commencer, le processeur (ou l'OS) doivent détecter les accès hors-partition, à savoir un programme qui lit/écrit de la mémoire au-delà de la partition qui lui réservée. La solution est de mémoriser les limites du segment dans la page des segments et de vérifier que les accès mémoire ne se font pas au-delà de cette limite. L'adresse de fin de segment est mémorisée dans la table des segments et est récupérée lors de chaque accès. Le processeur vérifie si l'accès se fait dans les clou, en comparant l'adresse accédée avec l'adresse limite. Une autre solution consiste à mémoriser non pas l'adresse, mais l'offset maximal possible dans le segment en cours. Cela économise quelques bits par entrée dans la table des tables. Voici comment se passe la traduction d'une adresse avec la segmentation, en tenant compte de la vérification des accès hors-segment.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Vient ensuite la '''gestion des droits d'accès''' : chaque partition/segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Par exemple, il est possible d'interdire d'exécuter quoique ce soit de localisé dans certains segments, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation. L'OS ou la MMU mémorisent les autorisations pour chaque segment, qui sont rassemblées avec d'autres informations (registre de base et limite) dans un '''descripteur de segment'''. Pour se simplifier la tache, les concepteurs de processeurs et de systèmes d'exploitation ont décidé de regrouper ces descripteurs dans une portion de la mémoire, spécialement réservée pour l'occasion : la '''table des descripteurs de segment'''. Pour des raisons de performance, le processeur utilise un registre pour mémoriser le descripteur de segment du segment en cours d'utilisation. Quand on accède à un segment, son descripteur est chargé dans des registres du processeur, l'ancien est effacé.
[[File:SegmentDescriptor.svg|centre|vignette|upright=2|Schéma d'un descripteur de segment sur une architecture x86.]]
===Le partage de segments===
Il faut préciser qu'il est possible de partager des segments entre applications. Il suffit de configurer les tables de segment convenablement. Cela peut servir quand plusieurs instances d'une même applications sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instance.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Il est aussi possible de faire en sorte que deux segments se recouvrent l'un autre, à savoir que les deux segments partagent la même mémoire physique. Cela peut servir à partager de la mémoire entre plusieurs applications qui doivent communiquer entre elles.
[[File:Overlapping realmode segments.svg|centre|vignette|upright=1.5|Recouvrement de segments.]]
==La pagination==
De nos jours, la segmentation est obsolète et n'est plus utilisée : à la place, les OS et processeurs utilisent la pagination. Avec la pagination, la mémoire virtuelle et la mémoire physique sont découpées en blocs de taille fixe, appelés des '''pages mémoires'''. La différence avec les segments est que les segments sont de taille variable, alors que les pages sont de taille fixe. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique. Cependant, le nombre total de pages en mémoire virtuelle dépasse celui de la mémoire physique.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Le surnombre est simplement placé sur le disque dur, dans un fichier appelé le '''fichier d'échange'''. Les pages virtuelles vont ainsi faire référence soit à une page en mémoire physique, soit à une page sur le disque dur. Tout accès à une page sur le disque dur va charger celle-ci dans la mémoire RAM, dans une page vide. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins.
[[File:Lazy Swapper.jpg|centre|vignette|upright=2|Mémoire virtuelle paginée et fichier d'échange.]]
===La protection mémoire avec la pagination===
La protection mémoire est garantie avec des '''clés de protection''', un nombre unique à chaque programme. Le processeur mémorise, pour chaque page, la clé de protection du programme qui a réservé la page. A chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution, et celle de la page adressée. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
Comme avec la segmentation, chaque page a des droits d'accès précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page. Ces autorisations/interdictions sont mémorisés sous la forme d'une suite de bits, chaque bit autorisant/interdisant une opération bien précise. Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, c'est lors du passage au 64 bits que l'interdiction d’exécution a été ajouté au jeu d’instruction. Avant, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'à été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé bit NX, est à 0 si la page n'est pas exécutable et à 1 sinon. De plus, diverses techniques intégrées aux processeur permettent de gérer celui-ci facilement. Notamment, le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
===La traduction d'adresse avec la pagination===
Une adresse (logique ou physique) se décompose donc en deux parties : un numéro de page qui identifie la page et un numéro permettant de localiser la donnée dans la page. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique. Pour faire cette traduction, il faut se souvenir des correspondances entre numéro de page et adresse de la page en mémoire fictive. Elles sont stockées dans une sorte de table, nommée la '''table des pages'''. Celle-ci contient aussi des bits pour savoir si une page est swappée sur le disque dur ou si elle est présente en RAM.
[[File:Paging.svg|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. Ainsi, pour chaque numéro (ou chaque adresse) de page logique, on stocke l'adresse de base de la page correspondante en mémoire physique. La table des pages est unique pour chaque programme, vu que les correspondances entre adresses physiques et logiques ne sont pas les mêmes. Cette table des pages est souvent stockée dans la mémoire RAM, à un endroit bien précis, connu du processeur. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, puis de calculer l'adresse de notre donnée, et enfin d’accéder à la donnée voulue. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages inversées====
Sur certains systèmes, la taille d'une table des pages serait beaucoup trop grande en utilisant les techniques vues au-dessus. Pour éviter cela, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur voudra convertir une adresse virtuelle en adresse physique, la MMU prendra le numéro de page de l'adresse virtuelle, et le recherchera dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, les concepteurs de systèmes d'exploitation peuvent stocker celle-ci avec ce que l'on appelle une table de hachage.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des page par processus/programme lancé sur l'ordinateur.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
Mais cela a un léger défaut : la table des pages de chaque processus bouffe beaucoup de mémoire. Aussi, les concepteurs de processeurs et de systèmes d'exploitation ont adapté les tables des pages précédentes pour limiter la casse. Ils ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Ainsi, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Chaque sous-espace d'adressage a sa propre page des tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage qui contiennent au moins une donnée.
Les premiers bits de l'adresse vont servir à sélectionner quelle table des pages utiliser. Pour cela, il faut connaître l'adresse à laquelle est stockée chaque table des pages. Pour cela, on utilise une super-table des pages, qui stocke les adresses de début des tables des pages de chaque sous-espace. On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
===Le ''Translation Lookaside Buffer''===
Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''translation lookaside buffer''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Pour ceux qui ont déjà lu le chapitre suivant, voici comment : la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif.
====Les succès et défauts d'accès à la TLB====
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances. De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeur superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
====La hiérarchie des TLB====
Pour des raisons de performances, ce cache est parfois découpé en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rares que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes tient dans l'associativité de la TLB, un concept que nous verrons dans le chapitre suivant sur les caches. Un autre problème est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages. Ces deux problèmes interagissent entre eux et font que l'usage d'une TLB unique est possible, mais pas forcément optimal. L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée pr la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===Le remplacement des pages mémoires===
La mémoire physique contient moins de pages que la mémoire virtuelle et il faut trouver un moyen pour que cela ne pose pas de problème. La solution consiste à utiliser des mémoires de stockage comme mémoire d'appoint : si on a besoin de plus de pages mémoires que la mémoire physique n'en contient, certaines pages mémoires vont être déplacées sur le disque dur pour faire de la place. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Lorsque l'on veut traduire l'adresse logique d'une page mémoire déplacée sur le disque dur, la MMU ne va pas pouvoir associer l'adresse logique à une adresse en mémoire RAM. Elle va alors lever une exception matérielle dont la routine rapatriera la page en mémoire RAM.
Charger une page en RAM ne pose aucun problème tant qu'il existe des pages inoccupée en RAM. Mais si toute la RAM est pleine, il faut déplacer une page mémoire en RAM sur le disque dur pour faire de la place. Tout cela est effectué par le système d'exploitation dont j'ai parlé plus haut. Notons que si on supprime une donnée dont on aura besoin dans le futur, il faudra recharger celle-ci, ce qui prend du temps. Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Les plus simples sont les suivants.
* Aléatoire : on choisit la page au hasard.
* FIFO : on supprime la donnée qui a été chargée dans la mémoire avant toutes les autres.
* LRU : on supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres.
* LFU : on vire la page qui est lue ou écrite le moins souvent comparée aux autres.
* etc.
Ces algorithmes ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter une interruption de gestion de page miss alors que la page contenant le code de l'interruption est placée sur le disque dur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le matériel réseau
| prevText=Le matériel réseau
| next=Les mémoires cache
| nextText=Les mémoires cache
}}
</noinclude>
9zyfewi102f3bdg8plcr1nfpy1gzidh
682066
682065
2022-07-20T20:31:05Z
Mewtow
31375
/* La hiérarchie des TLB */
wikitext
text/x-wiki
Un programme peut être exécuté sur des ordinateurs ayant des capacités mémoires diverses et variées et dans des conditions très différentes. Et il faut faire en sorte qu'un programme fonctionne sur des ordinateur ayant peu de mémoire sans poser problème. Après tout, quand on conçoit un programme, on ne sait pas toujours quelle sera la quantité mémoire que notre ordinateur contiendra, et encore moins comment celle-ci sera partagée entre nos différentes programmes en cours d’exécution : s'affranchir de limitations sur la quantité de mémoire disponible est un plus vraiment appréciable. Ce besoin est appelé l''''abstraction matérielle de la mémoire'''.
Enfin, plusieurs programmes sont présents en même temps dans notre ordinateur, et doivent se partager la mémoire physique. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur. Cela a des conséquences qui vont de comiques à catastrophiques, et cela fini très souvent par un joli plantage. Il faut donc introduire des mécanismes de '''protection mémoire'''. Pour éviter cela, chaque programme a accès à des portions de la mémoire dans laquelle lui seul peut écrire ou lire. Le reste de la mémoire est inaccessible en lecture et en écriture, à part pour la mémoire partagée entre différents programmes. Toute tentative d'accès à une partie de la mémoire non autorisée déclenchera une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui devra être traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
Ces détails concernant l'organisation et la gestion de la mémoire sont assez compliqués à gérer, et faire en sorte que les programmes applicatifs n'aient pas à s'en soucier est un plus vraiment appréciable. Ce genre de problèmes a eu des solutions purement logicielles, mais quelques techniques règlent ces problèmes directement au niveau du matériel : on parle de '''mémoire virtuelle'''. Avec la mémoire virtuelle, tout se passe comme si notre programme était seul au monde et pouvait lire et écrire à toutes les adresses disponibles à partir de l'adresse zéro. Chaque programme a accès à autant d'adresses que ce que le processeur peut adresser : on se moque du fait que des adresses soient réservées aux périphériques, de la quantité de mémoire réellement installée sur l'ordinateur, ou de la mémoire prise par d'autres programmes en cours d’exécution. Pour éviter que ce surplus de fausse mémoire pose problème, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante.
[[File:Memoire virtuelle.svg|centre|vignette|Mémoire virtuelle]]
[[File:MMU and IOMMU.svg|droite|vignette|Les périphériques peuvent eux aussi utiliser la mémoire virtuelle. Dans ce cas, ceux-ci intègrent une MMU dans leurs circuits. Ces MMU, aussi appelées IOMMU, sont séparées de la MMU du processeur.]]
Bien sûr, les adresses de la fausse mémoire vue par le programme sont des adresses fictives, qui devront être traduites en adresses mémoires réelles pour être utilisées. Les fausses adresses sont ce qu'on appelle des adresses logiques, alors que les adresses réelles sont appelées adresses physiques. Pour implémenter cette technique, il faut rajouter un circuit qui traduit les adresses logiques en adresses physiques : ce circuit est appelé la '''memory management unit'''. Il faut préciser qu'il existe différentes méthodes pour gérer ces adresses logiques et les transformer en adresses physiques : les principales sont la segmentation et la pagination. La suite du chapitre va détailler ces deux techniques.
==Le ''bank switching''==
[[File:Bankswitch memory map.svg|vignette|Exemple de Bank switching.]]
Le '''''bank switching''''', aussi appelé '''commutation de banque''', permet d'utiliser un bus d'adresse plus grand que les adresses du processeur. L'adresse réelle est plus grande que l'adresse utilisée par le processeur, les bits manquants étant fournit par un registre configurable du processeur : le '''registre de banque'''. L'espace mémoire du processeur est présent en plusieurs exemplaires, sélectionnés par la valeur du registre de banque. Chaque exemplaire de l'ensemble des adresses du processeur s'appelle une banque. On peut changer de banque en changeant le contenu de ce registre : le processeur dispose souvent d'instructions spécialisées qui en sont capables.
En répartissant les données utiles dans différentes banques, le processeur peut donc adresser beaucoup plus de mémoire. De plus, cette technique se marie assez bien avec les entrées-sorties mappées en mémoire. Par exemple, supposons que j'ai besoin d'adresser une mémoire ROM de 4 kibioctets, une RAM de 8 kibioctets, et divers périphériques. Le processeur a un bus d'adresse de 12 bits, ce qui limite à 4kibioctets. Dans ce cas, je peux réserver 4 banques : une pour la ROM, une pour les périphériques, et deux banques qui contiennent chacune la moitié de la RAM. La simplicité et l'efficacité de cette technique font qu'elle est beaucoup utilisée dans l'informatique embarquée.
{| class="wikitable flexible"
|[[File:Banque mémoire.png|Banque mémoire.]]
|[[File:Registre de banque.png|Registre de banque.]]
|}
==La segmentation==
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Segments typiques des programmes sur OS modernes.]]
Sur les vieux systèmes d'exploitation, chaque programme recevait un bloc de RAM pour lui tout seul, une '''partition mémoire'''. La segmentation est une amélioration de cette technique, qui affecte une ou plusieurs partitions par programme. Cela permet à un seul processus d'avoir plusieurs espaces d'adressages, plusieurs mémoires virtuelles à lui tout seul. L'utilité est qu'un programme est rarement un tout unique, mais peut souvent être décomposé en plusieurs ensembles de données/instructions séparés, qui peuvent grossir ou se réduire suivant les circonstances. Par exemple, la pile a une taille qui varie beaucoup, tandis que le code du programme ne change pas. La segmentation permet de découper la mémoire virtuelle d'un processus en sous-partitions de taille variable qu'on appelle improprement des '''segments'''. Ainsi, un programme peut utiliser des segments différents pour la pile, le tas, le programme lui-même, et les variables globales. Chaque segment pourra grandir ou diminuer à sa guise, suivant les besoins, ce qui serait beaucoup plus difficile avec une seule partition mémoire.
===La relocation avec la segmentation===
Chaque segment peut être placé n'importe où en mémoire physique par l'OS, ce qui fait qu'il n'a pas d'adresse fixée en mémoire physique et peut être déplacé comme bon nous semble. C'est le système d'exploitation qui gère le placement des partitions/segments dans la mémoire physique. Ainsi, les adresses de destination des branchements et les adresses des données ne sont jamais les mêmes. Il faut donc trouver un moyen pour que les programmes fonctionnent avec des adresses qui changent d'une exécution à l'autre. Ce besoin est appelé la '''relocalisation'''. Pour résoudre ce problème, le compilateur considère que le programme commence à l'adresse zéro et laisse l'OS corriger les adresses du programme. Cette correction d'adresse demande simplement d'ajouter un décalage, égal à la première adresse de la partition mémoire. Cette correction peut se faire de deux manières : soit l'OS corrige chaque adresse lors du lancement du programme, soit le processeur s'en charge à chaque accès mémoire. Dans le second cas, cette première adresse est mémorisée dans un '''registre de base''', mis à jour automatiquement lors de chaque changement de programme.
[[File:Segmentation et relocation.png|centre|vignette|upright=2|Segmentation et relocation.]]
Sur les processeurs x86, la mémoire virtuelle est découpée en 2^16 segments de 2^32 bits, ce qui donne des adresses de 48 bits. Pour identifier un segment, seuls les 16 bits de poids fort de l'adresse 48 bits sont utiles : ils forment ce qu'on appelle un sélecteur de segment. Les 32 bits servent à identifier la position de la donnée dans un segment : ils sont appelés le décalage (offset). La relocalisation se fait de la même manière pour un segment que pour une partition mémoire. Pour cela, l'OS associe chaque segment à son adresse de base dans une table de correspondance, appelée la '''table des segments'''. Elle est unique pour chaque programme : la même adresse logique ne donnera pas la même adresse physique selon le programme.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse.]]
Dans le cas le plus simple, la table des segments est stockée en mémoire RAM, le processeur ne contenant qu'un registre de base mis à jour à chaque changement de segment.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Une autre solution consiste à charger la table de correspondance complète dans un banc de registre dédié.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
===La protection mémoire avec la segmentation===
Un programme ne doit avoir accès qu'à la partition qui lui est dédiée et pas aux autres, sauf dans quelques rares exceptions. Toute tentative d'accès à une autre partition doit déclencher une exception matérielle, qui entraîne souvent l'apparition d'un message d'erreur. Tout cela est pris en charge par le système d'exploitation, par l'intermédiaire de mécanismes de '''protection mémoire''', que nous allons maintenant aborder.
Pour commencer, le processeur (ou l'OS) doivent détecter les accès hors-partition, à savoir un programme qui lit/écrit de la mémoire au-delà de la partition qui lui réservée. La solution est de mémoriser les limites du segment dans la page des segments et de vérifier que les accès mémoire ne se font pas au-delà de cette limite. L'adresse de fin de segment est mémorisée dans la table des segments et est récupérée lors de chaque accès. Le processeur vérifie si l'accès se fait dans les clou, en comparant l'adresse accédée avec l'adresse limite. Une autre solution consiste à mémoriser non pas l'adresse, mais l'offset maximal possible dans le segment en cours. Cela économise quelques bits par entrée dans la table des tables. Voici comment se passe la traduction d'une adresse avec la segmentation, en tenant compte de la vérification des accès hors-segment.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Vient ensuite la '''gestion des droits d'accès''' : chaque partition/segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Par exemple, il est possible d'interdire d'exécuter quoique ce soit de localisé dans certains segments, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation. L'OS ou la MMU mémorisent les autorisations pour chaque segment, qui sont rassemblées avec d'autres informations (registre de base et limite) dans un '''descripteur de segment'''. Pour se simplifier la tache, les concepteurs de processeurs et de systèmes d'exploitation ont décidé de regrouper ces descripteurs dans une portion de la mémoire, spécialement réservée pour l'occasion : la '''table des descripteurs de segment'''. Pour des raisons de performance, le processeur utilise un registre pour mémoriser le descripteur de segment du segment en cours d'utilisation. Quand on accède à un segment, son descripteur est chargé dans des registres du processeur, l'ancien est effacé.
[[File:SegmentDescriptor.svg|centre|vignette|upright=2|Schéma d'un descripteur de segment sur une architecture x86.]]
===Le partage de segments===
Il faut préciser qu'il est possible de partager des segments entre applications. Il suffit de configurer les tables de segment convenablement. Cela peut servir quand plusieurs instances d'une même applications sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instance.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Il est aussi possible de faire en sorte que deux segments se recouvrent l'un autre, à savoir que les deux segments partagent la même mémoire physique. Cela peut servir à partager de la mémoire entre plusieurs applications qui doivent communiquer entre elles.
[[File:Overlapping realmode segments.svg|centre|vignette|upright=1.5|Recouvrement de segments.]]
==La pagination==
De nos jours, la segmentation est obsolète et n'est plus utilisée : à la place, les OS et processeurs utilisent la pagination. Avec la pagination, la mémoire virtuelle et la mémoire physique sont découpées en blocs de taille fixe, appelés des '''pages mémoires'''. La différence avec les segments est que les segments sont de taille variable, alors que les pages sont de taille fixe. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique. Cependant, le nombre total de pages en mémoire virtuelle dépasse celui de la mémoire physique.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Le surnombre est simplement placé sur le disque dur, dans un fichier appelé le '''fichier d'échange'''. Les pages virtuelles vont ainsi faire référence soit à une page en mémoire physique, soit à une page sur le disque dur. Tout accès à une page sur le disque dur va charger celle-ci dans la mémoire RAM, dans une page vide. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins.
[[File:Lazy Swapper.jpg|centre|vignette|upright=2|Mémoire virtuelle paginée et fichier d'échange.]]
===La protection mémoire avec la pagination===
La protection mémoire est garantie avec des '''clés de protection''', un nombre unique à chaque programme. Le processeur mémorise, pour chaque page, la clé de protection du programme qui a réservé la page. A chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution, et celle de la page adressée. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
Comme avec la segmentation, chaque page a des droits d'accès précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page. Ces autorisations/interdictions sont mémorisés sous la forme d'une suite de bits, chaque bit autorisant/interdisant une opération bien précise. Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, c'est lors du passage au 64 bits que l'interdiction d’exécution a été ajouté au jeu d’instruction. Avant, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'à été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé bit NX, est à 0 si la page n'est pas exécutable et à 1 sinon. De plus, diverses techniques intégrées aux processeur permettent de gérer celui-ci facilement. Notamment, le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
===La traduction d'adresse avec la pagination===
Une adresse (logique ou physique) se décompose donc en deux parties : un numéro de page qui identifie la page et un numéro permettant de localiser la donnée dans la page. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique. Pour faire cette traduction, il faut se souvenir des correspondances entre numéro de page et adresse de la page en mémoire fictive. Elles sont stockées dans une sorte de table, nommée la '''table des pages'''. Celle-ci contient aussi des bits pour savoir si une page est swappée sur le disque dur ou si elle est présente en RAM.
[[File:Paging.svg|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. Ainsi, pour chaque numéro (ou chaque adresse) de page logique, on stocke l'adresse de base de la page correspondante en mémoire physique. La table des pages est unique pour chaque programme, vu que les correspondances entre adresses physiques et logiques ne sont pas les mêmes. Cette table des pages est souvent stockée dans la mémoire RAM, à un endroit bien précis, connu du processeur. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, puis de calculer l'adresse de notre donnée, et enfin d’accéder à la donnée voulue. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages inversées====
Sur certains systèmes, la taille d'une table des pages serait beaucoup trop grande en utilisant les techniques vues au-dessus. Pour éviter cela, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur voudra convertir une adresse virtuelle en adresse physique, la MMU prendra le numéro de page de l'adresse virtuelle, et le recherchera dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, les concepteurs de systèmes d'exploitation peuvent stocker celle-ci avec ce que l'on appelle une table de hachage.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des page par processus/programme lancé sur l'ordinateur.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
Mais cela a un léger défaut : la table des pages de chaque processus bouffe beaucoup de mémoire. Aussi, les concepteurs de processeurs et de systèmes d'exploitation ont adapté les tables des pages précédentes pour limiter la casse. Ils ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Ainsi, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Chaque sous-espace d'adressage a sa propre page des tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage qui contiennent au moins une donnée.
Les premiers bits de l'adresse vont servir à sélectionner quelle table des pages utiliser. Pour cela, il faut connaître l'adresse à laquelle est stockée chaque table des pages. Pour cela, on utilise une super-table des pages, qui stocke les adresses de début des tables des pages de chaque sous-espace. On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
===Le ''Translation Lookaside Buffer''===
Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''translation lookaside buffer''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Pour ceux qui ont déjà lu le chapitre suivant, voici comment : la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif.
====Les succès et défauts d'accès à la TLB====
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances. De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeur superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
====La hiérarchie des TLB====
Pour des raisons de performances, ce cache est parfois découpé en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rares que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes tient dans l'associativité de la TLB, un concept que nous verrons dans le chapitre suivant sur les caches, sans compter que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages. Ces deux problèmes interagissent entre eux et font que l'usage d'une TLB unique est possible, mais pas forcément optimal. L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===Le remplacement des pages mémoires===
La mémoire physique contient moins de pages que la mémoire virtuelle et il faut trouver un moyen pour que cela ne pose pas de problème. La solution consiste à utiliser des mémoires de stockage comme mémoire d'appoint : si on a besoin de plus de pages mémoires que la mémoire physique n'en contient, certaines pages mémoires vont être déplacées sur le disque dur pour faire de la place. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Lorsque l'on veut traduire l'adresse logique d'une page mémoire déplacée sur le disque dur, la MMU ne va pas pouvoir associer l'adresse logique à une adresse en mémoire RAM. Elle va alors lever une exception matérielle dont la routine rapatriera la page en mémoire RAM.
Charger une page en RAM ne pose aucun problème tant qu'il existe des pages inoccupée en RAM. Mais si toute la RAM est pleine, il faut déplacer une page mémoire en RAM sur le disque dur pour faire de la place. Tout cela est effectué par le système d'exploitation dont j'ai parlé plus haut. Notons que si on supprime une donnée dont on aura besoin dans le futur, il faudra recharger celle-ci, ce qui prend du temps. Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Les plus simples sont les suivants.
* Aléatoire : on choisit la page au hasard.
* FIFO : on supprime la donnée qui a été chargée dans la mémoire avant toutes les autres.
* LRU : on supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres.
* LFU : on vire la page qui est lue ou écrite le moins souvent comparée aux autres.
* etc.
Ces algorithmes ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter une interruption de gestion de page miss alors que la page contenant le code de l'interruption est placée sur le disque dur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le matériel réseau
| prevText=Le matériel réseau
| next=Les mémoires cache
| nextText=Les mémoires cache
}}
</noinclude>
45ugp4jkvl0a3qo6otm73pjsfnte79t
682070
682066
2022-07-20T20:40:37Z
Mewtow
31375
/* Le Translation Lookaside Buffer */
wikitext
text/x-wiki
Un programme peut être exécuté sur des ordinateurs ayant des capacités mémoires diverses et variées et dans des conditions très différentes. Et il faut faire en sorte qu'un programme fonctionne sur des ordinateur ayant peu de mémoire sans poser problème. Après tout, quand on conçoit un programme, on ne sait pas toujours quelle sera la quantité mémoire que notre ordinateur contiendra, et encore moins comment celle-ci sera partagée entre nos différentes programmes en cours d’exécution : s'affranchir de limitations sur la quantité de mémoire disponible est un plus vraiment appréciable. Ce besoin est appelé l''''abstraction matérielle de la mémoire'''.
Enfin, plusieurs programmes sont présents en même temps dans notre ordinateur, et doivent se partager la mémoire physique. Si un programme pouvait modifier les données d'un autre programme, on se retrouverait rapidement avec une situation non prévue par le programmeur. Cela a des conséquences qui vont de comiques à catastrophiques, et cela fini très souvent par un joli plantage. Il faut donc introduire des mécanismes de '''protection mémoire'''. Pour éviter cela, chaque programme a accès à des portions de la mémoire dans laquelle lui seul peut écrire ou lire. Le reste de la mémoire est inaccessible en lecture et en écriture, à part pour la mémoire partagée entre différents programmes. Toute tentative d'accès à une partie de la mémoire non autorisée déclenchera une exception matérielle (rappelez-vous le chapitre sur les interruptions) qui devra être traitée par une routine du système d'exploitation. Généralement, le programme fautif est sauvagement arrêté et un message d'erreur est affiché à l'écran.
Ces détails concernant l'organisation et la gestion de la mémoire sont assez compliqués à gérer, et faire en sorte que les programmes applicatifs n'aient pas à s'en soucier est un plus vraiment appréciable. Ce genre de problèmes a eu des solutions purement logicielles, mais quelques techniques règlent ces problèmes directement au niveau du matériel : on parle de '''mémoire virtuelle'''. Avec la mémoire virtuelle, tout se passe comme si notre programme était seul au monde et pouvait lire et écrire à toutes les adresses disponibles à partir de l'adresse zéro. Chaque programme a accès à autant d'adresses que ce que le processeur peut adresser : on se moque du fait que des adresses soient réservées aux périphériques, de la quantité de mémoire réellement installée sur l'ordinateur, ou de la mémoire prise par d'autres programmes en cours d’exécution. Pour éviter que ce surplus de fausse mémoire pose problème, on utilise une partie des mémoires de masse (disques durs) d'un ordinateur en remplacement de la mémoire physique manquante.
[[File:Memoire virtuelle.svg|centre|vignette|Mémoire virtuelle]]
[[File:MMU and IOMMU.svg|droite|vignette|Les périphériques peuvent eux aussi utiliser la mémoire virtuelle. Dans ce cas, ceux-ci intègrent une MMU dans leurs circuits. Ces MMU, aussi appelées IOMMU, sont séparées de la MMU du processeur.]]
Bien sûr, les adresses de la fausse mémoire vue par le programme sont des adresses fictives, qui devront être traduites en adresses mémoires réelles pour être utilisées. Les fausses adresses sont ce qu'on appelle des adresses logiques, alors que les adresses réelles sont appelées adresses physiques. Pour implémenter cette technique, il faut rajouter un circuit qui traduit les adresses logiques en adresses physiques : ce circuit est appelé la '''memory management unit'''. Il faut préciser qu'il existe différentes méthodes pour gérer ces adresses logiques et les transformer en adresses physiques : les principales sont la segmentation et la pagination. La suite du chapitre va détailler ces deux techniques.
==Le ''bank switching''==
[[File:Bankswitch memory map.svg|vignette|Exemple de Bank switching.]]
Le '''''bank switching''''', aussi appelé '''commutation de banque''', permet d'utiliser un bus d'adresse plus grand que les adresses du processeur. L'adresse réelle est plus grande que l'adresse utilisée par le processeur, les bits manquants étant fournit par un registre configurable du processeur : le '''registre de banque'''. L'espace mémoire du processeur est présent en plusieurs exemplaires, sélectionnés par la valeur du registre de banque. Chaque exemplaire de l'ensemble des adresses du processeur s'appelle une banque. On peut changer de banque en changeant le contenu de ce registre : le processeur dispose souvent d'instructions spécialisées qui en sont capables.
En répartissant les données utiles dans différentes banques, le processeur peut donc adresser beaucoup plus de mémoire. De plus, cette technique se marie assez bien avec les entrées-sorties mappées en mémoire. Par exemple, supposons que j'ai besoin d'adresser une mémoire ROM de 4 kibioctets, une RAM de 8 kibioctets, et divers périphériques. Le processeur a un bus d'adresse de 12 bits, ce qui limite à 4kibioctets. Dans ce cas, je peux réserver 4 banques : une pour la ROM, une pour les périphériques, et deux banques qui contiennent chacune la moitié de la RAM. La simplicité et l'efficacité de cette technique font qu'elle est beaucoup utilisée dans l'informatique embarquée.
{| class="wikitable flexible"
|[[File:Banque mémoire.png|Banque mémoire.]]
|[[File:Registre de banque.png|Registre de banque.]]
|}
==La segmentation==
[[File:Typical computer data memory arrangement.png|vignette|upright=0.5|Segments typiques des programmes sur OS modernes.]]
Sur les vieux systèmes d'exploitation, chaque programme recevait un bloc de RAM pour lui tout seul, une '''partition mémoire'''. La segmentation est une amélioration de cette technique, qui affecte une ou plusieurs partitions par programme. Cela permet à un seul processus d'avoir plusieurs espaces d'adressages, plusieurs mémoires virtuelles à lui tout seul. L'utilité est qu'un programme est rarement un tout unique, mais peut souvent être décomposé en plusieurs ensembles de données/instructions séparés, qui peuvent grossir ou se réduire suivant les circonstances. Par exemple, la pile a une taille qui varie beaucoup, tandis que le code du programme ne change pas. La segmentation permet de découper la mémoire virtuelle d'un processus en sous-partitions de taille variable qu'on appelle improprement des '''segments'''. Ainsi, un programme peut utiliser des segments différents pour la pile, le tas, le programme lui-même, et les variables globales. Chaque segment pourra grandir ou diminuer à sa guise, suivant les besoins, ce qui serait beaucoup plus difficile avec une seule partition mémoire.
===La relocation avec la segmentation===
Chaque segment peut être placé n'importe où en mémoire physique par l'OS, ce qui fait qu'il n'a pas d'adresse fixée en mémoire physique et peut être déplacé comme bon nous semble. C'est le système d'exploitation qui gère le placement des partitions/segments dans la mémoire physique. Ainsi, les adresses de destination des branchements et les adresses des données ne sont jamais les mêmes. Il faut donc trouver un moyen pour que les programmes fonctionnent avec des adresses qui changent d'une exécution à l'autre. Ce besoin est appelé la '''relocalisation'''. Pour résoudre ce problème, le compilateur considère que le programme commence à l'adresse zéro et laisse l'OS corriger les adresses du programme. Cette correction d'adresse demande simplement d'ajouter un décalage, égal à la première adresse de la partition mémoire. Cette correction peut se faire de deux manières : soit l'OS corrige chaque adresse lors du lancement du programme, soit le processeur s'en charge à chaque accès mémoire. Dans le second cas, cette première adresse est mémorisée dans un '''registre de base''', mis à jour automatiquement lors de chaque changement de programme.
[[File:Segmentation et relocation.png|centre|vignette|upright=2|Segmentation et relocation.]]
Sur les processeurs x86, la mémoire virtuelle est découpée en 2^16 segments de 2^32 bits, ce qui donne des adresses de 48 bits. Pour identifier un segment, seuls les 16 bits de poids fort de l'adresse 48 bits sont utiles : ils forment ce qu'on appelle un sélecteur de segment. Les 32 bits servent à identifier la position de la donnée dans un segment : ils sont appelés le décalage (offset). La relocalisation se fait de la même manière pour un segment que pour une partition mémoire. Pour cela, l'OS associe chaque segment à son adresse de base dans une table de correspondance, appelée la '''table des segments'''. Elle est unique pour chaque programme : la même adresse logique ne donnera pas la même adresse physique selon le programme.
[[File:Table des segments.png|centre|vignette|upright=2|Traduction d'adresse.]]
Dans le cas le plus simple, la table des segments est stockée en mémoire RAM, le processeur ne contenant qu'un registre de base mis à jour à chaque changement de segment.
[[File:Registre de base de segment.png|centre|vignette|upright=2|Registre de base de segment.]]
Une autre solution consiste à charger la table de correspondance complète dans un banc de registre dédié.
[[File:Table des segments dans un banc de registres.png|centre|vignette|upright=2|Table des segments dans un banc de registres.]]
===La protection mémoire avec la segmentation===
Un programme ne doit avoir accès qu'à la partition qui lui est dédiée et pas aux autres, sauf dans quelques rares exceptions. Toute tentative d'accès à une autre partition doit déclencher une exception matérielle, qui entraîne souvent l'apparition d'un message d'erreur. Tout cela est pris en charge par le système d'exploitation, par l'intermédiaire de mécanismes de '''protection mémoire''', que nous allons maintenant aborder.
Pour commencer, le processeur (ou l'OS) doivent détecter les accès hors-partition, à savoir un programme qui lit/écrit de la mémoire au-delà de la partition qui lui réservée. La solution est de mémoriser les limites du segment dans la page des segments et de vérifier que les accès mémoire ne se font pas au-delà de cette limite. L'adresse de fin de segment est mémorisée dans la table des segments et est récupérée lors de chaque accès. Le processeur vérifie si l'accès se fait dans les clou, en comparant l'adresse accédée avec l'adresse limite. Une autre solution consiste à mémoriser non pas l'adresse, mais l'offset maximal possible dans le segment en cours. Cela économise quelques bits par entrée dans la table des tables. Voici comment se passe la traduction d'une adresse avec la segmentation, en tenant compte de la vérification des accès hors-segment.
[[File:Vm7.svg|centre|vignette|upright=2|Traduction d'adresse avec vérification des accès hors-segment.]]
Vient ensuite la '''gestion des droits d'accès''' : chaque partition/segment se voit attribuer un certain nombre d'autorisations d'accès qui indiquent si l'on peut lire ou écrire dedans, si celui-ci contient un programme exécutable, etc. Par exemple, il est possible d'interdire d'exécuter quoique ce soit de localisé dans certains segments, ce qui fournit une protection contre certaines failles de sécurité ou certains virus. Lorsqu'on exécute une opération interdite, le processeur lève une exception matérielle, à charge du système d'exploitation de gérer la situation. L'OS ou la MMU mémorisent les autorisations pour chaque segment, qui sont rassemblées avec d'autres informations (registre de base et limite) dans un '''descripteur de segment'''. Pour se simplifier la tache, les concepteurs de processeurs et de systèmes d'exploitation ont décidé de regrouper ces descripteurs dans une portion de la mémoire, spécialement réservée pour l'occasion : la '''table des descripteurs de segment'''. Pour des raisons de performance, le processeur utilise un registre pour mémoriser le descripteur de segment du segment en cours d'utilisation. Quand on accède à un segment, son descripteur est chargé dans des registres du processeur, l'ancien est effacé.
[[File:SegmentDescriptor.svg|centre|vignette|upright=2|Schéma d'un descripteur de segment sur une architecture x86.]]
===Le partage de segments===
Il faut préciser qu'il est possible de partager des segments entre applications. Il suffit de configurer les tables de segment convenablement. Cela peut servir quand plusieurs instances d'une même applications sont lancés simultanément : le code n'ayant pas de raison de changer, celui-ci est partagé entre toutes les instance.
[[File:Vm9.png|centre|vignette|upright=2|Illustration du partage d'un segment entre deux applications.]]
Il est aussi possible de faire en sorte que deux segments se recouvrent l'un autre, à savoir que les deux segments partagent la même mémoire physique. Cela peut servir à partager de la mémoire entre plusieurs applications qui doivent communiquer entre elles.
[[File:Overlapping realmode segments.svg|centre|vignette|upright=1.5|Recouvrement de segments.]]
==La pagination==
De nos jours, la segmentation est obsolète et n'est plus utilisée : à la place, les OS et processeurs utilisent la pagination. Avec la pagination, la mémoire virtuelle et la mémoire physique sont découpées en blocs de taille fixe, appelés des '''pages mémoires'''. La différence avec les segments est que les segments sont de taille variable, alors que les pages sont de taille fixe. La taille des pages varie suivant le processeur et le système d'exploitation et tourne souvent autour de 4 kibioctets. Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages de 4 kibioctets sont les pages par défaut, les autres tailles de page sont appelées des ''pages larges''. Le contenu d'une page en mémoire fictive est rigoureusement le même que le contenu de la page correspondante en mémoire physique. Cependant, le nombre total de pages en mémoire virtuelle dépasse celui de la mémoire physique.
[[File:Principe de la pagination.png|centre|vignette|upright=2|Principe de la pagination.]]
Le surnombre est simplement placé sur le disque dur, dans un fichier appelé le '''fichier d'échange'''. Les pages virtuelles vont ainsi faire référence soit à une page en mémoire physique, soit à une page sur le disque dur. Tout accès à une page sur le disque dur va charger celle-ci dans la mémoire RAM, dans une page vide. Les pages font ainsi une sorte de va et vient entre le fichier d'échange et la RAM, suivant les besoins.
[[File:Lazy Swapper.jpg|centre|vignette|upright=2|Mémoire virtuelle paginée et fichier d'échange.]]
===La protection mémoire avec la pagination===
La protection mémoire est garantie avec des '''clés de protection''', un nombre unique à chaque programme. Le processeur mémorise, pour chaque page, la clé de protection du programme qui a réservé la page. A chaque accès mémoire, le processeur compare la clé de protection du programme en cours d’exécution, et celle de la page adressée. Si les deux clés sont différentes, alors un programme a effectué un accès hors des clous et il se fait sauvagement arrêter.
Comme avec la segmentation, chaque page a des droits d'accès précis, qui permettent d'autoriser ou interdire les accès en lecture, écriture, exécution, etc. La table des pages mémorise les autorisations pour chaque page. Ces autorisations/interdictions sont mémorisés sous la forme d'une suite de bits, chaque bit autorisant/interdisant une opération bien précise. Le format exact de la suite de bits a cependant changé dans le temps sur les processeurs x86 modernes. Par exemple, c'est lors du passage au 64 bits que l'interdiction d’exécution a été ajouté au jeu d’instruction. Avant, les CPU et OS ne pouvaient pas marquer une page mémoire comme non-exécutable. C'est seulement avec le passage au 64 bits qu'à été ajouté un bit pour interdire l'exécution de code depuis une page. Ce bit, nommé bit NX, est à 0 si la page n'est pas exécutable et à 1 sinon. De plus, diverses techniques intégrées aux processeur permettent de gérer celui-ci facilement. Notamment, le processeur vérifie à chaque chargement d'instruction si le bit NX de page lue est à 1. Sinon, il lève une exception matérielle et laisse la main à l'OS.
===La traduction d'adresse avec la pagination===
Une adresse (logique ou physique) se décompose donc en deux parties : un numéro de page qui identifie la page et un numéro permettant de localiser la donnée dans la page. Traduire l'adresse logique en adresse physique demande de remplacer le numéro de la page logique en un numéro de page physique. Pour faire cette traduction, il faut se souvenir des correspondances entre numéro de page et adresse de la page en mémoire fictive. Elles sont stockées dans une sorte de table, nommée la '''table des pages'''. Celle-ci contient aussi des bits pour savoir si une page est swappée sur le disque dur ou si elle est présente en RAM.
[[File:Paging.svg|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages simples====
Dans le cas le plus simple, il n'y a qu'une seule table des pages, qui est adressée par les numéros de page logique. Ainsi, pour chaque numéro (ou chaque adresse) de page logique, on stocke l'adresse de base de la page correspondante en mémoire physique. La table des pages est unique pour chaque programme, vu que les correspondances entre adresses physiques et logiques ne sont pas les mêmes. Cette table des pages est souvent stockée dans la mémoire RAM, à un endroit bien précis, connu du processeur. Accéder à la mémoire nécessite donc d’accéder d'abord à la table des pages en mémoire, puis de calculer l'adresse de notre donnée, et enfin d’accéder à la donnée voulue. Avec cette méthode, la table des pages a autant d'entrée qu'il y a de pages logiques en mémoire virtuelle.
[[File:Table des pages.png|centre|vignette|upright=2|Table des pages.]]
====Les tables des pages inversées====
Sur certains systèmes, la taille d'une table des pages serait beaucoup trop grande en utilisant les techniques vues au-dessus. Pour éviter cela, on a inventé les '''tables des pages inversées'''. L'idée derrière celles-ci est l'inverse de la méthode précédente. La méthode précédente stocke, pour chaque page logique, son numéro de page physique. Les tables des pages inversées font l'inverse : elles stockent, pour chaque numéro de page physique, la page logique qui correspond. Avec cette méthode table des pages contient ainsi autant d'entrées qu'il y a de pages physiques. Elle est donc plus petite qu'avant, vu que la mémoire physique est plus petite que la mémoire virtuelle.
Quand le processeur voudra convertir une adresse virtuelle en adresse physique, la MMU prendra le numéro de page de l'adresse virtuelle, et le recherchera dans la table des pages. Le numéro de l'entrée à laquelle se trouve ce morceau d'adresse virtuelle est le morceau de l'adresse physique. Pour faciliter le processus de recherche dans la page, les concepteurs de systèmes d'exploitation peuvent stocker celle-ci avec ce que l'on appelle une table de hachage.
[[File:Table des pages inversée.jpg|centre|vignette|upright=2|Table des pages inversée.]]
====Les tables des pages multiples par espace d'adressage====
Dans les deux cas précédents, il y a une table des page par processus/programme lancé sur l'ordinateur.
[[File:Vm5.png|centre|vignette|upright=2|Tables des pages de plusieurs processus.]]
Mais cela a un léger défaut : la table des pages de chaque processus bouffe beaucoup de mémoire. Aussi, les concepteurs de processeurs et de systèmes d'exploitation ont adapté les tables des pages précédentes pour limiter la casse. Ils ont remarqué que les adresses les plus hautes et/ou les plus basses sont les plus utilisées, alors que les adresses situées au milieu de l'espace d'adressage sont peu utilisées en raison du fonctionnement de la pile et du tas. Ainsi, les concepteurs d'OS ont décidé de découper l'espace d'adressage en plusieurs sous-espaces d'adressage de taille identique : certains localisés dans les adresses basses, d'autres au milieu, d'autres tout en haut, etc. Chaque sous-espace d'adressage a sa propre page des tables. Si un sous-espace d'adressage n'est pas utilisé, il n'y a pas besoin d'utiliser de la mémoire pour stocker la table des pages associée. On ne stocke que les tables des pages pour les espaces d'adressage qui contiennent au moins une donnée.
Les premiers bits de l'adresse vont servir à sélectionner quelle table des pages utiliser. Pour cela, il faut connaître l'adresse à laquelle est stockée chaque table des pages. Pour cela, on utilise une super-table des pages, qui stocke les adresses de début des tables des pages de chaque sous-espace. On peut aussi aller plus loin et découper la table des pages de manière hiérarchique, chaque sous-espace d'adressage étant lui aussi découpé en sous-espaces d'adressages.
[[File:Table des pages hiérarchique.png|centre|vignette|upright=2|Table des pages hiérarchique.]]
===Le remplacement des pages mémoires===
La mémoire physique contient moins de pages que la mémoire virtuelle et il faut trouver un moyen pour que cela ne pose pas de problème. La solution consiste à utiliser des mémoires de stockage comme mémoire d'appoint : si on a besoin de plus de pages mémoires que la mémoire physique n'en contient, certaines pages mémoires vont être déplacées sur le disque dur pour faire de la place. Les pages sur le disque dur doivent être chargées en RAM, avant d'être utilisables. Lorsque l'on veut traduire l'adresse logique d'une page mémoire déplacée sur le disque dur, la MMU ne va pas pouvoir associer l'adresse logique à une adresse en mémoire RAM. Elle va alors lever une exception matérielle dont la routine rapatriera la page en mémoire RAM.
Charger une page en RAM ne pose aucun problème tant qu'il existe des pages inoccupée en RAM. Mais si toute la RAM est pleine, il faut déplacer une page mémoire en RAM sur le disque dur pour faire de la place. Tout cela est effectué par le système d'exploitation dont j'ai parlé plus haut. Notons que si on supprime une donnée dont on aura besoin dans le futur, il faudra recharger celle-ci, ce qui prend du temps. Le choix de la page doit être fait avec le plus grand soin et il existe différents algorithmes qui permettent de décider quelle page supprimer de la RAM. Les plus simples sont les suivants.
* Aléatoire : on choisit la page au hasard.
* FIFO : on supprime la donnée qui a été chargée dans la mémoire avant toutes les autres.
* LRU : on supprime la donnée qui été lue ou écrite pour la dernière fois avant toutes les autres.
* LFU : on vire la page qui est lue ou écrite le moins souvent comparée aux autres.
* etc.
Ces algorithmes ont chacun deux variantes : une locale, et une globale. Avec la version locale, la page qui va être rapatriée sur le disque dur est une page réservée au programme qui est la cause du page miss. Avec la version globale, le système d'exploitation va choisir la page à virer parmi toutes les pages présentes en mémoire vive.
Sur la majorité des systèmes d'exploitation, il est possible d'interdire le déplacement de certaines pages sur le disque dur. Ces pages restent alors en mémoire RAM durant un temps plus ou moins long, parfois en permanence. Cette possibilité simplifie la vie des programmeurs qui conçoivent des systèmes d'exploitation : essayez d'exécuter une interruption de gestion de page miss alors que la page contenant le code de l'interruption est placée sur le disque dur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le matériel réseau
| prevText=Le matériel réseau
| next=Les mémoires cache
| nextText=Les mémoires cache
}}
</noinclude>
9ei31d9zxki46nnfcqs3tmt86lzv3ng
Fonctionnement d'un ordinateur/Le pipeline
0
65892
682074
678877
2022-07-20T20:42:26Z
Mewtow
31375
/* Le Loop Stream Detector */
wikitext
text/x-wiki
Dans le chapitre sur les performances d'un ordinateur, on a vu que le temps d’exécution d'une instruction dépend du CPI, le nombre moyen de cycles d'horloge par instruction et de la durée P d'un cycle d'horloge. En conséquence, rendre les instructions plus rapides demande de diminuer le CPI ou d'augmenter la fréquence. Monter en fréquence a commencé à monter ses limites avec le processeur Pentium 4, les contraintes de consommation énergétique se faisant de plus en plus lourdes. Les concepteurs de processeurs ont alors cherché à optimiser au mieux les instructions les plus utilisées et se sont plus ou moins heurtés à un mur. Il est devenu évident au fil du temps qu'il fallait réfléchir hors du cadre et trouver des solutions innovantes, ne ressemblant à rien de connu. Ils ont fini par trouver une solution assez incroyable : exécuter plusieurs instructions en même temps ! Pour cela, il a bien fallu trouver quelques solutions diverses et variées, dont le '''pipeline''' est la plus importante.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer en quoi il consiste, il va falloir faire un petit 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, du mode d'adressage, ou des manipulations qu'elle doit effectuer. 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 peut commencer à exécuter une nouvelle instruction sans attendre que la précédente soit terminée. Par exemple, on peut charger la prochaine instruction pendant que l'instruction en cours d’exécution en est à l'étape d'exécution. Après tout, ces deux étapes sont complètement indépendantes et utilisent des circuits séparés. Le principe du pipeline est simple : exécuter 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. Le nombre total d'étapes nécessaires pour effectuer une 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.
[[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. Pour l'exemple, nous allons utiliser une organisation relativement générale, où chaque instruction passe par les étapes suivantes :
* chargement : on charge notre instruction depuis la mémoire ;
* décodage : décodage de l'instruction ;
* exécution : on exécute l'instruction ;
* accès mémoire : accès à la mémoire RAM ;
* enregistrement : si besoin, le résultat de l’instruction est enregistré en mémoire.
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.]]
===L'isolation des étages du pipeline===
Concevoir un processeur avec un pipeline nécessite quelques modifications de l'architecture de notre 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. Il est donc impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. 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 sur un processeur doté d’un pipeline, on ne peut pas se le permettre, ce qui fait que chaque étape doit utiliser son propre additionneur. De même, l'étage de chargement peut entrer en conflit avec d'autres étages pour l'accès à la mémoire ou au cache, notamment pour les instructions d'accès mémoire. On peut résoudre ce conflit entre étage de chargement et étage d’accès mémoire en dupliquant le cache L1 en un cache d'instructions et un cache de données.
Et ce principe est général : il est important de séparer les circuits en charge de chaque étape. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''. La plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges, les contre-exemples étant assez rares. Les seuls exemples de pipelines sans registres sont appelés des ''pipelines à vague''. Mais la règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
===Les pipelines synchrones et asynchrones===
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]]
==La performance théorique 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. Ceux-ci ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Pour simplifier les calculs, nous allons négliger le temps de propagation de ces registres inter-étages. De plus, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Dans la réalité, les étages ne sont pas forcément équilibrés à la perfection, mais c'est une bonne approximation. Le pipeline étudié est donc un cas irréaliste de pipeline, idéal.
====La fréquence avec et sans pipeline====
La fréquence du processeur augmente avec un pipeline, comparé à la fréquence du même processeur mais sans pipeline. Mieux : elle augmente d'autant plus que le nombre d'étages est important. La raison est qu'un étage de pipeline a un temps de propagation plus petit qu'un processeur complet, ce qui permet d'en augmenter la fréquence. Et pour un pipeline à N étages, la fréquence est multipliée par à peu-près N. Pour comprendre pourquoi, démontrons-le avec des mathématiques très simples.
Sur un processeur sans pipeline, on suppose que l'instruction met un cycle d'horloge à l’exécuter (pour simplifier). La fréquence du processeur est donc l'inverse du temps d’exécution de l'instruction, soit :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
Sur un processeur avec pipeline, le temps de latence d'un étage est égal, par définition, à la durée d'un cycle d'horloge. La fréquence est l'inverse de cette durée, ce qui fait qu'elle vaut :
: <math>f_\text{pipeline} = {1 \over t_\text{étage}}</math>, avec <math>t_\text{étage}</math> le temps de propagation d'un étage du pipeline.
Le temps de propagation d'un étage de pipeline est naturellement plus faible que le temps de propagation d'un processeur complet. Pas étonnant donc que la fréquence avec un pipeline soit plus grande. Maintenant, supposons que les étages aient un temps de propagation équilibré, à savoir identique pour tous les étages. Avec cette hypothèse, le temps de propagation d'un étage est donc égal au temps d’exécution d'une instruction, divisée par le nombre d'étage N. On a donc :
: <math>t_\text{étage} = {t \over N}</math>, avec t le temps d’exécution d'une instruction (sans pipeline) et N le nombre d'étages.
En combinant les trois équations précédentes, on a :
: <math>f_\text{pipeline} = {N \over t} = N \times f</math>
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.
====Le temps d’exécution ne change pas avec un pipeline idéal====
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 ou sans pipeline (idéal, sans compter l'influence des registres entre les étages). Et si cela parait incompatible avec l'augmentation de la fréquence, ça ne l'est pas après une analyse minutieuse. En effet, la fréquence est multipliée par N, mais cela est compensée par le fait que l'instruction prend N étages pour s’exécuter, et donc N cycles d'horloge. L’augmentation de la fréquence est donc compensée par le fait qu'il faut plusieurs cycles d'horloge pour exécuter une instruction.
[[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.]]
Sur un pipeline, une instruction doit passer par <math>N</math> étages pour s’exécuter. Son temps d’exécution total est donc :
: <math>T = N \times t_\text{étage}</math>, avec <math>T</math> le temps d’exécution de l’instruction avec pipeline en secondes.
On utilise alors la formule <math>t_\text{étage} = {t \over N}</math>, vue plus haut :
: <math>T = N \times \frac{t}{N} = t</math>, avec t le temps d’exécution d'une instruction sans pipeline.
Pour résumer, le temps d’exécution d'une instruction ne change pas : l'augmentation de la fréquence compense l'augmentation du nombre d'étages.
Une autre manière de voir les choses est de partir de l'équation suivante, qui donne le temps d’exécution d'une instruction en fonction du nombre de cycle d'horloge qu'elle met pour s’exécuter (le CPI - ''cycles per instruction'').
: <math>t = \frac{CPI}{f}</math>
Avec un pipeline, la fréquence est multipliée par N, mais le CPI est aussi multiplié par N. Au final, les deux se compensent et le temps d’exécution reste identique.
: <math>T = \frac{CPI \times N}{f \times N} = \frac{CPI}{f} = t</math>
====Le nombre d'instructions par secondes====
Maintenant, nous allons voir quel est l'impact d'un pipeline sur le nombre d'instructions par secondes, abrévié IPS. Pour rappel, nous avions vu dans le chapitre sur la performance d'un ordinateur que cette unité correspond au nombre d'instructions que le processeur peut exécuter chaque seconde. Pour un processeur sans pipeline, celui-ci est égal à :
: <math>IPS = \frac{1}{t} = \frac{f}{CPI}</math>
Mais sur un processeur avec un pipeline, on peut charger une nouvelle instruction à chaque cycle d'horloge, ce qui donne une instruction dans chaque étage. Les équations précédentes doivent donc être multipliée par N, ce qui donne :
: <math>IPS = N \times \frac{1}{t} = N \times \frac{f}{CPI}</math>
On voit que la puissance de calcul a été multipliée par le nombre d'étages du pipeline. Par contre, le fait que plusieurs instructions puisse s’exécuter en même temps augmente les performances : si la latence reste la même, le débit du processeur augmente. Dit autrement, l'augmentation en performance provient de l'augmentation de l'IPC, le nombre d'instructions par cycle d'horloge. Plus un pipeline a d'étage, plus sa puissance de calcul théorique maximale est importante.
===La performance d'un pipeline non-idéal, 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. Refaisons donc les calculs précédents, en commençant par le temps de propagation d'un étage. Il suffit d'ajouter la latence du registre à l'équation précédente, ce qui donne :
: <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} = \frac{N}{T}</math>
Le débit, à savoir le nombre d'instructions exécutées par secondes, s'exprime à partir l'équation précédente. Pour un processeur sans pipeline, ce débit est simplement égal à <math>1 \over T</math>. Le pipeline peut exécuter <math>N</math> instructions en même temps, ce qui multiplie le débit par <math>N</math>, ce qui donne :
: <math>IPS = N \times \frac{f}{CPI} = \frac{N^2}{T} \frac{1}{CPI}</math>
===L'hétérogénéité des latences entre étages===
Sur les processeurs réels, les raisonnements précédents sont cependant invalides, vu que 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. Mais dans tous les cas, l'usage d'un pipeline permet au mieux de multiplier la fréquence par le nombre d'étages. Cela a poussé certains fabricants de processeurs à créer des processeurs ayant un nombre d'étages assez élevé pour les faire fonctionner à très haute fréquence. Par exemple, c'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur l'architecture Willamette et Northwood, et 31 étages pour ceux basés sur l'architecture Prescott et Cedar Mill.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
==Les pipelines de longueur fixe==
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 va suivre, nous allons utiliser une organisation relativement générale, où chaque instruction passe par les étapes suivantes :
* PC : mise à jour du ''program counter'' ;
* chargement : chargement de l'instruction depuis la mémoire ;
* décodage : décodage de l'instruction ;
* chargement d’opérandes : si besoin, les opérandes sont lus depuis la mémoire ou les registres ;
* exécution: exécution de l'instruction ;
* accès mémoire : accès à la mémoire RAM ;
* enregistrement : si besoin, le résultat de l’instruction est enregistré en mémoire.
L'étage de PC gère le ''program counter''. L'étage de chargement utilise l'interface de communication avec la mémoire. L'étage de décodage contient l'unité de décodage d'instruction. L'étage de lecture de registre contient le banc de registres. L'étage d'exécution contient l'ALU, l'étage d’accès mémoire a besoin de l'interface avec la mémoire et l'étage d’enregistrement a besoin des ports d'écriture du banc de registres. Naïvement, on peut être tenté de relier l'ensemble de cette façon.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
===Le chemin de données===
Toutes les instructions n'ont pas besoin d’accéder à la mémoire, tout comme certaines instructions n'ont pas à utiliser l'ALU ou lire des registres. Par exemple, certaines instructions n'ont pas besoin d’accéder à la RAM : on doit donc court-circuiter l'étage d’accès mémoire. De même, l'ALU aussi doit être court-circuitée pour les opérations qui ne font pas de calculs. En clair, certains étages sont « facultatifs » pour certaines instructions. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire, être rendus inactifs. Pour inactiver ces circuits, il suffit juste que ceux-ci puisse effectuer une instruction NOP, qui ne fait que recopier l'entrée sur la sortie. Pour les circuits qui ne s'inactivent pas facilement, on peut les court-circuiter en utilisant diverses techniques, la plus simple d'entre elle consistant à utiliser des multiplexeurs.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=1.5|Inactivation d'un étage de pipeline avec des multiplexeurs.]]
La lecture dans les registres peut être court-circuitée lors de l'utilisation de certains modes d'adressage. C'est notamment le cas lors de l'usage du mode d'adressage absolu. Pour le gérer, on envoie l'adresse fournie par l'unité de décodage sur l’entrée d'adresse de l'interface de communication avec la mémoire. Le principe est le même avec le mode d'adressage immédiat, sauf que l'on envoie une constante sur une entrée de l'ALU. On peut aller relativement loin comme cela.
[[File:Pipeline à 7 étages, avec mode d'adressage immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages, avec mode d’adressage immédiat et absolu gérés.]]
L'illustration ci-dessous montre ce que peut donner un pipeline MIPS à 5 étages qui gère les modes d'adressage les plus courants.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.0|MIPS Architecture (Pipelinée)]]
===Les signaux de commande===
Les signaux de commande qui servent à configurer le chemin de données sont générés par l'unité de décodage, dans le second étage du pipeline. Comment faire pour que ces signaux de commande traversent le pipeline ? Relier directement les sorties de l'unité de décodage aux circuits incriminés ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! La réponse consiste à faire passer ces signaux de commande d'un étage à l'autre en utilisant des registres.
[[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.]]
==Les pipelines de longueur variable==
Avec un pipeline de longueur fixe, toutes les instructions ont le même nombre d'étages, les étages inutiles étant court-circuités ou inactivés. Par exemple, si je prends une instruction qui effectue une addition entre deux registres, un des étages ne servira à rien : l'étage MEM. Normal, notre instruction n'accédera pas à la mémoire. Et on peut trouver beaucoup d'exemples de ce type. Par exemple, si je prends une instruction qui copie le contenu d'un registre dans un autre, ai-je besoin de l'étage d'Exec ou de MEM ? Non ! En clair : c'est un peu du gâchis. Si on regarde bien, on s’aperçoit que ce problème de nombre de micro-opérations variable vient du fait qu'il existe diverses classes d'instructions, qui ont chacune des besoins différents.
Sur un processeur non pipeliné, on peut éviter ces étapes inutiles en faisant varier le nombre de micro-opérations par instruction, certaines instructions pouvant prendre 7 cycles, d'autres 9, d'autres 25, etc. Il est possible de faire la même chose sur les processeurs pipelinés, dans une certaine limite, en utilisant plusieurs pipelines de longueurs différentes. Avec cette technique, le pipeline du processeur est décomposé en plusieurs parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions : la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, découpé en plusieurs unités adaptées à un certain type d'instructions, chacune formant ce qu'on appelle un '''aval''' (''back end''). Un aval est spécialisé dans une classe d'instructions : un pour les instructions arithmétiques et logiques, un autre pour les opérations d'accès mémoire, un autre pour les instructions d'échange de données entre registres, et ainsi de suite.
[[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=2.0|Pipeline avec un aval et un amont (back-end et front-end).]]
La longueur de l’aval peut varier suivant le type d'instructions.
[[File:Pipeline avec un nomrbe variable d'étages par instructions.png|centre|vignette|upright=2.5|Pipeline avec un nombre variable d'étages par instructions.]]
===Les pipelines variables des processeurs load-store===
Les processeurs ''load-store'' peuvent se contenter de deux avals : un pour les accès mémoire et un pour les calculs.
[[File:Pipeline à deux aval de type load-store.png|centre|vignette|upright=2.0|Pipeline à deux aval de type load-store.]]
On peut aussi utiliser une unité de calcul séparée pour les instructions de tests et branchements.
[[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=2.0|Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.]]
Il est possible de scinder les avals précédents en avals plus petits. Par exemple, l'aval pour les instructions arithmétiques peuvent être scindés en plusieurs avals séparés pour l'addition, la multiplication, la division, etc. Niveau accès mémoire, certains processeurs utilisent un aval pour les lectures et un autre pour les écritures.
Mais la séparation des avals n'est pas optimale pour les instructions qui se pipelinent mal, auxquelles il est difficile de leur créer un aval dédié. C'est notamment le cas de la division, dont les unités de calcul ne sont jamais totalement pipelinées, voire pas du tout. Il n'est ainsi pas rare d'avoir à gérer des unités de calcul dont chaque étage peut prendre deux à trois cycles pour s’exécuter : il faut attendre un certain nombre de cycles avant d'envoyer une nouvelle instruction dans l’aval.
===Les pipelines variables des processeurs non ''load-store''===
Les modes d'adressage complexes se marient mal avec un pipeline, notamment pour les modes d'adressages qui permettent plusieurs accès mémoire par instructions. Une méthode possible est de découper les instructions machines à mode d'adressage complexes en une suite de micro-instructions directement exécutables par le pipeline. Il va de soi que cette organisation complique pas mal le fonctionnement du séquenceur.
==Les optimisations du pipeline==
Sur certains pipeline, des optimisations permettent de se passer de certains étages du pipeline. Par se passer de certains étages, on veut dire que certaines instructions ne passeront pas par certaines étages, ce qui rend leur exécution plus rapide. Le cas d'étude le plus impressionnant est celui du '''''loop stream detector''''', présent sur les processeurs Intel depuis la micro-architecture Skylake.
===Le ''Loop Stream Detector''===
Le ''loop stream detector'' détecte certaines boucles et les conserve dans un cache situé tôt dans le pipeline. L'intérêt de ce cache est qu'il permet d'éviter que les instructions de la boucle soient chargées et décodées plusieurs fois de suite. Les instructions de la boucle sont lues dans le cache de boucle et sont injectées directement dans la suite du pipeline, dans le ''back-end''. Le fait de ne pas avoir à charger et décoder des instructions fait que le processeur consomme moins d'énergie, sans compter qu'on peut avoir un gain en performance du fait du raccourcissement du pipeline. Le gain est d'autant plus important si les instructions ont un encodage complexe, ou si les instructions sont à longueur variable. Globalement, plus le chargement et le décodage sont complexes, plus le ''loop stream detector'' fait des merveilles.
Le cache en question est situé soit après l'étage de chargement (''fetch''), soit après l'étape de décodage. Il était présent entre l'étage de chargement et de décodage sur les processeurs Core 2, mais est passé après l'étage de décodage sur les processeurs ultérieurs. S'il est situé après l'étage de chargement, il contient les instructions de la boucle, avant qu'elles soient décodées. Mais s'il est placé après l'étage de décodage, il contient les micro-instructions qui constituent la boucle. Évidemment, la taille du ''loop stream detector'' n'est pas infinie, comme pour tout cache. Le cache ne peut contenir qu'un nombre limité d'instruction, ce qui fait qu'il ne fonctionne que pour des boucles de petite taille. D'après la documentation d'Intel, le cache ne peut contenir que 18 instructions pour le processeur Core, 28 micro-instructions sur les processeurs Core i7. Il y a aussi des contraintes quant au nombres de branchements à l'intérieur de la boucle et le nombre d'accès mémoire.
Dans les deux cas, l'étage de chargement peut être éteint, les branchements non-alignés ne posent plus de problèmes de performance, et il est même possible qu'une partie du cache d'instruction puisse être éteint temporairement. Mais les deux solutions ont des avantages et inconvénients. Si le cache est placé après l'étage de chargement, les instructions doivent quand même être décodées, ce qui réduit les gains en consommation énergétique et en performance. À l'inverse, mettre le cache après l'étage de décodage fait que les instructions ne doivent plus être décodées et renommées fait que le ''front-end'' du pipeline peut être complètement éteint, ce qui entraîne un gain important en performance et en consommation d'énergie. Le gain est d'autant plus important si le programme utilise des instruction micro-codées ou des instructions complexes à décoder comme les instructions de longueur variable. D'un autre coté, l'encodage des instructions est plus compact que la ou les micro-instruction équivalente, ce qui fait que le cache est plus petit s'il est placé avant l'étage de décodage, ce qui entraîne un gain de circuits et de place, et réduit un petit peu la consommation énergétique du cache.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=Interruptions et pipeline
| nextText=Interruptions et pipeline
}}
</noinclude>
qo3noic9a7oobs6xuhxy3neqwefpv9g
Fonctionnement d'un ordinateur/Dépendances de contrôle
0
65955
682047
682013
2022-07-20T15:41:32Z
Mewtow
31375
/* La mitigation des interférences liées aux branchements biaisés */
wikitext
text/x-wiki
Les branchements ont des effets similaires aux exceptions et interruptions. Lorsqu'on charge un branchement dans le pipeline, l'adresse à laquelle brancher sera connue après quelques cycles et des instructions seront chargées durant ce temps. Pour éviter tout problème, on doit faire en sorte que ces instructions ne modifient pas les registres du processeur ou qu'elles ne lancent pas d'écritures en mémoire. La solution la plus simple consiste à inclure des instructions qui ne font rien à la suite du branchement : c'est ce qu'on appelle un '''délai de branchement'''. Le processeur chargera ces instructions, jusqu’à ce que l'adresse à laquelle brancher soit connue. Cette technique a les mêmes inconvénients que ceux vus dans le chapitre précédent. Pour éviter cela, on peut réutiliser les techniques vues dans les chapitres précédents, avec quelques améliorations.
Pour éviter les délais de branchement, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et l’exécuter avant que celle-ci ne soit connue. Cela demande de résoudre trois problèmes :
* reconnaître les branchements ;
* savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ;
* dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''.
Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit.
[[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]]
==La correction des erreurs de prédiction==
Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Pour corriger les erreurs de prédictions, il faut d'abord les détecter. Pour cela, on rajoute un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée.
[[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]]
Une fois la mauvaise prédiction détectée, il faut corriger le ''program counter'' immédiatement. En effet, si il y a eu erreur de prédiction, le ''program counter'' n'a pas été mis à jour correctement, et il faut faire reprendre le processeur au bon endroit. Pour implémenter cette correction du ''program counter'', il suffit d'utiliser un circuit qui restaure le ''program counter'' s'il y a eu erreur de prédiction. La gestion des mauvaises prédictions dépend fortement du processeur. Certains processeurs peuvent reprendre l’exécution du programme immédiatement après avoir détecté la mauvaise prédiction (ils sont capables de supprimer les instructions qui n'auraient pas dû être exécutées), tandis que d'autres attendent que les instructions chargées par erreur terminent avant de corriger le ''program counter''.
Second point : il faut que le pipeline soit vidé des instructions chargées par erreur. Tant que ces instructions ne sont pas sorties du pipeline, on doit attendre sans charger d'instructions dans le pipeline. Le temps d'attente nécessaire pour vider le pipeline est égal à son nombre d'étages. En effet, la dernière instruction à être chargée dans le pipeline le sera durant l'étape à laquelle on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. De plus, il faut remettre le pipeline dans l'état qu'il avait avant le chargement du branchement. Tout se passe lors de la dernière étape d'enregistrement des résultats en mémoire ou dans les registres. Et pour cela, on réutilise les techniques vues dans le chapitre précédent pour la gestion des exceptions et interruptions.
En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Mais pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient du être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction.
[[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]]
==La reconnaissance des branchements==
Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc du trouver une solution.
La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache.
Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il appelé le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. Si il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement.
==La prédiction de l'adresse de destination==
La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile).
La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible.
: Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit.
===La prédiction de l'adresse de destination pour les branchements directs===
Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Ce cache est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Ce cache est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile.
Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c'est à dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB.
Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l'''aliasing''.
Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares.
[[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]]
D'autres processeurs se passent de BTB. A la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode).
===La prédiction de l'adresse de destination pour les branchements indirects et implicites===
Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Pour cela, ils utilisent un tampon de destination de branchement amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations qui lui permettent de déduire plus ou moins efficacement quelle adresse de destination est la bonne. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits.
Certains processeurs peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'un sous-programme a fini de s’exécuter, cette adresse de retour étant stockée sur la pile, ou dans des registres spéciaux du processeur dans certains cas particuliers. Ils possèdent un circuit spécialisé capable de prédire cette adresse : la '''prédiction de retour de fonction''' (''return function predictor''). Lorsqu'une fonction est appelée, ce circuit stocke l'adresse de retour d'une fonction dans des registres internes au processeur organisés en pile. Avec cette organisation des registres en forme de pile, on sait d'avance que l'adresse de retour du sous-programme en cours d'exécution est au sommet de cette pile.
==La prédiction de branchement==
La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c'est à dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris.
===Les contraintes d’implémentation de la prédiction de branchement===
La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c'est à dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils vont paradoxalement poser quelques problèmes avec certaines techniques, comme nous le verrons plus bas.
Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent.
===La classification des techniques de prédiction de branchement===
Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc.
Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas.
Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. A l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux.
===La prédiction statique de branchement===
Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc.
Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre.
Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique.
Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit :
* les branchements inconditionnels sont toujours pris ;
* les branchements descendants sont toujours pris ;
* les branchements ascendants ne sont jamais pris.
===La prédiction de branchement avec des compteurs à saturation===
Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique.
Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement.
[[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]]
Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c'est à dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction deux deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB.
===La prédiction de branchement à deux niveaux avec un historique global===
La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux.
Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique.
[[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]]
L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique, et éventuellement avec d'autres informations annexes. Le choix du compteur à saturation à utiliser dépend uniquement du registre d'historique global, aucune autre information n'est utilisée.
[[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]]
Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail.
====La mitigation des interférences par usage de l'adresse de branchement====
La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en terme de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Voyons dans le détail cette dernière. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique pour choisir le compteur à saturation adéquat.
Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique.
Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors.
[[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]]
D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées.
Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente.
[[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]]
Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''.
[[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]]
Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation.
====La mitigation des interférences par la structuration en caches des PHTs====
Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. Si il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique.
La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé.
Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels.
====La mitigation des interférences liées aux branchements biaisés====
D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribué au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchement biaisés ou quasi-biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi-biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit.
Une première solution est d'économiser sur l'usage de la PHT, en la réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits sans PHT ou usage de l'historique, en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisé est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire es prédictions.
La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris.
[[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]]
L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchement très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant.
[[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]]
===Les unités de prédiction à deux niveau locales===
Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisit dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes.
[[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]]
On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécution du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale.
[[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]]
Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC.
[[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]]
Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaise pour les autres types de branchements.
Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats.
Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux.
===Les unités de prédiction à deux niveau mixtes (globale/locale)===
Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction.
Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement.
[[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]]
Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées.
[[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]]
===La prédiction de branchement avec des perceptrons===
Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons.
La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables.
====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements====
L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même.
Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé à été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1.
Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifié. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci :
: <math>Z = w_0 + \sum_i (h_i \times w_i)</math>
[[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]]
: Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids.
Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante :
: <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris.
====L'implémentation matérielle des perceptrons et ses optimisations====
Les calculs réalisés par un perceptrons sont simples, juste des multiplication et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle.
[[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]]
Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide.
Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en terme de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables.
Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit).
====Les avantages et inconvénients des perceptrons pour la prédiction de branchements====
L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge.
Un autre défaut est les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est définit par l'ensemble des points qui respecte l'équation suivante :
: <math>w_0 + \sum_i (h_i \times w_i) = 0</math>
Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcul avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédiction correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements.
Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure.
Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours.
===La prédiction basée sur un l'utilisation de plusieurs unités de branchement distinctes===
Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune ayant des registres d'historique de taille différente. Une unité aura un historique de 2 bits, une autre un historique de 3 bits, une autre de 4 bits, etc. Les branchements qui ont un motif répétitifs sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement se basent sur ce principe. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions.
====La méta-prédiction====
La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment.
====La fusion de prédictions====
Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée car elle demande un nombre d'unités important (au moins 4).
L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisis ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité.
[[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]]
====La priorisation des unités de prédiction====
Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction.
[[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]]
===La prédiction des branchements des boucles===
Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois.
Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité.
==Les processeurs à chemins multiples==
Pour limiter la casse en cas de mauvaise prédiction, certains processeurs chargent des instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris) dans une mémoire cache, qui permet de récupérer rapidement les instructions correctes en cas de mauvaise prédiction de branchement. Certains processeurs vont plus loin et exécutent les deux possibilités séparément : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement mais on peut poursuivre un peu plus loin. On peut remarquer que cette technique utilise la prédiction de direction branchement , pour savoir quelle la destination des branchements. Les techniques utilisées pour annuler les instructions du chemin non-pris sont les mêmes que celles utilisées pour la prédiction de branchement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
===Implémentation===
Implémenter cette technique demande de dupliquer les circuits de chargement pour charger des instructions provenant de plusieurs « chemins d’exécution ». Pour identifier les instructions à annuler, chaque instruction se voit attribuer un numéro par l'unité de chargement, qui indique à quel chemin elle appartient. Chaque chemin peut être dans quatre états, états qui doivent être mémorisés avec le ''program counter'' qui correspond :
* invalide (pas de second chemin) ;
* en cours de chargement ;
* mis en pause suite à un défaut de cache ;
* chemin stoppé car il s'est séparé en deux autres chemins, à cause d'un branchement.
Pour l'accès au cache d'instructions, l'implémentation varie suivant le type de cache utilisé. Certains processeurs utilisent un cache à un seul port avec un circuit d'arbitrage. La politique d'arbitrage peut être plus ou moins complexe. Dans le cas le plus simple, chaque chemin charge ses instructions au tour par tour. Des politiques plus complexes sont possibles : on peut notamment privilégier le chemin qui a la plus forte probabilité d'être pris, pas exemple. D'autres processeurs préfèrent utiliser un cache multi-port, capable d’alimenter plusieurs chemins à la fois.
===L'exécution stricte disjointe===
Avec cette technique, on est limité par le nombre d'unités de calculs, de registres, etc. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
Pour limiter la casse, on peut coupler exécution stricte et prédiction de branchement.
[[File:Exécution stricte 02.png|centre|vignette|upright=2|Exécution stricte.]]
L'idée est de ne pas exécuter les chemins qui ont une probabilité trop faible d'être pris. Si un chemin a une probabilité inférieure à un certain seuil, on ne charge pas ses instructions. On parle d’'''exécution stricte disjointe''' (''disjoint eager execution''). C'est la technique qui est censée donner les meilleurs résultats d'un point de vue théorique. À chaque fois qu'un nouveau branchement est rencontré, le processeur refait les calculs de probabilité. Cela signifie que d'anciens branchements qui n'avaient pas été exécutés car ils avaient une probabilité trop faible peuvent être exécuté si des branchements avec des probabilités encore plus faibles sont rencontrés en cours de route.
[[File:Exécution stricte 03.png|centre|vignette|upright=1|Exécution stricte disjointe.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Interruptions et pipeline
| prevText=Interruptions et pipeline
| next=Dépendances de données
| nextText=Dépendances de données
}}
</noinclude>
c0xj4uhduowi17xttihim8ednhn2cf3
682048
682047
2022-07-20T15:44:39Z
Mewtow
31375
/* La prédiction de branchement */
wikitext
text/x-wiki
Les branchements ont des effets similaires aux exceptions et interruptions. Lorsqu'on charge un branchement dans le pipeline, l'adresse à laquelle brancher sera connue après quelques cycles et des instructions seront chargées durant ce temps. Pour éviter tout problème, on doit faire en sorte que ces instructions ne modifient pas les registres du processeur ou qu'elles ne lancent pas d'écritures en mémoire. La solution la plus simple consiste à inclure des instructions qui ne font rien à la suite du branchement : c'est ce qu'on appelle un '''délai de branchement'''. Le processeur chargera ces instructions, jusqu’à ce que l'adresse à laquelle brancher soit connue. Cette technique a les mêmes inconvénients que ceux vus dans le chapitre précédent. Pour éviter cela, on peut réutiliser les techniques vues dans les chapitres précédents, avec quelques améliorations.
Pour éviter les délais de branchement, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et l’exécuter avant que celle-ci ne soit connue. Cela demande de résoudre trois problèmes :
* reconnaître les branchements ;
* savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ;
* dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''.
Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit.
[[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]]
==La correction des erreurs de prédiction==
Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Pour corriger les erreurs de prédictions, il faut d'abord les détecter. Pour cela, on rajoute un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée.
[[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]]
Une fois la mauvaise prédiction détectée, il faut corriger le ''program counter'' immédiatement. En effet, si il y a eu erreur de prédiction, le ''program counter'' n'a pas été mis à jour correctement, et il faut faire reprendre le processeur au bon endroit. Pour implémenter cette correction du ''program counter'', il suffit d'utiliser un circuit qui restaure le ''program counter'' s'il y a eu erreur de prédiction. La gestion des mauvaises prédictions dépend fortement du processeur. Certains processeurs peuvent reprendre l’exécution du programme immédiatement après avoir détecté la mauvaise prédiction (ils sont capables de supprimer les instructions qui n'auraient pas dû être exécutées), tandis que d'autres attendent que les instructions chargées par erreur terminent avant de corriger le ''program counter''.
Second point : il faut que le pipeline soit vidé des instructions chargées par erreur. Tant que ces instructions ne sont pas sorties du pipeline, on doit attendre sans charger d'instructions dans le pipeline. Le temps d'attente nécessaire pour vider le pipeline est égal à son nombre d'étages. En effet, la dernière instruction à être chargée dans le pipeline le sera durant l'étape à laquelle on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. De plus, il faut remettre le pipeline dans l'état qu'il avait avant le chargement du branchement. Tout se passe lors de la dernière étape d'enregistrement des résultats en mémoire ou dans les registres. Et pour cela, on réutilise les techniques vues dans le chapitre précédent pour la gestion des exceptions et interruptions.
En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Mais pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient du être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction.
[[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]]
==La reconnaissance des branchements==
Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc du trouver une solution.
La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache.
Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il appelé le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. Si il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement.
==La prédiction de l'adresse de destination==
La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile).
La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible.
: Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit.
===La prédiction de l'adresse de destination pour les branchements directs===
Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Ce cache est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Ce cache est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile.
Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c'est à dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB.
Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l'''aliasing''.
Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares.
[[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]]
D'autres processeurs se passent de BTB. A la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode).
===La prédiction de l'adresse de destination pour les branchements indirects et implicites===
Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Pour cela, ils utilisent un tampon de destination de branchement amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations qui lui permettent de déduire plus ou moins efficacement quelle adresse de destination est la bonne. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits.
Certains processeurs peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'un sous-programme a fini de s’exécuter, cette adresse de retour étant stockée sur la pile, ou dans des registres spéciaux du processeur dans certains cas particuliers. Ils possèdent un circuit spécialisé capable de prédire cette adresse : la '''prédiction de retour de fonction''' (''return function predictor''). Lorsqu'une fonction est appelée, ce circuit stocke l'adresse de retour d'une fonction dans des registres internes au processeur organisés en pile. Avec cette organisation des registres en forme de pile, on sait d'avance que l'adresse de retour du sous-programme en cours d'exécution est au sommet de cette pile.
==La prédiction de branchement==
La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c'est à dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris.
===Les contraintes d’implémentation de la prédiction de branchement===
La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c'est à dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils vont paradoxalement poser quelques problèmes avec certaines techniques, comme nous le verrons plus bas.
Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent.
===La classification des techniques de prédiction de branchement===
Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc.
Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas.
Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. A l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux.
===La prédiction statique de branchement===
Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc.
Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre.
Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique.
Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit :
* les branchements inconditionnels sont toujours pris ;
* les branchements descendants sont toujours pris ;
* les branchements ascendants ne sont jamais pris.
===La prédiction de branchement avec des compteurs à saturation===
Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique.
Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement.
[[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]]
Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c'est à dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction deux deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB.
===La prédiction de branchement à deux niveaux avec un historique global===
La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux.
Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique.
[[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]]
L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique, et éventuellement avec d'autres informations annexes. Le choix du compteur à saturation à utiliser dépend uniquement du registre d'historique global, aucune autre information n'est utilisée.
[[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]]
Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail.
====La mitigation des interférences par usage de l'adresse de branchement====
La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en terme de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Voyons dans le détail cette dernière. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique pour choisir le compteur à saturation adéquat.
Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique.
Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors.
[[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]]
D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées.
Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente.
[[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]]
Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''.
[[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]]
Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation.
====La mitigation des interférences par la structuration en caches des PHTs====
Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. Si il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique.
La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé.
Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels.
====La mitigation des interférences liées aux branchements biaisés====
D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribué au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchement biaisés ou quasi-biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi-biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit.
La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris.
[[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]]
L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchement très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant.
[[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]]
===Les unités de prédiction à deux niveau locales===
Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisit dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes.
[[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]]
On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécution du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale.
[[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]]
Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC.
[[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]]
Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaise pour les autres types de branchements.
Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats.
Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux.
===Les unités de prédiction à deux niveau mixtes (globale/locale)===
Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction.
Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement.
[[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]]
Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées.
[[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]]
===La prédiction de branchement avec des perceptrons===
Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons.
La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables.
====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements====
L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même.
Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé à été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1.
Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifié. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci :
: <math>Z = w_0 + \sum_i (h_i \times w_i)</math>
[[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]]
: Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids.
Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante :
: <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris.
====L'implémentation matérielle des perceptrons et ses optimisations====
Les calculs réalisés par un perceptrons sont simples, juste des multiplication et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle.
[[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]]
Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide.
Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en terme de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables.
Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit).
====Les avantages et inconvénients des perceptrons pour la prédiction de branchements====
L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge.
Un autre défaut est les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est définit par l'ensemble des points qui respecte l'équation suivante :
: <math>w_0 + \sum_i (h_i \times w_i) = 0</math>
Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcul avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédiction correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements.
Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure.
Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours.
===La prédiction des branchements des boucles===
Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois.
Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité.
==La prédiction basée sur un l'utilisation de plusieurs unités de branchement distinctes==
Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune ayant des registres d'historique de taille différente. Une unité aura un historique de 2 bits, une autre un historique de 3 bits, une autre de 4 bits, etc. Les branchements qui ont un motif répétitifs sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement se basent sur ce principe. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions.
===La méta-prédiction===
La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment.
===La fusion de prédictions===
Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée car elle demande un nombre d'unités important (au moins 4).
L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisis ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité.
[[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]]
===La priorisation des unités de prédiction===
Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction.
[[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]]
==Les optimisations de la prédiction de branchements==
La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées.
===Le filtrage de branchements===
Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisé est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire es prédictions.
==Les processeurs à chemins multiples==
Pour limiter la casse en cas de mauvaise prédiction, certains processeurs chargent des instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris) dans une mémoire cache, qui permet de récupérer rapidement les instructions correctes en cas de mauvaise prédiction de branchement. Certains processeurs vont plus loin et exécutent les deux possibilités séparément : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement mais on peut poursuivre un peu plus loin. On peut remarquer que cette technique utilise la prédiction de direction branchement , pour savoir quelle la destination des branchements. Les techniques utilisées pour annuler les instructions du chemin non-pris sont les mêmes que celles utilisées pour la prédiction de branchement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
===Implémentation===
Implémenter cette technique demande de dupliquer les circuits de chargement pour charger des instructions provenant de plusieurs « chemins d’exécution ». Pour identifier les instructions à annuler, chaque instruction se voit attribuer un numéro par l'unité de chargement, qui indique à quel chemin elle appartient. Chaque chemin peut être dans quatre états, états qui doivent être mémorisés avec le ''program counter'' qui correspond :
* invalide (pas de second chemin) ;
* en cours de chargement ;
* mis en pause suite à un défaut de cache ;
* chemin stoppé car il s'est séparé en deux autres chemins, à cause d'un branchement.
Pour l'accès au cache d'instructions, l'implémentation varie suivant le type de cache utilisé. Certains processeurs utilisent un cache à un seul port avec un circuit d'arbitrage. La politique d'arbitrage peut être plus ou moins complexe. Dans le cas le plus simple, chaque chemin charge ses instructions au tour par tour. Des politiques plus complexes sont possibles : on peut notamment privilégier le chemin qui a la plus forte probabilité d'être pris, pas exemple. D'autres processeurs préfèrent utiliser un cache multi-port, capable d’alimenter plusieurs chemins à la fois.
===L'exécution stricte disjointe===
Avec cette technique, on est limité par le nombre d'unités de calculs, de registres, etc. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
Pour limiter la casse, on peut coupler exécution stricte et prédiction de branchement.
[[File:Exécution stricte 02.png|centre|vignette|upright=2|Exécution stricte.]]
L'idée est de ne pas exécuter les chemins qui ont une probabilité trop faible d'être pris. Si un chemin a une probabilité inférieure à un certain seuil, on ne charge pas ses instructions. On parle d’'''exécution stricte disjointe''' (''disjoint eager execution''). C'est la technique qui est censée donner les meilleurs résultats d'un point de vue théorique. À chaque fois qu'un nouveau branchement est rencontré, le processeur refait les calculs de probabilité. Cela signifie que d'anciens branchements qui n'avaient pas été exécutés car ils avaient une probabilité trop faible peuvent être exécuté si des branchements avec des probabilités encore plus faibles sont rencontrés en cours de route.
[[File:Exécution stricte 03.png|centre|vignette|upright=1|Exécution stricte disjointe.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Interruptions et pipeline
| prevText=Interruptions et pipeline
| next=Dépendances de données
| nextText=Dépendances de données
}}
</noinclude>
2mytpgngvo0zxtq54q3e51g1uoasjup
682049
682048
2022-07-20T15:45:54Z
Mewtow
31375
/* Les optimisations de la prédiction de branchements */
wikitext
text/x-wiki
Les branchements ont des effets similaires aux exceptions et interruptions. Lorsqu'on charge un branchement dans le pipeline, l'adresse à laquelle brancher sera connue après quelques cycles et des instructions seront chargées durant ce temps. Pour éviter tout problème, on doit faire en sorte que ces instructions ne modifient pas les registres du processeur ou qu'elles ne lancent pas d'écritures en mémoire. La solution la plus simple consiste à inclure des instructions qui ne font rien à la suite du branchement : c'est ce qu'on appelle un '''délai de branchement'''. Le processeur chargera ces instructions, jusqu’à ce que l'adresse à laquelle brancher soit connue. Cette technique a les mêmes inconvénients que ceux vus dans le chapitre précédent. Pour éviter cela, on peut réutiliser les techniques vues dans les chapitres précédents, avec quelques améliorations.
Pour éviter les délais de branchement, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et l’exécuter avant que celle-ci ne soit connue. Cela demande de résoudre trois problèmes :
* reconnaître les branchements ;
* savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ;
* dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''.
Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit.
[[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]]
==La correction des erreurs de prédiction==
Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Pour corriger les erreurs de prédictions, il faut d'abord les détecter. Pour cela, on rajoute un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée.
[[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]]
Une fois la mauvaise prédiction détectée, il faut corriger le ''program counter'' immédiatement. En effet, si il y a eu erreur de prédiction, le ''program counter'' n'a pas été mis à jour correctement, et il faut faire reprendre le processeur au bon endroit. Pour implémenter cette correction du ''program counter'', il suffit d'utiliser un circuit qui restaure le ''program counter'' s'il y a eu erreur de prédiction. La gestion des mauvaises prédictions dépend fortement du processeur. Certains processeurs peuvent reprendre l’exécution du programme immédiatement après avoir détecté la mauvaise prédiction (ils sont capables de supprimer les instructions qui n'auraient pas dû être exécutées), tandis que d'autres attendent que les instructions chargées par erreur terminent avant de corriger le ''program counter''.
Second point : il faut que le pipeline soit vidé des instructions chargées par erreur. Tant que ces instructions ne sont pas sorties du pipeline, on doit attendre sans charger d'instructions dans le pipeline. Le temps d'attente nécessaire pour vider le pipeline est égal à son nombre d'étages. En effet, la dernière instruction à être chargée dans le pipeline le sera durant l'étape à laquelle on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. De plus, il faut remettre le pipeline dans l'état qu'il avait avant le chargement du branchement. Tout se passe lors de la dernière étape d'enregistrement des résultats en mémoire ou dans les registres. Et pour cela, on réutilise les techniques vues dans le chapitre précédent pour la gestion des exceptions et interruptions.
En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Mais pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient du être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction.
[[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]]
==La reconnaissance des branchements==
Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc du trouver une solution.
La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache.
Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il appelé le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. Si il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement.
==La prédiction de l'adresse de destination==
La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile).
La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible.
: Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit.
===La prédiction de l'adresse de destination pour les branchements directs===
Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Ce cache est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Ce cache est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile.
Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c'est à dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB.
Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l'''aliasing''.
Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares.
[[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]]
D'autres processeurs se passent de BTB. A la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode).
===La prédiction de l'adresse de destination pour les branchements indirects et implicites===
Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Pour cela, ils utilisent un tampon de destination de branchement amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations qui lui permettent de déduire plus ou moins efficacement quelle adresse de destination est la bonne. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits.
Certains processeurs peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'un sous-programme a fini de s’exécuter, cette adresse de retour étant stockée sur la pile, ou dans des registres spéciaux du processeur dans certains cas particuliers. Ils possèdent un circuit spécialisé capable de prédire cette adresse : la '''prédiction de retour de fonction''' (''return function predictor''). Lorsqu'une fonction est appelée, ce circuit stocke l'adresse de retour d'une fonction dans des registres internes au processeur organisés en pile. Avec cette organisation des registres en forme de pile, on sait d'avance que l'adresse de retour du sous-programme en cours d'exécution est au sommet de cette pile.
==La prédiction de branchement==
La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c'est à dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris.
===Les contraintes d’implémentation de la prédiction de branchement===
La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c'est à dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils vont paradoxalement poser quelques problèmes avec certaines techniques, comme nous le verrons plus bas.
Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent.
===La classification des techniques de prédiction de branchement===
Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc.
Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas.
Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. A l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux.
===La prédiction statique de branchement===
Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc.
Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre.
Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique.
Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit :
* les branchements inconditionnels sont toujours pris ;
* les branchements descendants sont toujours pris ;
* les branchements ascendants ne sont jamais pris.
===La prédiction de branchement avec des compteurs à saturation===
Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique.
Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement.
[[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]]
Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c'est à dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction deux deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB.
===La prédiction de branchement à deux niveaux avec un historique global===
La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux.
Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique.
[[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]]
L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique, et éventuellement avec d'autres informations annexes. Le choix du compteur à saturation à utiliser dépend uniquement du registre d'historique global, aucune autre information n'est utilisée.
[[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]]
Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail.
====La mitigation des interférences par usage de l'adresse de branchement====
La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en terme de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Voyons dans le détail cette dernière. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique pour choisir le compteur à saturation adéquat.
Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique.
Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors.
[[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]]
D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées.
Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente.
[[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]]
Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''.
[[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]]
Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation.
====La mitigation des interférences par la structuration en caches des PHTs====
Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. Si il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique.
La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé.
Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels.
====La mitigation des interférences liées aux branchements biaisés====
D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribué au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchement biaisés ou quasi-biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi-biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit.
La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris.
[[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]]
L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchement très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant.
[[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]]
===Les unités de prédiction à deux niveau locales===
Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisit dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes.
[[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]]
On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécution du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale.
[[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]]
Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC.
[[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]]
Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaise pour les autres types de branchements.
Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats.
Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux.
===Les unités de prédiction à deux niveau mixtes (globale/locale)===
Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction.
Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement.
[[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]]
Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées.
[[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]]
===La prédiction de branchement avec des perceptrons===
Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons.
La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables.
====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements====
L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même.
Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé à été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1.
Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifié. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci :
: <math>Z = w_0 + \sum_i (h_i \times w_i)</math>
[[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]]
: Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids.
Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante :
: <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris.
====L'implémentation matérielle des perceptrons et ses optimisations====
Les calculs réalisés par un perceptrons sont simples, juste des multiplication et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle.
[[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]]
Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide.
Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en terme de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables.
Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit).
====Les avantages et inconvénients des perceptrons pour la prédiction de branchements====
L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge.
Un autre défaut est les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est définit par l'ensemble des points qui respecte l'équation suivante :
: <math>w_0 + \sum_i (h_i \times w_i) = 0</math>
Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcul avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédiction correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements.
Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure.
Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours.
===La prédiction des branchements des boucles===
Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois.
Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité.
==La prédiction basée sur un l'utilisation de plusieurs unités de branchement distinctes==
Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune ayant des registres d'historique de taille différente. Une unité aura un historique de 2 bits, une autre un historique de 3 bits, une autre de 4 bits, etc. Les branchements qui ont un motif répétitifs sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement se basent sur ce principe. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions.
===La méta-prédiction===
La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment.
===La fusion de prédictions===
Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée car elle demande un nombre d'unités important (au moins 4).
L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisis ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité.
[[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]]
===La priorisation des unités de prédiction===
Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction.
[[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]]
==Les optimisations de la prédiction de branchements==
La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quelque soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général.
===Le filtrage de branchements===
Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisé est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire es prédictions.
==Les processeurs à chemins multiples==
Pour limiter la casse en cas de mauvaise prédiction, certains processeurs chargent des instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris) dans une mémoire cache, qui permet de récupérer rapidement les instructions correctes en cas de mauvaise prédiction de branchement. Certains processeurs vont plus loin et exécutent les deux possibilités séparément : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement mais on peut poursuivre un peu plus loin. On peut remarquer que cette technique utilise la prédiction de direction branchement , pour savoir quelle la destination des branchements. Les techniques utilisées pour annuler les instructions du chemin non-pris sont les mêmes que celles utilisées pour la prédiction de branchement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
===Implémentation===
Implémenter cette technique demande de dupliquer les circuits de chargement pour charger des instructions provenant de plusieurs « chemins d’exécution ». Pour identifier les instructions à annuler, chaque instruction se voit attribuer un numéro par l'unité de chargement, qui indique à quel chemin elle appartient. Chaque chemin peut être dans quatre états, états qui doivent être mémorisés avec le ''program counter'' qui correspond :
* invalide (pas de second chemin) ;
* en cours de chargement ;
* mis en pause suite à un défaut de cache ;
* chemin stoppé car il s'est séparé en deux autres chemins, à cause d'un branchement.
Pour l'accès au cache d'instructions, l'implémentation varie suivant le type de cache utilisé. Certains processeurs utilisent un cache à un seul port avec un circuit d'arbitrage. La politique d'arbitrage peut être plus ou moins complexe. Dans le cas le plus simple, chaque chemin charge ses instructions au tour par tour. Des politiques plus complexes sont possibles : on peut notamment privilégier le chemin qui a la plus forte probabilité d'être pris, pas exemple. D'autres processeurs préfèrent utiliser un cache multi-port, capable d’alimenter plusieurs chemins à la fois.
===L'exécution stricte disjointe===
Avec cette technique, on est limité par le nombre d'unités de calculs, de registres, etc. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
Pour limiter la casse, on peut coupler exécution stricte et prédiction de branchement.
[[File:Exécution stricte 02.png|centre|vignette|upright=2|Exécution stricte.]]
L'idée est de ne pas exécuter les chemins qui ont une probabilité trop faible d'être pris. Si un chemin a une probabilité inférieure à un certain seuil, on ne charge pas ses instructions. On parle d’'''exécution stricte disjointe''' (''disjoint eager execution''). C'est la technique qui est censée donner les meilleurs résultats d'un point de vue théorique. À chaque fois qu'un nouveau branchement est rencontré, le processeur refait les calculs de probabilité. Cela signifie que d'anciens branchements qui n'avaient pas été exécutés car ils avaient une probabilité trop faible peuvent être exécuté si des branchements avec des probabilités encore plus faibles sont rencontrés en cours de route.
[[File:Exécution stricte 03.png|centre|vignette|upright=1|Exécution stricte disjointe.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Interruptions et pipeline
| prevText=Interruptions et pipeline
| next=Dépendances de données
| nextText=Dépendances de données
}}
</noinclude>
cpece5p5thlkwqe35eum2k6ra8kihbg
682050
682049
2022-07-20T15:50:11Z
Mewtow
31375
/* Les optimisations de la prédiction de branchements */
wikitext
text/x-wiki
Les branchements ont des effets similaires aux exceptions et interruptions. Lorsqu'on charge un branchement dans le pipeline, l'adresse à laquelle brancher sera connue après quelques cycles et des instructions seront chargées durant ce temps. Pour éviter tout problème, on doit faire en sorte que ces instructions ne modifient pas les registres du processeur ou qu'elles ne lancent pas d'écritures en mémoire. La solution la plus simple consiste à inclure des instructions qui ne font rien à la suite du branchement : c'est ce qu'on appelle un '''délai de branchement'''. Le processeur chargera ces instructions, jusqu’à ce que l'adresse à laquelle brancher soit connue. Cette technique a les mêmes inconvénients que ceux vus dans le chapitre précédent. Pour éviter cela, on peut réutiliser les techniques vues dans les chapitres précédents, avec quelques améliorations.
Pour éviter les délais de branchement, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et l’exécuter avant que celle-ci ne soit connue. Cela demande de résoudre trois problèmes :
* reconnaître les branchements ;
* savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ;
* dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''.
Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit.
[[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]]
==La correction des erreurs de prédiction==
Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Pour corriger les erreurs de prédictions, il faut d'abord les détecter. Pour cela, on rajoute un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée.
[[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]]
Une fois la mauvaise prédiction détectée, il faut corriger le ''program counter'' immédiatement. En effet, si il y a eu erreur de prédiction, le ''program counter'' n'a pas été mis à jour correctement, et il faut faire reprendre le processeur au bon endroit. Pour implémenter cette correction du ''program counter'', il suffit d'utiliser un circuit qui restaure le ''program counter'' s'il y a eu erreur de prédiction. La gestion des mauvaises prédictions dépend fortement du processeur. Certains processeurs peuvent reprendre l’exécution du programme immédiatement après avoir détecté la mauvaise prédiction (ils sont capables de supprimer les instructions qui n'auraient pas dû être exécutées), tandis que d'autres attendent que les instructions chargées par erreur terminent avant de corriger le ''program counter''.
Second point : il faut que le pipeline soit vidé des instructions chargées par erreur. Tant que ces instructions ne sont pas sorties du pipeline, on doit attendre sans charger d'instructions dans le pipeline. Le temps d'attente nécessaire pour vider le pipeline est égal à son nombre d'étages. En effet, la dernière instruction à être chargée dans le pipeline le sera durant l'étape à laquelle on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. De plus, il faut remettre le pipeline dans l'état qu'il avait avant le chargement du branchement. Tout se passe lors de la dernière étape d'enregistrement des résultats en mémoire ou dans les registres. Et pour cela, on réutilise les techniques vues dans le chapitre précédent pour la gestion des exceptions et interruptions.
En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Mais pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient du être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction.
[[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]]
==La reconnaissance des branchements==
Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc du trouver une solution.
La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache.
Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il appelé le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. Si il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement.
==La prédiction de l'adresse de destination==
La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile).
La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible.
: Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit.
===La prédiction de l'adresse de destination pour les branchements directs===
Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Ce cache est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Ce cache est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile.
Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c'est à dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB.
Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l'''aliasing''.
Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares.
[[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]]
D'autres processeurs se passent de BTB. A la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode).
===La prédiction de l'adresse de destination pour les branchements indirects et implicites===
Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Pour cela, ils utilisent un tampon de destination de branchement amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations qui lui permettent de déduire plus ou moins efficacement quelle adresse de destination est la bonne. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits.
Certains processeurs peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'un sous-programme a fini de s’exécuter, cette adresse de retour étant stockée sur la pile, ou dans des registres spéciaux du processeur dans certains cas particuliers. Ils possèdent un circuit spécialisé capable de prédire cette adresse : la '''prédiction de retour de fonction''' (''return function predictor''). Lorsqu'une fonction est appelée, ce circuit stocke l'adresse de retour d'une fonction dans des registres internes au processeur organisés en pile. Avec cette organisation des registres en forme de pile, on sait d'avance que l'adresse de retour du sous-programme en cours d'exécution est au sommet de cette pile.
==La prédiction de branchement==
La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c'est à dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris.
===Les contraintes d’implémentation de la prédiction de branchement===
La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c'est à dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils vont paradoxalement poser quelques problèmes avec certaines techniques, comme nous le verrons plus bas.
Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent.
===La classification des techniques de prédiction de branchement===
Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc.
Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas.
Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. A l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux.
===La prédiction statique de branchement===
Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc.
Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre.
Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique.
Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit :
* les branchements inconditionnels sont toujours pris ;
* les branchements descendants sont toujours pris ;
* les branchements ascendants ne sont jamais pris.
===La prédiction de branchement avec des compteurs à saturation===
Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique.
Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement.
[[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]]
Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c'est à dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction deux deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB.
===La prédiction de branchement à deux niveaux avec un historique global===
La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux.
Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique.
[[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]]
L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique, et éventuellement avec d'autres informations annexes. Le choix du compteur à saturation à utiliser dépend uniquement du registre d'historique global, aucune autre information n'est utilisée.
[[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]]
Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail.
====La mitigation des interférences par usage de l'adresse de branchement====
La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en terme de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Voyons dans le détail cette dernière. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique pour choisir le compteur à saturation adéquat.
Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique.
Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors.
[[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]]
D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées.
Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente.
[[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]]
Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''.
[[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]]
Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation.
====La mitigation des interférences par la structuration en caches des PHTs====
Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. Si il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique.
La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé.
Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels.
====La mitigation des interférences liées aux branchements biaisés====
D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribué au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchement biaisés ou quasi-biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi-biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit.
La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris.
[[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]]
L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchement très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant.
[[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]]
===Les unités de prédiction à deux niveau locales===
Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisit dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes.
[[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]]
On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécution du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale.
[[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]]
Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC.
[[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]]
Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaise pour les autres types de branchements.
Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats.
Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux.
===Les unités de prédiction à deux niveau mixtes (globale/locale)===
Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction.
Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement.
[[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]]
Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées.
[[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]]
===La prédiction de branchement avec des perceptrons===
Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons.
La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables.
====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements====
L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même.
Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé à été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1.
Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifié. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci :
: <math>Z = w_0 + \sum_i (h_i \times w_i)</math>
[[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]]
: Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids.
Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante :
: <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris.
====L'implémentation matérielle des perceptrons et ses optimisations====
Les calculs réalisés par un perceptrons sont simples, juste des multiplication et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle.
[[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]]
Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide.
Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en terme de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables.
Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit).
====Les avantages et inconvénients des perceptrons pour la prédiction de branchements====
L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge.
Un autre défaut est les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est définit par l'ensemble des points qui respecte l'équation suivante :
: <math>w_0 + \sum_i (h_i \times w_i) = 0</math>
Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcul avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédiction correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements.
Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure.
Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours.
===La prédiction des branchements des boucles===
Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois.
Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité.
==La prédiction basée sur un l'utilisation de plusieurs unités de branchement distinctes==
Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune ayant des registres d'historique de taille différente. Une unité aura un historique de 2 bits, une autre un historique de 3 bits, une autre de 4 bits, etc. Les branchements qui ont un motif répétitifs sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement se basent sur ce principe. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions.
===La méta-prédiction===
La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment.
===La fusion de prédictions===
Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée car elle demande un nombre d'unités important (au moins 4).
L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisis ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité.
[[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]]
===La priorisation des unités de prédiction===
Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction.
[[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]]
==Les optimisations de la prédiction de branchements==
La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quelque soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements de manière globale, sans modifier les historiques, etc.
===Le filtrage de branchements===
Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisé est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire es prédictions.
==Les processeurs à chemins multiples==
Pour limiter la casse en cas de mauvaise prédiction, certains processeurs chargent des instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris) dans une mémoire cache, qui permet de récupérer rapidement les instructions correctes en cas de mauvaise prédiction de branchement. Certains processeurs vont plus loin et exécutent les deux possibilités séparément : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement mais on peut poursuivre un peu plus loin. On peut remarquer que cette technique utilise la prédiction de direction branchement , pour savoir quelle la destination des branchements. Les techniques utilisées pour annuler les instructions du chemin non-pris sont les mêmes que celles utilisées pour la prédiction de branchement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
===Implémentation===
Implémenter cette technique demande de dupliquer les circuits de chargement pour charger des instructions provenant de plusieurs « chemins d’exécution ». Pour identifier les instructions à annuler, chaque instruction se voit attribuer un numéro par l'unité de chargement, qui indique à quel chemin elle appartient. Chaque chemin peut être dans quatre états, états qui doivent être mémorisés avec le ''program counter'' qui correspond :
* invalide (pas de second chemin) ;
* en cours de chargement ;
* mis en pause suite à un défaut de cache ;
* chemin stoppé car il s'est séparé en deux autres chemins, à cause d'un branchement.
Pour l'accès au cache d'instructions, l'implémentation varie suivant le type de cache utilisé. Certains processeurs utilisent un cache à un seul port avec un circuit d'arbitrage. La politique d'arbitrage peut être plus ou moins complexe. Dans le cas le plus simple, chaque chemin charge ses instructions au tour par tour. Des politiques plus complexes sont possibles : on peut notamment privilégier le chemin qui a la plus forte probabilité d'être pris, pas exemple. D'autres processeurs préfèrent utiliser un cache multi-port, capable d’alimenter plusieurs chemins à la fois.
===L'exécution stricte disjointe===
Avec cette technique, on est limité par le nombre d'unités de calculs, de registres, etc. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
Pour limiter la casse, on peut coupler exécution stricte et prédiction de branchement.
[[File:Exécution stricte 02.png|centre|vignette|upright=2|Exécution stricte.]]
L'idée est de ne pas exécuter les chemins qui ont une probabilité trop faible d'être pris. Si un chemin a une probabilité inférieure à un certain seuil, on ne charge pas ses instructions. On parle d’'''exécution stricte disjointe''' (''disjoint eager execution''). C'est la technique qui est censée donner les meilleurs résultats d'un point de vue théorique. À chaque fois qu'un nouveau branchement est rencontré, le processeur refait les calculs de probabilité. Cela signifie que d'anciens branchements qui n'avaient pas été exécutés car ils avaient une probabilité trop faible peuvent être exécuté si des branchements avec des probabilités encore plus faibles sont rencontrés en cours de route.
[[File:Exécution stricte 03.png|centre|vignette|upright=1|Exécution stricte disjointe.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Interruptions et pipeline
| prevText=Interruptions et pipeline
| next=Dépendances de données
| nextText=Dépendances de données
}}
</noinclude>
mr5xody7ljjaznuf4pjr9nan9mmc9p2
682051
682050
2022-07-20T15:52:36Z
Mewtow
31375
/* Les optimisations de la prédiction de branchements */
wikitext
text/x-wiki
Les branchements ont des effets similaires aux exceptions et interruptions. Lorsqu'on charge un branchement dans le pipeline, l'adresse à laquelle brancher sera connue après quelques cycles et des instructions seront chargées durant ce temps. Pour éviter tout problème, on doit faire en sorte que ces instructions ne modifient pas les registres du processeur ou qu'elles ne lancent pas d'écritures en mémoire. La solution la plus simple consiste à inclure des instructions qui ne font rien à la suite du branchement : c'est ce qu'on appelle un '''délai de branchement'''. Le processeur chargera ces instructions, jusqu’à ce que l'adresse à laquelle brancher soit connue. Cette technique a les mêmes inconvénients que ceux vus dans le chapitre précédent. Pour éviter cela, on peut réutiliser les techniques vues dans les chapitres précédents, avec quelques améliorations.
Pour éviter les délais de branchement, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et l’exécuter avant que celle-ci ne soit connue. Cela demande de résoudre trois problèmes :
* reconnaître les branchements ;
* savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ;
* dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''.
Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit.
[[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]]
==La correction des erreurs de prédiction==
Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Pour corriger les erreurs de prédictions, il faut d'abord les détecter. Pour cela, on rajoute un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée.
[[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]]
Une fois la mauvaise prédiction détectée, il faut corriger le ''program counter'' immédiatement. En effet, si il y a eu erreur de prédiction, le ''program counter'' n'a pas été mis à jour correctement, et il faut faire reprendre le processeur au bon endroit. Pour implémenter cette correction du ''program counter'', il suffit d'utiliser un circuit qui restaure le ''program counter'' s'il y a eu erreur de prédiction. La gestion des mauvaises prédictions dépend fortement du processeur. Certains processeurs peuvent reprendre l’exécution du programme immédiatement après avoir détecté la mauvaise prédiction (ils sont capables de supprimer les instructions qui n'auraient pas dû être exécutées), tandis que d'autres attendent que les instructions chargées par erreur terminent avant de corriger le ''program counter''.
Second point : il faut que le pipeline soit vidé des instructions chargées par erreur. Tant que ces instructions ne sont pas sorties du pipeline, on doit attendre sans charger d'instructions dans le pipeline. Le temps d'attente nécessaire pour vider le pipeline est égal à son nombre d'étages. En effet, la dernière instruction à être chargée dans le pipeline le sera durant l'étape à laquelle on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. De plus, il faut remettre le pipeline dans l'état qu'il avait avant le chargement du branchement. Tout se passe lors de la dernière étape d'enregistrement des résultats en mémoire ou dans les registres. Et pour cela, on réutilise les techniques vues dans le chapitre précédent pour la gestion des exceptions et interruptions.
En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Mais pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient du être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction.
[[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]]
==La reconnaissance des branchements==
Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc du trouver une solution.
La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache.
Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il appelé le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. Si il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement.
==La prédiction de l'adresse de destination==
La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile).
La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible.
: Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit.
===La prédiction de l'adresse de destination pour les branchements directs===
Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Ce cache est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Ce cache est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile.
Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c'est à dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB.
Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l'''aliasing''.
Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares.
[[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]]
D'autres processeurs se passent de BTB. A la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode).
===La prédiction de l'adresse de destination pour les branchements indirects et implicites===
Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Pour cela, ils utilisent un tampon de destination de branchement amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations qui lui permettent de déduire plus ou moins efficacement quelle adresse de destination est la bonne. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits.
Certains processeurs peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'un sous-programme a fini de s’exécuter, cette adresse de retour étant stockée sur la pile, ou dans des registres spéciaux du processeur dans certains cas particuliers. Ils possèdent un circuit spécialisé capable de prédire cette adresse : la '''prédiction de retour de fonction''' (''return function predictor''). Lorsqu'une fonction est appelée, ce circuit stocke l'adresse de retour d'une fonction dans des registres internes au processeur organisés en pile. Avec cette organisation des registres en forme de pile, on sait d'avance que l'adresse de retour du sous-programme en cours d'exécution est au sommet de cette pile.
==La prédiction de branchement==
La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c'est à dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris.
===Les contraintes d’implémentation de la prédiction de branchement===
La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c'est à dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils vont paradoxalement poser quelques problèmes avec certaines techniques, comme nous le verrons plus bas.
Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent.
===La classification des techniques de prédiction de branchement===
Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc.
Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas.
Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. A l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux.
===La prédiction statique de branchement===
Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc.
Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre.
Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique.
Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit :
* les branchements inconditionnels sont toujours pris ;
* les branchements descendants sont toujours pris ;
* les branchements ascendants ne sont jamais pris.
===La prédiction de branchement avec des compteurs à saturation===
Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique.
Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement.
[[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]]
Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c'est à dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction deux deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB.
===La prédiction de branchement à deux niveaux avec un historique global===
La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux.
Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique.
[[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]]
L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique, et éventuellement avec d'autres informations annexes. Le choix du compteur à saturation à utiliser dépend uniquement du registre d'historique global, aucune autre information n'est utilisée.
[[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]]
Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail.
====La mitigation des interférences par usage de l'adresse de branchement====
La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en terme de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Voyons dans le détail cette dernière. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique pour choisir le compteur à saturation adéquat.
Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique.
Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors.
[[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]]
D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées.
Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente.
[[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]]
Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''.
[[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]]
Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation.
====La mitigation des interférences par la structuration en caches des PHTs====
Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. Si il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique.
La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé.
Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels.
====La mitigation des interférences liées aux branchements biaisés====
D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribué au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchement biaisés ou quasi-biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi-biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit.
La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris.
[[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]]
L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchement très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant.
[[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]]
===Les unités de prédiction à deux niveau locales===
Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisit dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes.
[[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]]
On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécution du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale.
[[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]]
Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC.
[[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]]
Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaise pour les autres types de branchements.
Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats.
Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux.
===Les unités de prédiction à deux niveau mixtes (globale/locale)===
Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction.
Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement.
[[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]]
Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées.
[[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]]
===La prédiction de branchement avec des perceptrons===
Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons.
La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables.
====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements====
L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même.
Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé à été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1.
Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifié. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci :
: <math>Z = w_0 + \sum_i (h_i \times w_i)</math>
[[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]]
: Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids.
Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante :
: <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris.
====L'implémentation matérielle des perceptrons et ses optimisations====
Les calculs réalisés par un perceptrons sont simples, juste des multiplication et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle.
[[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]]
Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide.
Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en terme de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables.
Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit).
====Les avantages et inconvénients des perceptrons pour la prédiction de branchements====
L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge.
Un autre défaut est les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est définit par l'ensemble des points qui respecte l'équation suivante :
: <math>w_0 + \sum_i (h_i \times w_i) = 0</math>
Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcul avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédiction correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements.
Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure.
Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours.
===La prédiction des branchements des boucles===
Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois.
Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité.
==La prédiction basée sur un l'utilisation de plusieurs unités de branchement distinctes==
Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune ayant des registres d'historique de taille différente. Une unité aura un historique de 2 bits, une autre un historique de 3 bits, une autre de 4 bits, etc. Les branchements qui ont un motif répétitifs sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement se basent sur ce principe. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions.
===La méta-prédiction===
La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment.
===La fusion de prédictions===
Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée car elle demande un nombre d'unités important (au moins 4).
L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisis ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité.
[[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]]
===La priorisation des unités de prédiction===
Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction.
[[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]]
==Les optimisations de la prédiction de branchements==
La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quelque soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc.
===Le filtrage de branchements===
Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisé est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire es prédictions.
==Les processeurs à chemins multiples==
Pour limiter la casse en cas de mauvaise prédiction, certains processeurs chargent des instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris) dans une mémoire cache, qui permet de récupérer rapidement les instructions correctes en cas de mauvaise prédiction de branchement. Certains processeurs vont plus loin et exécutent les deux possibilités séparément : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement mais on peut poursuivre un peu plus loin. On peut remarquer que cette technique utilise la prédiction de direction branchement , pour savoir quelle la destination des branchements. Les techniques utilisées pour annuler les instructions du chemin non-pris sont les mêmes que celles utilisées pour la prédiction de branchement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
===Implémentation===
Implémenter cette technique demande de dupliquer les circuits de chargement pour charger des instructions provenant de plusieurs « chemins d’exécution ». Pour identifier les instructions à annuler, chaque instruction se voit attribuer un numéro par l'unité de chargement, qui indique à quel chemin elle appartient. Chaque chemin peut être dans quatre états, états qui doivent être mémorisés avec le ''program counter'' qui correspond :
* invalide (pas de second chemin) ;
* en cours de chargement ;
* mis en pause suite à un défaut de cache ;
* chemin stoppé car il s'est séparé en deux autres chemins, à cause d'un branchement.
Pour l'accès au cache d'instructions, l'implémentation varie suivant le type de cache utilisé. Certains processeurs utilisent un cache à un seul port avec un circuit d'arbitrage. La politique d'arbitrage peut être plus ou moins complexe. Dans le cas le plus simple, chaque chemin charge ses instructions au tour par tour. Des politiques plus complexes sont possibles : on peut notamment privilégier le chemin qui a la plus forte probabilité d'être pris, pas exemple. D'autres processeurs préfèrent utiliser un cache multi-port, capable d’alimenter plusieurs chemins à la fois.
===L'exécution stricte disjointe===
Avec cette technique, on est limité par le nombre d'unités de calculs, de registres, etc. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
Pour limiter la casse, on peut coupler exécution stricte et prédiction de branchement.
[[File:Exécution stricte 02.png|centre|vignette|upright=2|Exécution stricte.]]
L'idée est de ne pas exécuter les chemins qui ont une probabilité trop faible d'être pris. Si un chemin a une probabilité inférieure à un certain seuil, on ne charge pas ses instructions. On parle d’'''exécution stricte disjointe''' (''disjoint eager execution''). C'est la technique qui est censée donner les meilleurs résultats d'un point de vue théorique. À chaque fois qu'un nouveau branchement est rencontré, le processeur refait les calculs de probabilité. Cela signifie que d'anciens branchements qui n'avaient pas été exécutés car ils avaient une probabilité trop faible peuvent être exécuté si des branchements avec des probabilités encore plus faibles sont rencontrés en cours de route.
[[File:Exécution stricte 03.png|centre|vignette|upright=1|Exécution stricte disjointe.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Interruptions et pipeline
| prevText=Interruptions et pipeline
| next=Dépendances de données
| nextText=Dépendances de données
}}
</noinclude>
sc0nfbgjsywrgpvia9u0y4owhrcio13
682056
682051
2022-07-20T18:34:55Z
Mewtow
31375
/* Le filtrage de branchements */
wikitext
text/x-wiki
Les branchements ont des effets similaires aux exceptions et interruptions. Lorsqu'on charge un branchement dans le pipeline, l'adresse à laquelle brancher sera connue après quelques cycles et des instructions seront chargées durant ce temps. Pour éviter tout problème, on doit faire en sorte que ces instructions ne modifient pas les registres du processeur ou qu'elles ne lancent pas d'écritures en mémoire. La solution la plus simple consiste à inclure des instructions qui ne font rien à la suite du branchement : c'est ce qu'on appelle un '''délai de branchement'''. Le processeur chargera ces instructions, jusqu’à ce que l'adresse à laquelle brancher soit connue. Cette technique a les mêmes inconvénients que ceux vus dans le chapitre précédent. Pour éviter cela, on peut réutiliser les techniques vues dans les chapitres précédents, avec quelques améliorations.
Pour éviter les délais de branchement, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et l’exécuter avant que celle-ci ne soit connue. Cela demande de résoudre trois problèmes :
* reconnaître les branchements ;
* savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ;
* dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''.
Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit.
[[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]]
==La correction des erreurs de prédiction==
Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Pour corriger les erreurs de prédictions, il faut d'abord les détecter. Pour cela, on rajoute un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée.
[[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]]
Une fois la mauvaise prédiction détectée, il faut corriger le ''program counter'' immédiatement. En effet, si il y a eu erreur de prédiction, le ''program counter'' n'a pas été mis à jour correctement, et il faut faire reprendre le processeur au bon endroit. Pour implémenter cette correction du ''program counter'', il suffit d'utiliser un circuit qui restaure le ''program counter'' s'il y a eu erreur de prédiction. La gestion des mauvaises prédictions dépend fortement du processeur. Certains processeurs peuvent reprendre l’exécution du programme immédiatement après avoir détecté la mauvaise prédiction (ils sont capables de supprimer les instructions qui n'auraient pas dû être exécutées), tandis que d'autres attendent que les instructions chargées par erreur terminent avant de corriger le ''program counter''.
Second point : il faut que le pipeline soit vidé des instructions chargées par erreur. Tant que ces instructions ne sont pas sorties du pipeline, on doit attendre sans charger d'instructions dans le pipeline. Le temps d'attente nécessaire pour vider le pipeline est égal à son nombre d'étages. En effet, la dernière instruction à être chargée dans le pipeline le sera durant l'étape à laquelle on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. De plus, il faut remettre le pipeline dans l'état qu'il avait avant le chargement du branchement. Tout se passe lors de la dernière étape d'enregistrement des résultats en mémoire ou dans les registres. Et pour cela, on réutilise les techniques vues dans le chapitre précédent pour la gestion des exceptions et interruptions.
En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Mais pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient du être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction.
[[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]]
==La reconnaissance des branchements==
Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc du trouver une solution.
La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache.
Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il appelé le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. Si il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement.
==La prédiction de l'adresse de destination==
La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile).
La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible.
: Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit.
===La prédiction de l'adresse de destination pour les branchements directs===
Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Ce cache est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Ce cache est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile.
Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c'est à dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB.
Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l'''aliasing''.
Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares.
[[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]]
D'autres processeurs se passent de BTB. A la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode).
===La prédiction de l'adresse de destination pour les branchements indirects et implicites===
Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Pour cela, ils utilisent un tampon de destination de branchement amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations qui lui permettent de déduire plus ou moins efficacement quelle adresse de destination est la bonne. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits.
Certains processeurs peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'un sous-programme a fini de s’exécuter, cette adresse de retour étant stockée sur la pile, ou dans des registres spéciaux du processeur dans certains cas particuliers. Ils possèdent un circuit spécialisé capable de prédire cette adresse : la '''prédiction de retour de fonction''' (''return function predictor''). Lorsqu'une fonction est appelée, ce circuit stocke l'adresse de retour d'une fonction dans des registres internes au processeur organisés en pile. Avec cette organisation des registres en forme de pile, on sait d'avance que l'adresse de retour du sous-programme en cours d'exécution est au sommet de cette pile.
==La prédiction de branchement==
La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c'est à dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris.
===Les contraintes d’implémentation de la prédiction de branchement===
La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c'est à dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils vont paradoxalement poser quelques problèmes avec certaines techniques, comme nous le verrons plus bas.
Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent.
===La classification des techniques de prédiction de branchement===
Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc.
Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas.
Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. A l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux.
===La prédiction statique de branchement===
Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc.
Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre.
Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique.
Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit :
* les branchements inconditionnels sont toujours pris ;
* les branchements descendants sont toujours pris ;
* les branchements ascendants ne sont jamais pris.
===La prédiction de branchement avec des compteurs à saturation===
Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique.
Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement.
[[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]]
Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c'est à dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction deux deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB.
===La prédiction de branchement à deux niveaux avec un historique global===
La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux.
Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique.
[[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]]
L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique, et éventuellement avec d'autres informations annexes. Le choix du compteur à saturation à utiliser dépend uniquement du registre d'historique global, aucune autre information n'est utilisée.
[[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]]
Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail.
====La mitigation des interférences par usage de l'adresse de branchement====
La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en terme de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Voyons dans le détail cette dernière. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique pour choisir le compteur à saturation adéquat.
Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique.
Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors.
[[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]]
D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées.
Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente.
[[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]]
Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''.
[[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]]
Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation.
====La mitigation des interférences par la structuration en caches des PHTs====
Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. Si il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique.
La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé.
Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels.
====La mitigation des interférences liées aux branchements biaisés====
D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribué au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchement biaisés ou quasi-biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi-biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit.
La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris.
[[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]]
L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchement très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant.
[[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]]
===Les unités de prédiction à deux niveau locales===
Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisit dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes.
[[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]]
On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécution du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale.
[[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]]
Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC.
[[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]]
Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaise pour les autres types de branchements.
Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats.
Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux.
===Les unités de prédiction à deux niveau mixtes (globale/locale)===
Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction.
Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement.
[[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]]
Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées.
[[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]]
===La prédiction de branchement avec des perceptrons===
Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons.
La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables.
====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements====
L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même.
Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé à été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1.
Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifié. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci :
: <math>Z = w_0 + \sum_i (h_i \times w_i)</math>
[[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]]
: Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids.
Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante :
: <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris.
====L'implémentation matérielle des perceptrons et ses optimisations====
Les calculs réalisés par un perceptrons sont simples, juste des multiplication et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle.
[[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]]
Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide.
Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en terme de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables.
Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit).
====Les avantages et inconvénients des perceptrons pour la prédiction de branchements====
L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge.
Un autre défaut est les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est définit par l'ensemble des points qui respecte l'équation suivante :
: <math>w_0 + \sum_i (h_i \times w_i) = 0</math>
Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcul avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédiction correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements.
Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure.
Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours.
===La prédiction des branchements des boucles===
Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois.
Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité.
==La prédiction basée sur un l'utilisation de plusieurs unités de branchement distinctes==
Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune ayant des registres d'historique de taille différente. Une unité aura un historique de 2 bits, une autre un historique de 3 bits, une autre de 4 bits, etc. Les branchements qui ont un motif répétitifs sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement se basent sur ce principe. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions.
===La méta-prédiction===
La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment.
===La fusion de prédictions===
Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée car elle demande un nombre d'unités important (au moins 4).
L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisis ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité.
[[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]]
===La priorisation des unités de prédiction===
Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction.
[[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]]
==Les optimisations de la prédiction de branchements==
La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quelque soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc.
===Le filtrage de branchements===
Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisé est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements.
Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire es prédictions.
===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables===
Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historique locaux, ou encore de la SRAM des poids des unités à base de peerceptrons. En pratique, ces structures sont adressés non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées.
Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques.
Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise.
==Les processeurs à chemins multiples==
Pour limiter la casse en cas de mauvaise prédiction, certains processeurs chargent des instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris) dans une mémoire cache, qui permet de récupérer rapidement les instructions correctes en cas de mauvaise prédiction de branchement. Certains processeurs vont plus loin et exécutent les deux possibilités séparément : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement mais on peut poursuivre un peu plus loin. On peut remarquer que cette technique utilise la prédiction de direction branchement , pour savoir quelle la destination des branchements. Les techniques utilisées pour annuler les instructions du chemin non-pris sont les mêmes que celles utilisées pour la prédiction de branchement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
===Implémentation===
Implémenter cette technique demande de dupliquer les circuits de chargement pour charger des instructions provenant de plusieurs « chemins d’exécution ». Pour identifier les instructions à annuler, chaque instruction se voit attribuer un numéro par l'unité de chargement, qui indique à quel chemin elle appartient. Chaque chemin peut être dans quatre états, états qui doivent être mémorisés avec le ''program counter'' qui correspond :
* invalide (pas de second chemin) ;
* en cours de chargement ;
* mis en pause suite à un défaut de cache ;
* chemin stoppé car il s'est séparé en deux autres chemins, à cause d'un branchement.
Pour l'accès au cache d'instructions, l'implémentation varie suivant le type de cache utilisé. Certains processeurs utilisent un cache à un seul port avec un circuit d'arbitrage. La politique d'arbitrage peut être plus ou moins complexe. Dans le cas le plus simple, chaque chemin charge ses instructions au tour par tour. Des politiques plus complexes sont possibles : on peut notamment privilégier le chemin qui a la plus forte probabilité d'être pris, pas exemple. D'autres processeurs préfèrent utiliser un cache multi-port, capable d’alimenter plusieurs chemins à la fois.
===L'exécution stricte disjointe===
Avec cette technique, on est limité par le nombre d'unités de calculs, de registres, etc. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
Pour limiter la casse, on peut coupler exécution stricte et prédiction de branchement.
[[File:Exécution stricte 02.png|centre|vignette|upright=2|Exécution stricte.]]
L'idée est de ne pas exécuter les chemins qui ont une probabilité trop faible d'être pris. Si un chemin a une probabilité inférieure à un certain seuil, on ne charge pas ses instructions. On parle d’'''exécution stricte disjointe''' (''disjoint eager execution''). C'est la technique qui est censée donner les meilleurs résultats d'un point de vue théorique. À chaque fois qu'un nouveau branchement est rencontré, le processeur refait les calculs de probabilité. Cela signifie que d'anciens branchements qui n'avaient pas été exécutés car ils avaient une probabilité trop faible peuvent être exécuté si des branchements avec des probabilités encore plus faibles sont rencontrés en cours de route.
[[File:Exécution stricte 03.png|centre|vignette|upright=1|Exécution stricte disjointe.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Interruptions et pipeline
| prevText=Interruptions et pipeline
| next=Dépendances de données
| nextText=Dépendances de données
}}
</noinclude>
4lc9duww4h7bhsjjx11m9lqz92fb90p
682057
682056
2022-07-20T18:39:58Z
Mewtow
31375
/* L'usage de fonctions de hashage pour indexer les diverses SRAM/tables */
wikitext
text/x-wiki
Les branchements ont des effets similaires aux exceptions et interruptions. Lorsqu'on charge un branchement dans le pipeline, l'adresse à laquelle brancher sera connue après quelques cycles et des instructions seront chargées durant ce temps. Pour éviter tout problème, on doit faire en sorte que ces instructions ne modifient pas les registres du processeur ou qu'elles ne lancent pas d'écritures en mémoire. La solution la plus simple consiste à inclure des instructions qui ne font rien à la suite du branchement : c'est ce qu'on appelle un '''délai de branchement'''. Le processeur chargera ces instructions, jusqu’à ce que l'adresse à laquelle brancher soit connue. Cette technique a les mêmes inconvénients que ceux vus dans le chapitre précédent. Pour éviter cela, on peut réutiliser les techniques vues dans les chapitres précédents, avec quelques améliorations.
Pour éviter les délais de branchement, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et l’exécuter avant que celle-ci ne soit connue. Cela demande de résoudre trois problèmes :
* reconnaître les branchements ;
* savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ;
* dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''.
Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit.
[[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]]
==La correction des erreurs de prédiction==
Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Pour corriger les erreurs de prédictions, il faut d'abord les détecter. Pour cela, on rajoute un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée.
[[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]]
Une fois la mauvaise prédiction détectée, il faut corriger le ''program counter'' immédiatement. En effet, si il y a eu erreur de prédiction, le ''program counter'' n'a pas été mis à jour correctement, et il faut faire reprendre le processeur au bon endroit. Pour implémenter cette correction du ''program counter'', il suffit d'utiliser un circuit qui restaure le ''program counter'' s'il y a eu erreur de prédiction. La gestion des mauvaises prédictions dépend fortement du processeur. Certains processeurs peuvent reprendre l’exécution du programme immédiatement après avoir détecté la mauvaise prédiction (ils sont capables de supprimer les instructions qui n'auraient pas dû être exécutées), tandis que d'autres attendent que les instructions chargées par erreur terminent avant de corriger le ''program counter''.
Second point : il faut que le pipeline soit vidé des instructions chargées par erreur. Tant que ces instructions ne sont pas sorties du pipeline, on doit attendre sans charger d'instructions dans le pipeline. Le temps d'attente nécessaire pour vider le pipeline est égal à son nombre d'étages. En effet, la dernière instruction à être chargée dans le pipeline le sera durant l'étape à laquelle on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. De plus, il faut remettre le pipeline dans l'état qu'il avait avant le chargement du branchement. Tout se passe lors de la dernière étape d'enregistrement des résultats en mémoire ou dans les registres. Et pour cela, on réutilise les techniques vues dans le chapitre précédent pour la gestion des exceptions et interruptions.
En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Mais pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient du être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction.
[[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]]
==La reconnaissance des branchements==
Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc du trouver une solution.
La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache.
Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il appelé le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. Si il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement.
==La prédiction de l'adresse de destination==
La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile).
La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible.
: Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit.
===La prédiction de l'adresse de destination pour les branchements directs===
Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Ce cache est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Ce cache est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile.
Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c'est à dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB.
Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l'''aliasing''.
Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares.
[[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]]
D'autres processeurs se passent de BTB. A la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode).
===La prédiction de l'adresse de destination pour les branchements indirects et implicites===
Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Pour cela, ils utilisent un tampon de destination de branchement amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations qui lui permettent de déduire plus ou moins efficacement quelle adresse de destination est la bonne. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits.
Certains processeurs peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'un sous-programme a fini de s’exécuter, cette adresse de retour étant stockée sur la pile, ou dans des registres spéciaux du processeur dans certains cas particuliers. Ils possèdent un circuit spécialisé capable de prédire cette adresse : la '''prédiction de retour de fonction''' (''return function predictor''). Lorsqu'une fonction est appelée, ce circuit stocke l'adresse de retour d'une fonction dans des registres internes au processeur organisés en pile. Avec cette organisation des registres en forme de pile, on sait d'avance que l'adresse de retour du sous-programme en cours d'exécution est au sommet de cette pile.
==La prédiction de branchement==
La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c'est à dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris.
===Les contraintes d’implémentation de la prédiction de branchement===
La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c'est à dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils vont paradoxalement poser quelques problèmes avec certaines techniques, comme nous le verrons plus bas.
Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent.
===La classification des techniques de prédiction de branchement===
Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc.
Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas.
Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. A l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux.
===La prédiction statique de branchement===
Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc.
Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre.
Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique.
Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit :
* les branchements inconditionnels sont toujours pris ;
* les branchements descendants sont toujours pris ;
* les branchements ascendants ne sont jamais pris.
===La prédiction de branchement avec des compteurs à saturation===
Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique.
Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement.
[[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]]
Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c'est à dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction deux deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB.
===La prédiction de branchement à deux niveaux avec un historique global===
La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux.
Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique.
[[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]]
L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique, et éventuellement avec d'autres informations annexes. Le choix du compteur à saturation à utiliser dépend uniquement du registre d'historique global, aucune autre information n'est utilisée.
[[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]]
Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail.
====La mitigation des interférences par usage de l'adresse de branchement====
La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en terme de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Voyons dans le détail cette dernière. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique pour choisir le compteur à saturation adéquat.
Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique.
Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors.
[[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]]
D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées.
Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente.
[[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]]
Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''.
[[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]]
Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation.
====La mitigation des interférences par la structuration en caches des PHTs====
Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. Si il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique.
La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé.
Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels.
====La mitigation des interférences liées aux branchements biaisés====
D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribué au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchement biaisés ou quasi-biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi-biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit.
La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris.
[[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]]
L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchement très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant.
[[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]]
===Les unités de prédiction à deux niveau locales===
Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisit dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes.
[[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]]
On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécution du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale.
[[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]]
Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC.
[[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]]
Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaise pour les autres types de branchements.
Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats.
Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux.
===Les unités de prédiction à deux niveau mixtes (globale/locale)===
Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction.
Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement.
[[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]]
Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées.
[[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]]
===La prédiction de branchement avec des perceptrons===
Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons.
La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables.
====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements====
L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même.
Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé à été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1.
Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifié. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci :
: <math>Z = w_0 + \sum_i (h_i \times w_i)</math>
[[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]]
: Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids.
Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante :
: <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris.
====L'implémentation matérielle des perceptrons et ses optimisations====
Les calculs réalisés par un perceptrons sont simples, juste des multiplication et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle.
[[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]]
Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide.
Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en terme de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables.
Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit).
====Les avantages et inconvénients des perceptrons pour la prédiction de branchements====
L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge.
Un autre défaut est les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est définit par l'ensemble des points qui respecte l'équation suivante :
: <math>w_0 + \sum_i (h_i \times w_i) = 0</math>
Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcul avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédiction correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements.
Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure.
Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours.
===La prédiction des branchements des boucles===
Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois.
Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité.
==La prédiction basée sur un l'utilisation de plusieurs unités de branchement distinctes==
Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune ayant des registres d'historique de taille différente. Une unité aura un historique de 2 bits, une autre un historique de 3 bits, une autre de 4 bits, etc. Les branchements qui ont un motif répétitifs sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement se basent sur ce principe. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions.
===La méta-prédiction===
La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment.
===La fusion de prédictions===
Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée car elle demande un nombre d'unités important (au moins 4).
L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisis ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité.
[[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]]
===La priorisation des unités de prédiction===
Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction.
[[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]]
==Les optimisations de la prédiction de branchements==
La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quelque soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc.
===Le filtrage de branchements===
Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisé est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements.
Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire es prédictions.
===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables===
Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historique locaux, ou encore de la SRAM des poids des unités à base de peerceptrons. En pratique, ces structures sont adressés non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées.
Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques.
Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers.
L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé.
==Les processeurs à chemins multiples==
Pour limiter la casse en cas de mauvaise prédiction, certains processeurs chargent des instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris) dans une mémoire cache, qui permet de récupérer rapidement les instructions correctes en cas de mauvaise prédiction de branchement. Certains processeurs vont plus loin et exécutent les deux possibilités séparément : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement mais on peut poursuivre un peu plus loin. On peut remarquer que cette technique utilise la prédiction de direction branchement , pour savoir quelle la destination des branchements. Les techniques utilisées pour annuler les instructions du chemin non-pris sont les mêmes que celles utilisées pour la prédiction de branchement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
===Implémentation===
Implémenter cette technique demande de dupliquer les circuits de chargement pour charger des instructions provenant de plusieurs « chemins d’exécution ». Pour identifier les instructions à annuler, chaque instruction se voit attribuer un numéro par l'unité de chargement, qui indique à quel chemin elle appartient. Chaque chemin peut être dans quatre états, états qui doivent être mémorisés avec le ''program counter'' qui correspond :
* invalide (pas de second chemin) ;
* en cours de chargement ;
* mis en pause suite à un défaut de cache ;
* chemin stoppé car il s'est séparé en deux autres chemins, à cause d'un branchement.
Pour l'accès au cache d'instructions, l'implémentation varie suivant le type de cache utilisé. Certains processeurs utilisent un cache à un seul port avec un circuit d'arbitrage. La politique d'arbitrage peut être plus ou moins complexe. Dans le cas le plus simple, chaque chemin charge ses instructions au tour par tour. Des politiques plus complexes sont possibles : on peut notamment privilégier le chemin qui a la plus forte probabilité d'être pris, pas exemple. D'autres processeurs préfèrent utiliser un cache multi-port, capable d’alimenter plusieurs chemins à la fois.
===L'exécution stricte disjointe===
Avec cette technique, on est limité par le nombre d'unités de calculs, de registres, etc. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
Pour limiter la casse, on peut coupler exécution stricte et prédiction de branchement.
[[File:Exécution stricte 02.png|centre|vignette|upright=2|Exécution stricte.]]
L'idée est de ne pas exécuter les chemins qui ont une probabilité trop faible d'être pris. Si un chemin a une probabilité inférieure à un certain seuil, on ne charge pas ses instructions. On parle d’'''exécution stricte disjointe''' (''disjoint eager execution''). C'est la technique qui est censée donner les meilleurs résultats d'un point de vue théorique. À chaque fois qu'un nouveau branchement est rencontré, le processeur refait les calculs de probabilité. Cela signifie que d'anciens branchements qui n'avaient pas été exécutés car ils avaient une probabilité trop faible peuvent être exécuté si des branchements avec des probabilités encore plus faibles sont rencontrés en cours de route.
[[File:Exécution stricte 03.png|centre|vignette|upright=1|Exécution stricte disjointe.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Interruptions et pipeline
| prevText=Interruptions et pipeline
| next=Dépendances de données
| nextText=Dépendances de données
}}
</noinclude>
afie9pjpiik7ig36r9vfwyx0sh04az9
682058
682057
2022-07-20T18:56:03Z
Mewtow
31375
orthotypo
wikitext
text/x-wiki
Les branchements ont des effets similaires aux exceptions et interruptions. Lorsqu'on charge un branchement dans le pipeline, l'adresse à laquelle brancher sera connue après quelques cycles et des instructions seront chargées durant ce temps. Pour éviter tout problème, on doit faire en sorte que ces instructions ne modifient pas les registres du processeur ou qu'elles ne lancent pas d'écritures en mémoire. La solution la plus simple consiste à inclure des instructions qui ne font rien à la suite du branchement : c'est ce qu'on appelle un '''délai de branchement'''. Le processeur chargera ces instructions, jusqu’à ce que l'adresse à laquelle brancher soit connue. Cette technique a les mêmes inconvénients que ceux vus dans le chapitre précédent. Pour éviter cela, on peut réutiliser les techniques vues dans les chapitres précédents, avec quelques améliorations.
Pour éviter les délais de branchement, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et l’exécuter avant que celle-ci ne soit connue. Cela demande de résoudre trois problèmes :
* reconnaître les branchements ;
* savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ;
* dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''.
Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit.
[[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]]
==La correction des erreurs de prédiction==
Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Pour corriger les erreurs de prédictions, il faut d'abord les détecter. Pour cela, on rajoute un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée.
[[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]]
Une fois la mauvaise prédiction détectée, il faut corriger le ''program counter'' immédiatement. En effet, s’il y a eu erreur de prédiction, le ''program counter'' n'a pas été mis à jour correctement, et il faut faire reprendre le processeur au bon endroit. Pour implémenter cette correction du ''program counter'', il suffit d'utiliser un circuit qui restaure le ''program counter'' s'il y a eu erreur de prédiction. La gestion des mauvaises prédictions dépend fortement du processeur. Certains processeurs peuvent reprendre l’exécution du programme immédiatement après avoir détecté la mauvaise prédiction (ils sont capables de supprimer les instructions qui n'auraient pas dû être exécutées), tandis que d'autres attendent que les instructions chargées par erreur terminent avant de corriger le ''program counter''.
Second point : il faut que le pipeline soit vidé des instructions chargées par erreur. Tant que ces instructions ne sont pas sorties du pipeline, on doit attendre sans charger d'instructions dans le pipeline. Le temps d'attente nécessaire pour vider le pipeline est égal à son nombre d'étages. En effet, la dernière instruction à être chargée dans le pipeline le sera durant l'étape à laquelle on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. De plus, il faut remettre le pipeline dans l'état qu'il avait avant le chargement du branchement. Tout se passe lors de la dernière étape d'enregistrement des résultats en mémoire ou dans les registres. Et pour cela, on réutilise les techniques vues dans le chapitre précédent pour la gestion des exceptions et interruptions.
En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction.
[[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]]
==La reconnaissance des branchements==
Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution.
La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache.
Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement.
==La prédiction de l'adresse de destination==
La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile).
La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible.
: Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit.
===La prédiction de l'adresse de destination pour les branchements directs===
Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Ce cache est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Ce cache est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile.
Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB.
Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''.
Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares.
[[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]]
D'autres processeurs se passent de BTB. A la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode).
===La prédiction de l'adresse de destination pour les branchements indirects et implicites===
Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Pour cela, ils utilisent un tampon de destination de branchement amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations qui lui permettent de déduire plus ou moins efficacement quelle adresse de destination est la bonne. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits.
Certains processeurs peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'un sous-programme a fini de s’exécuter, cette adresse de retour étant stockée sur la pile, ou dans des registres spéciaux du processeur dans certains cas particuliers. Ils possèdent un circuit spécialisé capable de prédire cette adresse : la '''prédiction de retour de fonction''' (''return function predictor''). Lorsqu'une fonction est appelée, ce circuit stocke l'adresse de retour d'une fonction dans des registres internes au processeur, organisés en pile. Avec cette organisation des registres en forme de pile, on sait d'avance que l'adresse de retour du sous-programme en cours d'exécution est au sommet de cette pile.
==La prédiction de branchement==
La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris.
===Les contraintes d’implémentation de la prédiction de branchement===
La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes avec certaines techniques, comme nous le verrons plus bas.
Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent.
===La classification des techniques de prédiction de branchement===
Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc.
Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas.
Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux.
===La prédiction statique de branchement===
Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc.
Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre.
Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique.
Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit :
* les branchements inconditionnels sont toujours pris ;
* les branchements descendants sont toujours pris ;
* les branchements ascendants ne sont jamais pris.
===La prédiction de branchement avec des compteurs à saturation===
Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique.
Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement.
[[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]]
Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB.
===La prédiction de branchement à deux niveaux avec un historique global===
La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux.
Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique.
[[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]]
L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique, et éventuellement avec d'autres informations annexes. Le choix du compteur à saturation à utiliser dépend uniquement du registre d'historique global, aucune autre information n'est utilisée.
[[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]]
Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail.
====La mitigation des interférences par usage de l'adresse de branchement====
La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Voyons dans le détail cette dernière. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat.
Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique.
Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors.
[[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]]
D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées.
Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente.
[[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]]
Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''.
[[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]]
Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation.
====La mitigation des interférences par la structuration en caches des PHTs====
Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique.
La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé.
Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels.
====La mitigation des interférences liées aux branchements biaisés====
D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit.
La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris.
[[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]]
L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant.
[[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]]
===Les unités de prédiction à deux niveaux locales===
Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes.
[[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]]
On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale.
[[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]]
Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC.
[[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]]
Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements.
Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats.
Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux.
===Les unités de prédiction à deux niveaux mixtes (globale/locale)===
Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction.
Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement.
[[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]]
Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées.
[[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]]
===La prédiction de branchement avec des perceptrons===
Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons.
La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables.
====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements====
L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même.
Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1.
Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci :
: <math>Z = w_0 + \sum_i (h_i \times w_i)</math>
[[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]]
: Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids.
Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante :
: <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris.
====L'implémentation matérielle des perceptrons et ses optimisations====
Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle.
[[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]]
Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide.
Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables.
Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit).
====Les avantages et inconvénients des perceptrons pour la prédiction de branchements====
L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge.
Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante :
: <math>w_0 + \sum_i (h_i \times w_i) = 0</math>
Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements.
Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure.
Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours.
===La prédiction des branchements des boucles===
Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois.
Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité.
==La prédiction basée sur un l'utilisation de plusieurs unités de branchement distinctes==
Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune ayant des registres d'historique de taille différente. Une unité aura un historique de 2 bits, une autre un historique de 3 bits, une autre de 4 bits, etc. Les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement se basent sur ce principe. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions.
===La méta-prédiction===
La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment.
===La fusion de prédictions===
Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4).
L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité.
[[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]]
===La priorisation des unités de prédiction===
Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction.
[[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]]
==Les optimisations de la prédiction de branchements==
La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc.
===Le filtrage de branchements===
Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements.
Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions.
===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables===
Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de peerceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées.
Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques.
Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers.
L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé.
==Les processeurs à chemins multiples==
Pour limiter la casse en cas de mauvaise prédiction, certains processeurs chargent des instructions en provenance des deux chemins (celui du branchement pris, et celui du branchement non pris) dans une mémoire cache, qui permet de récupérer rapidement les instructions correctes en cas de mauvaise prédiction de branchement. Certains processeurs vont plus loin et exécutent les deux possibilités séparément : c'est ce qu'on appelle l''''exécution stricte''' (''eager execution''). Bien sûr, on n’est pas limité à un seul branchement, mais on peut poursuivre un peu plus loin. On peut remarquer que cette technique utilise la prédiction de direction branchement, pour savoir quelle la destination des branchements. Les techniques utilisées pour annuler les instructions du chemin non-pris sont les mêmes que celles utilisées pour la prédiction de branchement.
[[File:Exécution stricte 04.png|centre|vignette|upright=2|Exécution stricte]]
===Implémentation===
Implémenter cette technique demande de dupliquer les circuits de chargement pour charger des instructions provenant de plusieurs « chemins d’exécution ». Pour identifier les instructions à annuler, chaque instruction se voit attribuer un numéro par l'unité de chargement, qui indique à quel chemin elle appartient. Chaque chemin peut être dans quatre états, états qui doivent être mémorisés avec le ''program counter'' qui correspond :
* invalide (pas de second chemin) ;
* en cours de chargement ;
* mis en pause suite à un défaut de cache ;
* chemin stoppé car il s'est séparé en deux autres chemins, à cause d'un branchement.
Pour l'accès au cache d'instructions, l'implémentation varie suivant le type de cache utilisé. Certains processeurs utilisent un cache à un seul port avec un circuit d'arbitrage. La politique d'arbitrage peut être plus ou moins complexe. Dans le cas le plus simple, chaque chemin charge ses instructions au tour par tour. Des politiques plus complexes sont possibles : on peut notamment privilégier le chemin qui a la plus forte probabilité d'être pris, pas exemple. D'autres processeurs préfèrent utiliser un cache multi-port, capable d’alimenter plusieurs chemins à la fois.
===L'exécution stricte disjointe===
Avec cette technique, on est limité par le nombre d'unités de calculs, de registres, etc. Au bout d'un certain nombre d’embranchements, le processeur finit par ne plus pouvoir poursuivre l’exécution, par manque de ressources matérielles.
[[File:Exécution stricte 01.png|centre|vignette|upright=2|Exécution stricte, seconde.]]
Pour limiter la casse, on peut coupler exécution stricte et prédiction de branchement.
[[File:Exécution stricte 02.png|centre|vignette|upright=2|Exécution stricte.]]
L'idée est de ne pas exécuter les chemins qui ont une probabilité trop faible d'être pris. Si un chemin a une probabilité inférieure à un certain seuil, on ne charge pas ses instructions. On parle d’'''exécution stricte disjointe''' (''disjoint eager execution''). C'est la technique qui est censée donner les meilleurs résultats d'un point de vue théorique. À chaque fois qu'un nouveau branchement est rencontré, le processeur refait les calculs de probabilité. Cela signifie que d'anciens branchements qui n'avaient pas été exécutés car ils avaient une probabilité trop faible peuvent être exécutés si des branchements avec des probabilités encore plus faibles sont rencontrés en cours de route.
[[File:Exécution stricte 03.png|centre|vignette|upright=1|Exécution stricte disjointe.]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Interruptions et pipeline
| prevText=Interruptions et pipeline
| next=Dépendances de données
| nextText=Dépendances de données
}}
</noinclude>
95qgdu423g1qjxrozfjb04fyetn9jw5
Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading
0
65961
682062
677458
2022-07-20T20:07:54Z
Mewtow
31375
/* La fenêtre d'instruction */
wikitext
text/x-wiki
Vous pensez surement qu'il faut obligatoirement plusieurs cœurs pour exécuter plusieurs programmes en parallèle, mais sachez que c'est faux ! Il existe des processeurs spéciaux qui n'ont qu'un seul cœur, mais sont cependant capables d’exécuter plusieurs programmes quasi-simultanément. La différence avec les processeurs multicœurs est qu'ils ne sont pas capable d’exécuter plusieurs programmes en même temps, mais qu'il alternent entre les programmes à exécuter. Plusieurs programmes s’exécutent donc sur le même processeur, mais chacun à leur tour et non en même temps. D'ordinaire, cette alternance est gérée par le système d'exploitation, mais certains processeurs se passent de l'aide du système d'exploitation et gèrent cette alternance eux-même, directement au niveau matériel. Les processeurs en question sont appelés des processeurs multithreadés, ou encore des '''architectures multithreadées''', pour les distinguer des processeurs multicœurs.
==Du parallélisme avec un seul processeur==
Pour comprendre en quoi les architectures multithreadées sont intéressantes, il faut savoir qu'il arrive que l'unité de calcul d'un processeur ne fasse rien, pour diverses raisons. Par exemple, l'unité de calcul peut n'avoir aucun calcul à effectuer pendant que le processeur accède à la mémoire. Pour éviter cela, les différentes techniques d’exécution dans le désordre sont une solution toute indiquée, mais elles ne suffisent pas toujours. Les architectures multithreadées sont une autre solution à ce problème. Elles visent à exécuter plusieurs programmes sur un même processeur, de manière à ce que les temps morts d'un programme soient remplis par les calculs du second et réciproquement.
Notons que cette méthode marche d'autant mieux sur les processeurs sans exécution dans le désordre et autres optimisations du même genre. Architectures multithreadées et exécution dans le désordre ont en effet le même but, bien que les solutions apportées soient différentes. Il y a donc un conflit entre les deux approches. C'est la raison pour laquelle les processeurs grand public modernes, qui ont une exécution dans le désordre très performante, ne sont pas des architectures multithreadées.
===Les registres sont dupliqués et le séquenceur adapté===
L'implémentation de ces architectures est assez simple. Partons du principe que le processeur est composé d'un séquenceur, d'un chemin de données et d'une fenêtre d'instruction. Le séquenceur charge et décode les instructions, le chemin de données fait les calculs et communique avec la mémoire. La fenêtre d'instruction sert d'interface entre les deux en accumulant les instructions décodées et prêtes à être exécutée.
Dans les grandes lignes, il suffit de dupliquer les séquenceurs et les registres. Il est obligatoire de dupliquer les registres pour que chaque programme ait son ensemble de registres rien qu'à lui. Cela demande soit un banc de registre par programme, soit un banc de registre commun géré par fenêtrage de registre (chaque programme ayant sa propre fenêtre de registres). Il faut aussi dupliquer le séquenceur en autant de programmes qu'on veut exécuter en simultané, afin que chaque séquenceur s'occupe d'un programme bien à lui. Mais quelques optimisations, dépendantes de l'architecture, permettent d'éviter de dupliquer une bonne partie du séquenceur.
Au minimum, il faut utiliser plusieurs Program Counter, vu que le processeur charge des instructions en provenance de programmes différents. Un circuit se charge de choisir le Program Counter - le thread - qui a la chance de charger ses instructions. Et l’algorithme de choix du programme, l'algorithme d'ordonnancement, dépend du processeur, comme on le verra dans la suite du chapitre. Par exemple, certains processeurs multithreadés changent de programme à chaque cycle d'horloge et donnent la main à chaque programme l'un après l'autre, en boucle. Une autre possibilité est de donner la priorité aux threads qui accèdent le moins à la mémoire RAM (peu de cache miss), à ceux qui ont exécutés le moins d'instructions de branchement récemment, etc. Pour ce qui est de l'unité de chargement et des décodeurs, ils sont en général dupliqués, mais ce n'est pas systématique et tout dépend de l'algorithme d'ordonnancement utilisé. Par exemple, dans le cas où le processeur change de programme à chaque cycle d'horloge, il n'y a besoin que d'une seule unité de chargement et un seul décodeur.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé dans lequel le séquenceur (ici nommé Fetch) est dupliqué.]]
===Le partage des ressources matérielles===
Généralement, la fenêtre d'instruction (instruction windows) est partagée entre les différents programmes. Chaque instruction mise en attente (chaque entrée) stocke le numéro du programme, attribué à l'instruction par l'unité de Fetch.
[[File:Instruction buffer et SMT.png|centre|vignette|upright=2|Instruction buffer et SMT.]]
La fenêtre d'instruction est partitionnée en morceaux réservés à un programme bien précis. Si ces morceaux ont la même taille, on parle de ''partitionnement statique''. Dans d'autres cas, si jamais un programme a besoin de plus de place que l'autre, il peut réserver un peu plus de place que son concurrent : on parle de ''partitionnement dynamique''.
[[File:Partitionnement de l'Instruction Buffer avec l'hyperthreading.png|centre|vignette|upright=2|Partitionnement de l'Instruction Buffer avec l'hyperthreading.]]
Il en est de même pour la TLB, qui est elle aussi partitionnée dynamiquement entre plusieurs ''threads''.
==Les différents types d'architectures multithreadées==
Il existe plusieurs techniques pour ce faire, qui portent les doux noms de ''Coarse Grained Multithreading'', ''Fine Grained Multithreading'', ''multithreading'' total et ''Simultaneous MultiThreading''. Voyons les en détail.
===Le multithreading sur les processeurs à émission unique===
[[File:Multithreading et mitigation de la latence mémoire.png|vignette|Multithreading et mitigation de la latence mémoire.]]
Le '''Coarse Grained Multithreading''' change de programme quand un évènement bien précis a lieu. Le changement de programme peut s’effectuer pour des raisons différentes. En général, on change de programme lorsque certaines instructions sont exécutées : accès à la mémoire, branchements, etc. Le cas le plus fréquent est un accès à la mémoire (lors d'un cache miss). Il faut dire que l'accès à la mémoire est quelque chose de très lent, aussi changer de programme et exécuter des instructions pour recouvrir l'accès à la mémoire est une bonne chose. On peut aussi utiliser une instruction de changement de programme, fournie par le jeu d'instruction du processeur.
[[File:Coarse Grained Multithreading.png|centre|vignette|upright=2|Coarse Grained Multithreading.]]
Le '''Fine Grained Multithreading''' change de programme à chaque cycle d'horloge. L'avantage est que deux instructions successives n'appartiennent pas au même programme. Il n'y a donc pas de dépendances entre instructions successives et les circuits liés à la détection ou la prise en compte de ces dépendances disparaissent. Mais pour cela, on est obligé d'avoir autant de programmes en cours d’exécution qu'il y a d'étages de pipeline. Et quand ce n'est pas le cas, on trouver des solutions pour limiter la casse. Par exemple, certains processeurs lancent plusieurs instructions d'un même programme à la suite, chaque instruction contenant quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. Cette technique s'appelle l''''anticipation de dépendances'''.
[[File:Fine Grained Multithreading.png|centre|vignette|upright=2|Fine Grained Multithreading.]]
Sur d'autres processeurs, plusieurs programmes s’exécutent en même temps et chaque programme utilise l'unité de calcul dès que les autres la laissent libre. On parle alors de '''multithreading total'''.
[[File:Full multithreading.png|centre|vignette|upright=2|Full multithreading.]]
===Le multithreading sur les processeurs à émission multiple===
Les techniques vues au-dessus peuvent s'adapter sur les processeurs superscalaires, à savoir qui sont capables de démarrer plusieurs instructions simultanément sur des unités de calcul séparées. Dans les deux cas, les unités de calcul exécutent les instruction d'un même programme en même temps. Ce qui veut dire qu'on a pas de situation où une unité de calcul exécute une instruction du premier programme alors qu'une autre exécute une instruction du second programme. Lors d'un cycle d'horloge, les deux unités de calcul doivent exécuter des opérations d'un même programme.
[[File:CGMT sur processeur superscalaire.png|centre|vignette|upright=2|CGMT sur processeur superscalaire]]
[[File:FGMT sur processeur superscalaire.png|centre|vignette|upright=2|FGMT sur processeur superscalaire]]
Mais on peut aussi faire en sorte que plusieurs programmes puissent faire démarrer leurs instructions en même temps. A chaque cycle d'horloge, le processeur peut exécuter des instructions provenant de divers programmes. On obtient la technique du '''Simultaneous Multi-Threading''' ou SMT.
[[File:Simultaneous Multi-Threading.png|centre|vignette|upright=2|Simultaneous Multi-Threading]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Architectures multiprocesseurs et multicœurs
| prevText=Architectures multiprocesseurs et multicœurs
| next=Architectures distribuées, NUMA et COMA
| nextText=Architectures distribuées, NUMA et COMA
}}
</noinclude>
kezc0uu359dpkvf7u17vydsbt1pv341
682063
682062
2022-07-20T20:14:01Z
Mewtow
31375
/* Le partage des ressources matérielles */
wikitext
text/x-wiki
Vous pensez surement qu'il faut obligatoirement plusieurs cœurs pour exécuter plusieurs programmes en parallèle, mais sachez que c'est faux ! Il existe des processeurs spéciaux qui n'ont qu'un seul cœur, mais sont cependant capables d’exécuter plusieurs programmes quasi-simultanément. La différence avec les processeurs multicœurs est qu'ils ne sont pas capable d’exécuter plusieurs programmes en même temps, mais qu'il alternent entre les programmes à exécuter. Plusieurs programmes s’exécutent donc sur le même processeur, mais chacun à leur tour et non en même temps. D'ordinaire, cette alternance est gérée par le système d'exploitation, mais certains processeurs se passent de l'aide du système d'exploitation et gèrent cette alternance eux-même, directement au niveau matériel. Les processeurs en question sont appelés des processeurs multithreadés, ou encore des '''architectures multithreadées''', pour les distinguer des processeurs multicœurs.
==Du parallélisme avec un seul processeur==
Pour comprendre en quoi les architectures multithreadées sont intéressantes, il faut savoir qu'il arrive que l'unité de calcul d'un processeur ne fasse rien, pour diverses raisons. Par exemple, l'unité de calcul peut n'avoir aucun calcul à effectuer pendant que le processeur accède à la mémoire. Pour éviter cela, les différentes techniques d’exécution dans le désordre sont une solution toute indiquée, mais elles ne suffisent pas toujours. Les architectures multithreadées sont une autre solution à ce problème. Elles visent à exécuter plusieurs programmes sur un même processeur, de manière à ce que les temps morts d'un programme soient remplis par les calculs du second et réciproquement.
Notons que cette méthode marche d'autant mieux sur les processeurs sans exécution dans le désordre et autres optimisations du même genre. Architectures multithreadées et exécution dans le désordre ont en effet le même but, bien que les solutions apportées soient différentes. Il y a donc un conflit entre les deux approches. C'est la raison pour laquelle les processeurs grand public modernes, qui ont une exécution dans le désordre très performante, ne sont pas des architectures multithreadées.
===Les registres sont dupliqués et le séquenceur adapté===
L'implémentation de ces architectures est assez simple. Partons du principe que le processeur est composé d'un séquenceur, d'un chemin de données et d'une fenêtre d'instruction. Le séquenceur charge et décode les instructions, le chemin de données fait les calculs et communique avec la mémoire. La fenêtre d'instruction sert d'interface entre les deux en accumulant les instructions décodées et prêtes à être exécutée.
Dans les grandes lignes, il suffit de dupliquer les séquenceurs et les registres. Il est obligatoire de dupliquer les registres pour que chaque programme ait son ensemble de registres rien qu'à lui. Cela demande soit un banc de registre par programme, soit un banc de registre commun géré par fenêtrage de registre (chaque programme ayant sa propre fenêtre de registres). Il faut aussi dupliquer le séquenceur en autant de programmes qu'on veut exécuter en simultané, afin que chaque séquenceur s'occupe d'un programme bien à lui. Mais quelques optimisations, dépendantes de l'architecture, permettent d'éviter de dupliquer une bonne partie du séquenceur.
Au minimum, il faut utiliser plusieurs Program Counter, vu que le processeur charge des instructions en provenance de programmes différents. Un circuit se charge de choisir le Program Counter - le thread - qui a la chance de charger ses instructions. Et l’algorithme de choix du programme, l'algorithme d'ordonnancement, dépend du processeur, comme on le verra dans la suite du chapitre. Par exemple, certains processeurs multithreadés changent de programme à chaque cycle d'horloge et donnent la main à chaque programme l'un après l'autre, en boucle. Une autre possibilité est de donner la priorité aux threads qui accèdent le moins à la mémoire RAM (peu de cache miss), à ceux qui ont exécutés le moins d'instructions de branchement récemment, etc. Pour ce qui est de l'unité de chargement et des décodeurs, ils sont en général dupliqués, mais ce n'est pas systématique et tout dépend de l'algorithme d'ordonnancement utilisé. Par exemple, dans le cas où le processeur change de programme à chaque cycle d'horloge, il n'y a besoin que d'une seule unité de chargement et un seul décodeur.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé dans lequel le séquenceur (ici nommé Fetch) est dupliqué.]]
===Le partage des ressources matérielles===
Généralement, la fenêtre d'instruction (instruction windows) est partagée entre les différents programmes. Chaque instruction mise en attente (chaque entrée) stocke le numéro du programme, attribué à l'instruction par l'unité de Fetch.
[[File:Instruction buffer et SMT.png|centre|vignette|upright=2|Instruction buffer et SMT.]]
La fenêtre d'instruction est partitionnée en morceaux réservés à un programme bien précis. Si ces morceaux ont la même taille, on parle de ''partitionnement statique''. Si les morceaux ont des tailles qui changent lors de l’exécution, on parle de ''partitionnement dynamique''. Le partitionnement statique a l'avantage d'être simple, facile à implémenter. Par contre, il gère mal les situations où un programme est plus gourmand que l'autre. Idéalement, le programme plus gourmand devrait avoir plus d'entrées allouées dans la fenêtre d'instruction, là où le programme qui a besoin de moins devrait laisser la place. Le partitionnement statique donne des portions égale à chaque programme, ce qui marche mal pour de telles situations. A l'opposé, le partitionnement dynamique permet à chaque programme d'avoir le plus de ressources possibles, ce qui maximise les performances. Si jamais un programme a besoin de plus de place que l'autre, il peut réserver un peu plus de place que son concurrent. Mais la gestion du partitionnement est alors plus compliquée, et demande de partitionner efficacement la fenêtre d'instruction, sans quoi les deux programmes exécutées risquent de se marcher dessus et d'entrer en compétition pour les ressources de la fenêtre d'instruction.
[[File:Partitionnement de l'Instruction Buffer avec l'hyperthreading.png|centre|vignette|upright=2|Partitionnement de l'Instruction Buffer avec l'hyperthreading.]]
Il en est de même pour la TLB, qui est elle aussi partitionnée entre plusieurs ''threads''. La plupart du temps, le partitionnement est statique sur les TLB dédiées aux instructions, alors que les TLB pour les données sont partagées dynamiquement. C'est le cas sur les architectures Skylake d'Intel, où les 128 entrées de la TLB d'instruction de niveau 1 ont découpées en deux sections de 64 entrées, une par programme/''thread'', les autres TLB étant partitionnées dynamiquement.
==Les différents types d'architectures multithreadées==
Il existe plusieurs techniques pour ce faire, qui portent les doux noms de ''Coarse Grained Multithreading'', ''Fine Grained Multithreading'', ''multithreading'' total et ''Simultaneous MultiThreading''. Voyons les en détail.
===Le multithreading sur les processeurs à émission unique===
[[File:Multithreading et mitigation de la latence mémoire.png|vignette|Multithreading et mitigation de la latence mémoire.]]
Le '''Coarse Grained Multithreading''' change de programme quand un évènement bien précis a lieu. Le changement de programme peut s’effectuer pour des raisons différentes. En général, on change de programme lorsque certaines instructions sont exécutées : accès à la mémoire, branchements, etc. Le cas le plus fréquent est un accès à la mémoire (lors d'un cache miss). Il faut dire que l'accès à la mémoire est quelque chose de très lent, aussi changer de programme et exécuter des instructions pour recouvrir l'accès à la mémoire est une bonne chose. On peut aussi utiliser une instruction de changement de programme, fournie par le jeu d'instruction du processeur.
[[File:Coarse Grained Multithreading.png|centre|vignette|upright=2|Coarse Grained Multithreading.]]
Le '''Fine Grained Multithreading''' change de programme à chaque cycle d'horloge. L'avantage est que deux instructions successives n'appartiennent pas au même programme. Il n'y a donc pas de dépendances entre instructions successives et les circuits liés à la détection ou la prise en compte de ces dépendances disparaissent. Mais pour cela, on est obligé d'avoir autant de programmes en cours d’exécution qu'il y a d'étages de pipeline. Et quand ce n'est pas le cas, on trouver des solutions pour limiter la casse. Par exemple, certains processeurs lancent plusieurs instructions d'un même programme à la suite, chaque instruction contenant quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. Cette technique s'appelle l''''anticipation de dépendances'''.
[[File:Fine Grained Multithreading.png|centre|vignette|upright=2|Fine Grained Multithreading.]]
Sur d'autres processeurs, plusieurs programmes s’exécutent en même temps et chaque programme utilise l'unité de calcul dès que les autres la laissent libre. On parle alors de '''multithreading total'''.
[[File:Full multithreading.png|centre|vignette|upright=2|Full multithreading.]]
===Le multithreading sur les processeurs à émission multiple===
Les techniques vues au-dessus peuvent s'adapter sur les processeurs superscalaires, à savoir qui sont capables de démarrer plusieurs instructions simultanément sur des unités de calcul séparées. Dans les deux cas, les unités de calcul exécutent les instruction d'un même programme en même temps. Ce qui veut dire qu'on a pas de situation où une unité de calcul exécute une instruction du premier programme alors qu'une autre exécute une instruction du second programme. Lors d'un cycle d'horloge, les deux unités de calcul doivent exécuter des opérations d'un même programme.
[[File:CGMT sur processeur superscalaire.png|centre|vignette|upright=2|CGMT sur processeur superscalaire]]
[[File:FGMT sur processeur superscalaire.png|centre|vignette|upright=2|FGMT sur processeur superscalaire]]
Mais on peut aussi faire en sorte que plusieurs programmes puissent faire démarrer leurs instructions en même temps. A chaque cycle d'horloge, le processeur peut exécuter des instructions provenant de divers programmes. On obtient la technique du '''Simultaneous Multi-Threading''' ou SMT.
[[File:Simultaneous Multi-Threading.png|centre|vignette|upright=2|Simultaneous Multi-Threading]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Architectures multiprocesseurs et multicœurs
| prevText=Architectures multiprocesseurs et multicœurs
| next=Architectures distribuées, NUMA et COMA
| nextText=Architectures distribuées, NUMA et COMA
}}
</noinclude>
igag2p89sgyvo8o6gdthmn5wfj981us
Ataraxie chez Épicure
0
66512
682095
681935
2022-07-21T01:00:36Z
JackBot
14683
Formatage, [[Spécial:Pages non catégorisées]]
wikitext
text/x-wiki
==Le bonheur du sage==
===Lettre à Ménécée===
C'est une lettre qu’Épicure (341 av. JC) adresse à un jeune disciple. Il ne s'adresse donc pas à un large public, mais à quelqu'un en particulier à qui il présente la philosophie comme exercice continuel et pas seulement un apprentissage de dogmes. C'est pourquoi il n'y a pas d'âge pour philosopher. Il n'y a pas de terme à la pratique. D'autre part le lien que crée l'amitié est plus fort que le lien politique, c'est ce que montre le choix de la lettre.
*Philosopher est d’abord une façon d’être
-ne pas craindre la mort
-ne pas craindre les dieux
-modérer ses désirs pour éviter la souffrance
-chercher le bonheur
*Il s’agit donc de se libérer de la crainte.
-> La philosophie est un exercice de libération, un apprentissage de la liberté
*Après la présentation de la philosophie comme exercice et poser le bonheur comme but à atteindre, le texte s'articule ainsi:
Les Dieux ne sont pas à craindre. Ils ne s'intéressent pas aux hommes. Poser le contraire c'est être superstitieux et perdre le bonheur.
Refus du hasard et de la fatalité.
Certaines circonstances peuvent nuire à notre liberté mais elles ne sont pas une entrave.
Le plaisir est la quête essentielle de l'homme. Il est fondamentalement absence de douleur.
Le plaisir est dans les limites du besoin, sans exclure le superflu.
La tempérance est une expérience singulière: refus de toute pensée systématique.
Le bonheur s'obtient par une expérience et une pensée pratique. Nécessité de connaître la physique pour comprendre que nous n'échappons pas aux lois de la nature.
le plaisir ultime: la réflexion.
*à lire :
[http://www.scienceshumaines.com/epicure-et-le-bonheur-de-l-homme-libre_fr_25170.html|le bonheur du sage]dans Sciences Humaines Alain Gigandet. Maître de conférences en histoire de la philosophie ancienne à l’université de Paris-XII, il a dirigé avec Pierre-Marie Morel Lire Épicure et les épicuriens, Puf, 2007. Article publié le 29/03/2010
{{AutoCat}}
fbaop5sip8y37god1yo8va71eque9n5
Fonctionnement d'un ordinateur/Sommaire
0
69596
682067
680110
2022-07-20T20:36:57Z
Mewtow
31375
/* La hiérarchie mémoire */
wikitext
text/x-wiki
__NOTOC__
* [[Fonctionnement d'un ordinateur/Introduction|Introduction]]
==Le codage des informations==
* [[Fonctionnement d'un ordinateur/L'encodage des données|L'encodage des données]]
* [[Fonctionnement d'un ordinateur/Le codage des nombres|Le codage des nombres]]
==Les circuits électroniques==
* [[Fonctionnement d'un ordinateur/Les portes logiques|Les portes logiques]]
* [[Fonctionnement d'un ordinateur/Les transistors et portes logiques|Les transistors et portes logiques]]
===Les circuits combinatoires===
* [[Fonctionnement d'un ordinateur/Les circuits combinatoires|Les circuits combinatoires]]
* [[Fonctionnement d'un ordinateur/Les circuits de calcul logique et bit à bit|Les circuits de calcul logique et bit à bit]]
* [[Fonctionnement d'un ordinateur/Les circuits de sélection|Les circuits de sélection]]
* [[Fonctionnement d'un ordinateur/Les opérations FFS, FFZ, CTO et CLO|Les opérations FFS, FFZ, CTO et CLO]]
===Les circuits séquentiels===
* [[Fonctionnement d'un ordinateur/Les bascules : des mémoires de 1 bit|Les bascules : des mémoires de 1 bit]]
* [[Fonctionnement d'un ordinateur/Les circuits synchrones et asynchrones|Les circuits synchrones et asynchrones]]
* [[Fonctionnement d'un ordinateur/Les registres et mémoires adressables|Les registres et mémoires adressables]]
* [[Fonctionnement d'un ordinateur/Les circuits compteurs et décompteurs|Les circuits compteurs et décompteurs]]
* [[Fonctionnement d'un ordinateur/Les circuits de génération d'aléatoire|Les circuits de génération d'aléatoire]]
===Les circuits de calcul et de comparaison===
* [[Fonctionnement d'un ordinateur/Les circuits de comparaison|Les circuits de comparaison]]
* [[Fonctionnement d'un ordinateur/Les circuits de décalage et de rotation|Les circuits de décalage et de rotation]]
* [[Fonctionnement d'un ordinateur/Les circuits de calcul entier|Les circuits de calcul entier]]
* [[Fonctionnement d'un ordinateur/Les circuits de calcul flottant|Les circuits de calcul flottant]]
===Les autres circuits===
* [[Fonctionnement d'un ordinateur/Les circuits de correction d'erreur|Les circuits de correction d'erreur]]
* [[Fonctionnement d'un ordinateur/Les circuits de conversion analogique-numérique|Les circuits de conversion analogique-numérique]]
==L'architecture d'un ordinateur==
* [[Fonctionnement d'un ordinateur/L'architecture de base d'un ordinateur|L'architecture de base d'un ordinateur]]
* [[Fonctionnement d'un ordinateur/La hiérarchie mémoire|La hiérarchie mémoire]]
* [[Fonctionnement d'un ordinateur/La performance d'un ordinateur|La performance d'un ordinateur]]
* [[Fonctionnement d'un ordinateur/La consommation d'énergie d'un ordinateur|La consommation d'énergie d'un ordinateur]]
==Les bus et liaisons point à point==
* [[Fonctionnement d'un ordinateur/Les bus et liaisons point à point (généralités)|Les bus et liaisons point à point (généralités)]]
* [[Fonctionnement d'un ordinateur/Les liaisons point à point|Les liaisons point à point]]
* [[Fonctionnement d'un ordinateur/Les bus électroniques|Les bus électroniques]]
* [[Fonctionnement d'un ordinateur/Quelques exemples de bus et de liaisons point à point|Quelques exemples de bus et de liaisons point à point]]
==Les mémoires==
* [[Fonctionnement d'un ordinateur/Les différents types de mémoires|Les différents types de mémoires]]
* [[Fonctionnement d'un ordinateur/L'interface d'une mémoire électronique|L'interface d'une mémoire électronique]]
===La micro-architecture d'une mémoire adressable===
* [[Fonctionnement d'un ordinateur/Les cellules mémoires|Les cellules mémoires]]
* [[Fonctionnement d'un ordinateur/Le plan mémoire|Le plan mémoire]]
* [[Fonctionnement d'un ordinateur/Contrôleur mémoire interne|Le contrôleur mémoire interne]]
* [[Fonctionnement d'un ordinateur/L'interface avec le bus mémoire|L'interface avec le bus mémoire]]
* [[Fonctionnement d'un ordinateur/Mémoires évoluées|Les mémoires évoluées]]
===Les mémoires primaires===
* [[Fonctionnement d'un ordinateur/Les mémoires ROM|Les mémoires ROM : Mask ROM, PROM, EPROM, EEPROM, Flash]]
* [[Fonctionnement d'un ordinateur/Les mémoires RAM asynchrones|Les mémoires DRAM asynchrones : FPM et EDO-RAM]]
* [[Fonctionnement d'un ordinateur/Les mémoires DRAM synchrones|Les mémoires DRAM synchrones : SDRAM et DDR]]
* [[Fonctionnement d'un ordinateur/Contrôleur mémoire externe|Le contrôleur mémoire externe]]
* [[Fonctionnement d'un ordinateur/Barrettes de mémoire|Les barrettes de mémoire]]
* [[Fonctionnement d'un ordinateur/Les mémoires associatives|Les mémoires associatives]]
* [[Fonctionnement d'un ordinateur/Les mémoires FIFO et LIFO|Les mémoires FIFO et LIFO]]
===Les mémoires de masse===
* [[Fonctionnement d'un ordinateur/Les disques durs|Les disques durs]]
* [[Fonctionnement d'un ordinateur/Les solid-state drives|Les solid-state drives]]
* [[Fonctionnement d'un ordinateur/Les technologies RAID|Les technologies RAID]]
* [[Fonctionnement d'un ordinateur/Les disques optiques|Les disques optiques]]
==Le processeur==
===L'architecture externe===
* [[Fonctionnement d'un ordinateur/Langage machine et assembleur|Langage machine et assembleur]]
* [[Fonctionnement d'un ordinateur/La pile d'appel et les fonctions|La pile d'appel et les fonctions]]
* [[Fonctionnement d'un ordinateur/Les registres du processeur|Les registres du processeur]]
* [[Fonctionnement d'un ordinateur/L'encodage des instructions|L'encodage des instructions]]
* [[Fonctionnement d'un ordinateur/Les jeux d'instructions|Les jeux d'instructions]]
* [[Fonctionnement d'un ordinateur/Le modèle mémoire : alignement et boutisme|Le modèle mémoire : alignement et boutisme]]
* [[Fonctionnement d'un ordinateur/Un exemple de jeu d'instruction : l'extension x87|Un exemple de jeu d'instruction : l'extension x87]]
===La micro-architecture===
* [[Fonctionnement d'un ordinateur/Les composants d'un processeur|Les composants d'un processeur]]
* [[Fonctionnement d'un ordinateur/Le chemin de données|Le chemin de données]]
* [[Fonctionnement d'un ordinateur/L'unité de contrôle|L'unité de contrôle]]
==Les entrées-sorties==
===La communication avec les entrées-sorties===
* [[Fonctionnement d'un ordinateur/Le contrôleur de périphériques|Le contrôleur de périphériques]]
* [[Fonctionnement d'un ordinateur/L'adressage des périphériques|L'adressage des périphériques]]
* [[Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques|Les méthodes de synchronisation entre processeur et périphériques]]
===Les périphériques et composants communs des PCs===
* [[Fonctionnement d'un ordinateur/La carte mère, chipset et BIOS|La carte mère, chipset et BIOS]]
* [[Fonctionnement d'un ordinateur/Les périphériques|Les périphériques]]
* [[Fonctionnement d'un ordinateur/Les cartes filles|Les cartes filles]]
* [[Fonctionnement d'un ordinateur/Le matériel réseau|Le matériel réseau]]
==La hiérarchie mémoire==
===La mémoire virtuelle===
* [[Fonctionnement d'un ordinateur/La mémoire virtuelle|La mémoire virtuelle]]
===La mémoire cache===
* [[Fonctionnement d'un ordinateur/Les mémoires cache|Les mémoires cache]]
* [[Fonctionnement d'un ordinateur/Le préchargement|Le préchargement]]
===Le ''Translation Lookaside Buffer''===
* [[Fonctionnement d'un ordinateur/Le Translation Lookaside Buffer|Le ''Translation Lookaside Buffer'']]
==Le parallélisme d’instructions==
* [[Fonctionnement d'un ordinateur/Le pipeline|Le pipeline]]
===Dépendances et pipeline===
* [[Fonctionnement d'un ordinateur/Interruptions et pipeline|Interruptions et pipeline]]
* [[Fonctionnement d'un ordinateur/Dépendances de contrôle|Les dépendances de contrôle]]
* [[Fonctionnement d'un ordinateur/Dépendances de données|Les dépendances de données]]
* [[Fonctionnement d'un ordinateur/Dépendances structurelles|Les dépendances structurelles]]
===Exécution dans le désordre===
* [[Fonctionnement d'un ordinateur/Exécution dans le désordre|L'exécution dans le désordre]]
* [[Fonctionnement d'un ordinateur/Fenêtres d’instruction et stations de réservation|Fenêtres d’instruction et stations de réservation]]
* [[Fonctionnement d'un ordinateur/Le renommage de registres|Le renommage de registres]]
* [[Fonctionnement d'un ordinateur/Désambigüisation de la mémoire|La désambigüisation de la mémoire]]
===Émission multiple===
* [[Fonctionnement d'un ordinateur/Processeurs à émissions multiples|Les processeurs à émissions multiples]]
==Les architectures parallèles==
* [[Fonctionnement d'un ordinateur/Les architectures parallèles|Les architectures parallèles]]
===Le parallélisme de tâches===
* [[Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs|Les architectures multiprocesseurs et multicœurs]]
* [[Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading|Les architectures multithreadées et Hyperthreading]]
* [[Fonctionnement d'un ordinateur/Architectures distribuées, NUMA et COMA|Les architectures distribuées, NUMA et COMA]]
===Le parallélisme de données===
* [[Fonctionnement d'un ordinateur/Les architectures à parallélisme de données|Les architectures à parallélisme de données]]
===Les architectures parallèles exotiques===
* [[Fonctionnement d'un ordinateur/Les architectures parallèles exotiques|Les architectures parallèles exotiques]]
==Les jeux d’instructions spécialisés pour la performance / une application==
* [[Fonctionnement d'un ordinateur/Les processeurs de traitement du signal|Les processeurs de traitement du signal]]
===Les architectures spécialisées pour le parallélisme===
* [[Fonctionnement d'un ordinateur/Les architectures actionnées par déplacement|Les architectures actionnées par déplacement]]
* [[Fonctionnement d'un ordinateur/Les architectures découplées|Les architectures découplées]]
* [[Fonctionnement d'un ordinateur/Les architectures dataflow|Les architectures dataflow]]
===Les architectures dédiées===
* [[Fonctionnement d'un ordinateur/Les architectures pour langages fonctionnels|Les architectures pour langages fonctionnels]]
* [[Fonctionnement d'un ordinateur/Les architectures à capacités|Les architectures à capacités]]
* [[Fonctionnement d'un ordinateur/Les architectures tolérantes aux pannes|Les architectures tolérantes aux pannes]]
* [[Fonctionnement d'un ordinateur/Les architectures neuromorphiques|Les architectures neuromorphiques]]
* [[Fonctionnement d'un ordinateur/Les architectures stochastiques|Les architectures stochastiques]]
==L'histoire de l'informatique==
* [[Fonctionnement d'un ordinateur/Les mémoires historiques|Les mémoires historiques]]
* [[Fonctionnement d'un ordinateur/Les premiers ordinateurs|Les premiers ordinateurs (en cours de rédaction)]]
==Annexes==
* [[Fonctionnement d'un ordinateur/Les circuits réversibles|Les circuits réversibles]]
{{autocat}}
aziir2liccctxqd8itjgdbhl1j612v0
Fonctionnement d'un ordinateur/Le plan mémoire
0
71475
682028
678910
2022-07-20T13:30:48Z
Mewtow
31375
/* L'égaliseur de tension */
wikitext
text/x-wiki
Avec le chapitre précédent, on sait que les RAM et ROM contiennent des cellules mémoires, qui mémorisent chacune un bit. On pourrait croire que cela suffit à créer une mémoire, mais il n'en est rien. Il faut aussi des circuits pour gérer l'adressage, le sens de transfert (lecture ou écriture), et bien d'autres choses. Schématiquement, on peut subdiviser toute mémoire en plusieurs circuits principaux.
* La mémorisation des informations est prise en charge par le '''plan mémoire'''. Il est composé d'un regroupement de cellules mémoires, auxquelles on a ajouté quelques fils pour communiquer avec le bus.
* La gestion de l'adressage et de la communication avec le bus sont assurées par un circuit spécialisé : le '''contrôleur mémoire''', composé d'un décodeur et de circuits de contrôle.
* L''''interface avec le bus''' relie le plan mémoire au bus de données. C'est le plus souvent ici qu'est géré le sens de transfert des données, ainsi que tout ce qui se rapporte aux lectures et écritures.
[[File:Td6bfig1.png|centre|vignette|upright=2|Organisation interne d'une mémoire adressable.]]
Nous allons étudier le plan mémoire dans ce chapitre, le contrôleur mémoire et l'interface avec le bus seront vu dans les deux chapitres suivants. Cela peut paraitre bizarre de dédier un chapitre complet au plan mémoire, mais il y a de quoi. Celui-ci n'est pas qu'un simple amoncellement de cellules mémoire et de connexions vaguement organisées. On y trouve aussi des circuits électroniques aux noms barbares : amplificateur de tension, égaliseur de ligne de bit, circuits de pré-charge, etc. L'organisation des fils dans le plan mémoire est aussi intéressante à étudier, celle-ci étant bien plus complexe qu'on peut le croire.
==Les fils et signaux reliés aux cellules==
[[File:Transparent Latch Symbol.svg|vignette|upright=0.5|Interface d'une bascule D.]]
Le plan mémoire est surtout composé de fils, sur lesquels on connecte des cellules mémoires. Rappelons que les cellules mémoires se présentent avec une interface simple, qui contient des broches pour le transfert des données et d'autres broches pour les commandes de lecture/écriture. Reste à voir comment toutes ses broches sont reliées aux différents bus et au contrôleur mémoire. Ce qui va nous amener à parler des lignes de bit et des signaux de sélection de ligne. Il faut préciser que la distinction entre broches de commande et de données est ici très importante : les broches de données sont connectées indirectement au bus, alors que les broches de commande sont reliées au contrôleur mémoire. Aussi, nous allons devoir parler des deux types de broches dans des sections séparées.
===La connexion des broches de données : les lignes de bit===
Afin de simplifier l'exposé, nous allons étudier une mémoire série dont le byte est de 1 bit. Une telle mémoire est dite '''bit-adressable''', c’est-à-dire que chaque bit de la mémoire a sa propre adresse. Nous étudierons le cas d'une mémoire quelconque plus loin, et ce pour une raison : on peut construire une mémoire quelconque en améliorant le plan mémoire d'une mémoire bit-adressable, d'une manière assez simple qui plus est. Parler de ces dernières est donc un bon marche-pied pour aboutir au cas général.
====Le cas d'une mémoire bit-adressable====
Une mémoire bit-adressable est de loin celle qui a le plan mémoire le plus rudimentaire. Quand on sélectionne un bit, avec son adresse, son contenu va se retrouver sur le bus de données. Dit autrement, la cellule mémoire va se connecter sur ce fils pour y placer son contenu. On devine donc comment est organisé le plan mémoire : il est composé d'un fil directement relié au bus de donnée, sur lequel les cellules mémoire se connectent si besoin. Le plan mémoire se résume donc à un ensemble de cellules mémoires dont l'entrée/sortie est connectée à un unique fil. Ce fil s'appelle la '''ligne de bit''' (''bitline'' en anglais).
[[File:Plan mémoire simplifié d'une mémoire bit-adressable.png|centre|vignette|upright=1|Plan mémoire simplifié d'une mémoire bit-adressable.]]
En réalité, peu de mémoires suivent actuellement le principe précédent. Les mémoires assez évoluées utilisent deux lignes de bit par colonne ! La première transmet le bit lu et l'autre son inverse, ce qui se marie bien avec le fait que certaines bascules fournissent le bit et son inverse sur deux broches distinctes. La mémoire utilise la différence de tension entre ces deux fils pour représenter le bit lu ou à écrire. Un tel codage est appelé un '''codage différentiel'''. L'utilité d'un tel codage assez difficile à expliquer sans faire intervenir des connaissances en électricité, mais tout est une histoire de fiabilité et de résistance aux parasites électriques.
[[File:Bitlines différentielles.png|centre|vignette|upright=1|Bitlines différentielles.]]
Certaines mémoires ont amélioré les lignes de bit différentielles en interchangeant leur place à chaque cellule mémoire. La ligne de bit change donc de côté à chaque passage d'une cellule mémoire. Cette organisation porte le nom de '''lignes de bit croisées'''.
[[File:Bitlines croisées.png|centre|vignette|upright=1|Bitlines croisées.]]
Les lignes de bit différentielles se marient assez mal avec les cellules mémoire de DRAM, qui n'ont pas de seconde sortie pour lire l'inverse du bit stocké. Malgré tout, l'usage de lignes de bit différentielle est possible, bien que compliqué, grâce à des techniques comme l'usage de cellules factices (nous en reparlerons plus bas). Cependant, il est possible d'utiliser une organisation intermédiaire entre des lignes de bit simple et des lignes différentielles, qui connecte des cellules consécutives comme illustré ci-dessous. On voit qu'il y a deux lignes de bit : la moitié des cellules est connectée à la première ligne, l'autre moitié à la seconde, avec une alternance entre cellules consécutives. Cela permet d'avoir moins de cellules mémoires connectées sur le même fil, ce qui améliore certains paramètres électriques des lignes de bit. Cette organisation porte le nom de '''ligne de bit repliée'''.
[[File:Lignes de bit repliées.png|centre|vignette|upright=1|Lignes de bit repliées]]
====Le cas d'une mémoire quelconque (avec byte > 1)====
Après avoir vu le cas des mémoires bit-adressables, il est temps d'étudier les mémoires quelconques, celles où un byte contient plus que 1 bit. Surprenamment, ces mémoires peuvent être conçues en utilisant plusieurs mémoires bit-adressables. Par exemple, prenons une mémoire dont le byte fait deux bits (ce qui est rare, convenons-en). On peut l'émuler à partir de deux mémoires de 1 bit : la première stocke le bit de poids faible de chaque byte, alors que l'autre stock le bit de poids fort. Et on peut élargir le raisonnement pour des bytes de 3, 4, 8, 16 bits, et autres. Par exemple, pour une mémoire dont le byte fait 64 bits, il suffit de mettre en parallèle 64 mémoires de 1 bit.
Mais cette technique n'est pas appliquée à la lettre, car il y a moyen d'optimiser le tout. En effet, on ne va pas mettre effectivement plusieurs mémoires bit-adressables en parallèle, car seuls les plans mémoires doivent être dupliqués. Si on utilisait effectivement plusieurs mémoires, chacune aurait son propre plan mémoire, mais aussi son propre contrôleur mémoire, ses propres circuits de communication avec le bus, etc. Or, ces circuits sont en fait redondants dans le cas qui nous intéresse.
Prenons le cas du contrôleur mémoire, qui reçoit l'adresse à lire/écrire et qui envoie les signaux de commande au plan mémoire. Avec N mémoires en parallèle, N contrôleurs mémoire recevront l'adresse et généreront les N mêmes signaux, qui seront envoyés à N plans mémoire distincts. Au lieu de cela, il est préférable d'utiliser un seul contrôleur mémoire, mais de dupliquer les signaux de commande en autant N exemplaires (autant qu'il y a de plan mémoire). Et c'est ainsi que sont conçues les mémoires quelconques : pour un byte de N bits, il faut prendre N plans mémoires de 1 bit. Cela demande donc d'utiliser N lignes de bits, reliée convenablement aux cellules mémoires. Le résultat est un rectangle de cellules mémoires, où chaque colonne est traversée par une ligne de bit. Chaque ligne du tableau/rectangle, correspond à un byte, c'est-à-dire une case mémoire.
Là encore, chaque colonne peut utiliser des lignes de bits différentielles ou croisées.
[[File:Plan mémoire, avec les bitlines.png|centre|vignette|upright=1.5|Plan mémoire, avec les bitlines.]]
===La connexion des broches de commande : le transistor et le signal de sélection===
Évidemment, les cellules mémoires ne doivent pas envoyer leur contenu sur la ligne de bit en permanence. En réalité, chaque cellule est connectée sur la ligne de bit selon les besoins. Les cellules correspondant au mot adressé se connectent sur la ligne de bit, alors que les autres ne doivent pas le faire. La connexion des cellules mémoire à la ligne de bit est réalisée par un interrupteur commandable, c’est-à-dire par un transistor appelé '''transistor de sélection'''. Quand la cellule mémoire est sélectionnée, le transistor se ferme, ce qui connecte la cellule mémoire à la ligne de bit. À l'inverse, quand une cellule mémoire n'est pas sélectionnée, le transistor de sélection se comporte comme un interrupteur ouvert : la cellule mémoire est déconnectée du bus.
La commande du transistor de sélection est effectuée par le contrôleur mémoire. Pour chaque ligne de bit, le contrôleur mémoire n'ouvre qu'un seul transistor à la fois (celui qui correspond à l'adresse voulue) et ferme tous les autres. La correspondance entre un transistor de sélection et l'adresse est réalisée dans le contrôleur mémoire, par des moyens que nous étudierons dans les prochains chapitres. Toujours est-il que le contrôleur mémoire génère, pour chaque octet, un bit qui dit si celui-ci est adressé ou non. Ce bit est appelé le '''signal de sélection'''. Le signal de sélection est envoyé à toutes les cellules mémoire qui correspondent au byte adressé. Vu que tous les bits d'un byte sont lus ou écrits en même temps, toutes les cellules correspondantes doivent être connectées à la ligne de bit en même temps, et donc tous les transistors de sélection associés doivent se fermer en même temps. En clair, le signal de sélection est partagé par toutes les cellules d'un même mot mémoire.
[[File:Signal row line.png|centre|vignette|upright=2.5|Signal de sélection et Byte.]]
====Le cas des lignes de bit simples et repliées====
Voyons comment les bitlines simples sont reliées aux cellules mémoires. Les mémoires 1T-DRAM n'ont qu'une seule broche entrée/sortie, sur laquelle on effectue à la fois les lectures et les écritures. Cela se marie très bien avec des bitlines simples, mais ça les rend incompatibles avec des bitlines différentielles. Le cas des DRAM à bitlines simples, avec une seule sortie, un seul transistor de sélection, est illustré ci-dessous. On peut noter que les cellules de SRAM peuvent malgré tout s’accommoder de bitlines simples : il suffit de connecter la sortie Q sur la bitline simple et ne pas connecter la sortie <math>\overline{Q}</math> à quoi que ce soit. On peut même en profiter pour supprimer le transistor de sélection de cette sortie, ce qui réduit le nombre de transistors à seulement 5.
[[File:Plan mémoire d'une mémoire bit-adressable.png|centre|vignette|upright=1.5|Plan mémoire d'une mémoire bit-adressable.]]
La connexion des transistors de sélection pour des lignes de bit repliée n’est pas très différente de celle des lignes de bit simple. Elle est illustrée ci-dessous.
[[File:Ligne de bit repliée.png|centre|vignette|upright=1.5|Ligne de bit repliée.]]
====Le cas des lignes de bit différentielles====
Le cas des mémoires SRAM est de loin le plus simple à comprendre. Celles-ci utilisent toutes (ou presque) des bitlines différentielles, chose qui se marie très bien avec l'interface des cellules SRAM. Rappelons que celle-ci possèdent deux broches pour les données : une broche Q sur laquelle on peut lire ou écrire un bit, et une broche complémentaire sur laquelle on récupère l'inverse du bit lu. À chaque broche correspond un transistor de sélection différents. Dans ce cas, la sortie Q est connectée sur une bitline, alors que l'autre sortie complémentaire <math>\overline{Q}</math> l'est sur l'autre bitline.
[[File:Connexion d'une cellule mémoire de SRAM à une bitline différentielle.png|centre|vignette|upright=2|Connexion d'une cellule mémoire de SRAM à une bitline différentielle.]]
Précisons que les DRAM se marient assez mal avec l'usage de lignes de bit différentielle, vu qu'elles n'ont pas de sortie complémentaire <math>\overline{Q}</math>, mais n'ont qu'une seule sortie Q. Mais quelques astuces permettent d'utiliser des lignes de bit différentielles sur ces mémoires. La plus connue est de loin l'utilisation de '''cellules factices''' (''dummy cells''), des cellules mémoires vides placées aux bouts des lignes de bit. Lors d'une lecture, ces cellules vides se remplissent avec l'inverse du bit à lire. La ligne de bit inverse (celle qui contient l'inverse du bit) est alors remplie avec le contenu de la cellule factice, ce qui donne bien un signal différentiel. Le bit inversé est fournit par une porte logique qui inverse la tension fournie par la cellule mémoire. Cette tension remplis alors la cellule factice, avec l'inverse du bit lu.
====Le cas des cellules mémoires double port====
Après avoir vu les cellules mémoire "normales" plus haut, il est temps de passer aux cellules mémoire de type double port. Pour simplifier, les cellules double port possèdent une sortie pour les lectures et une entrée pour les écritures, toutes deux d'un bit. On peut les utiliser pour concevoir des mémoires double port, mais aussi des mémoires simple port.
Cette particularité est exploitée pour créer des mémoires double-port bit-adressables, qui ont une broche pour les lectures et une autre pour les écritures. Le plan mémoire contient alors deux lignes de bit, une pour la broche de lecture et une autre pour la broche d'écriture. Le transistor de lecture est connecté à la ligne de bit de lecture, alors que celui pour l'écriture est relié à la ligne de bit d'écriture.
[[File:Plan mémoire d'une SRAM double port.png|centre|vignette|upright=2|Plan mémoire d'une SRAM double port.]]
Pour les mémoires simple port, c'est-à-dire avec une seule broche qui sert à la fois pour les lectures et écritures, les deux transistors sont reliés à la même ligne de bit. Ils vont s'ouvrir ou se fermer selon les besoins, sous commande du contrôleur mémoire.
[[File:Plan mémoire d'une SRAM simple port.png|centre|vignette|upright=2|Plan mémoire d'une SRAM simple port.]]
Dans les deux cas, le contrôleur mémoire est relié directement aux transistors de sélection. Il doit générer à la fois les signaux d'autorisation de lecture que ceux pour l'écriture. Ces deux signaux peuvent être déduit du bit de sélection et du bit R/W, comme vu dans le chapitre précédent.
[[File:Circuit d'interface entre contrôleur mémoire et cellule mémoire.png|centre|vignette|upright=1.5|Circuit d'interface entre contrôleur mémoire et cellule mémoire.]]
==L'amplificateur de tension==
Quand on connecte une cellule mémoire à une ligne de bit, c'est à la cellule mémoire de fournir le courant pour mettre la ligne à 0 ou à 1. Mais dans certains cas, la cellule mémoire ne peut pas fournir assez courant pour cela. Cela arrive surtout sur les mémoires DRAM, basées sur un condensateur. Ces condensateurs ont une faible capacité et ne peuvent pas conserver beaucoup d'électrons, surtout sur les mémoires modernes. Du fait de la miniaturisation, les condensateurs des DRAM actuelles ne peuvent stocker que quelques centaines d'électrons, parfois beaucoup moins. Autant dire que la vidange du condensateur dans la ligne de bit ne suffit pas à la mettre à 1, même si la cellule mémorisait bien un 1. La lecture crée à peine une tension de quelques millivolts dans la ligne de bit, pas plus. Mais il y a une solution : amplifier la tension de quelques millivolts induite par la vidange du condensateur sur le bus. Pour cela, il faut donc placer un dispositif capable d'amplifier cette tension, bien nommé '''amplificateur de lecture'''.
[[File:Differential amplifier.svg|vignette|Amplificateur différentiel.]]
L’amplificateur utilisé n'est pas le même avec des lignes de bit simples et des lignes de bit différentielles. Dans le cas différentiel, l'amplificateur doit faire la différence entre les tensions sur les deux lignes de bit et traduire cela en un niveau logique. C'est l'amplificateur lui-même qui fait la conversion entre codage différentiel (sur deux lignes de bit) et codage binaire. Pour le distinguer des autres amplificateurs, il porte le nom d''''amplificateur différentiel'''. L'amplificateur différentiel possède deux entrées, une pour chaque ligne de bit, et une sortie. Dans ce qui va suivre, les entrées seront notées <math>V_{in}^+</math> et <math>V_{in}^-</math>, la sortie sera notée <math>V_{out}</math>. L’amplificateur différentiel fait la différence entre ces deux entrées et amplifie celle-ci. En clair :
: <math>V_{out} = A \times ( V_{in}^+ - V_{in}^- )</math>
Il faut noter qu'un amplificateur différentiel peut fonctionner aussi bien avec des lignes de bit différentielles qu'avec des lignes de bit simples. Avec des lignes de bit simples, il suffit de placer l'autre entrée à la masse, au 0 Volts, et de n'utiliser qu'une seule sortie.
Il existe de nombreuses manières de concevoir un amplificateur différentiel, mais nous n'allons aborder que les circuits les plus simples. Dans les grandes lignes, il existe deux types d'amplificateurs de lecture : ceux basés sur des bascules et ceux basés sur une paire différentielle. Bizarrement, vous verrez que les deux ont une certaine ressemblance avec les cellules de SRAM ! Il faut dire qu'une porte NON, fabriquée avec des transistors, est en réalité un petit amplificateur spécialisé, chose qui tient au fonctionnement de son circuit.
===L'amplificateur de lecture à paire différentielle===
[[File:Long tailed pair.svg|vignette|Paire différentielle. Le générateur de courant est en jaune, la charge est en bleu. Ici, la charge est un miroir de courant. Les transistors ne sont pas des transistors MOS, mais le circuit fonctionne de la même manière que si c'était le cas.]]
Le premier type d'amplificateur différentiel est la '''paire différentielle''', composée de deux transistors mis en série avec une charge et un générateur de courant. La charge est placée entre la tension d'alimentation et le transistor, alors que le générateur de courant est placé entre le transistor et la tension basse (la masse, ou l'opposé de la tension d'alimentation, selon le montage). Le circuit ci-contre illustre le circuit de la paire différentielle.
Précisons que la charge mentionnée précédemment varie selon le circuit, de même que le générateur de courant. Dans le cas le plus simple, une simple résistance suffit pour les deux. Mais ce n'est pas cette solution qui est utilisée dans les mémoires actuelles. En effet, intégrer des résistances est compliqué dans les circuits à semi-conducteurs modernes, et les mémoires RAM en sont. Si on peut y intégrer des résistances, des condensateurs ou des inductances/bobines, c'est une chose très complexe et qui ne vaut généralement pas le coup. Aussi, les résistances et condensateurs sont généralement remplacés par des circuits équivalents, qui ont le même rôle ou qui peuvent remplacer une résistance dans le montage voulu.
[[File:Differential amplifier long-tailed pair.svg|centre|vignette|Paire différentielle, avec des résistances.]]
Le générateur de courant et la charge doivent être fabriqués avec des transistors MOS, voire CMOS, ce qui n'est pas un problème. Chacun de ces circuits est remplacée par un ''miroir de courant'', à savoir un circuit qui crée un courant constant sur une sortie et le recopie sur une seconde sortie. L'avantage est que le miroir de courant fournit le même courant aux deux ''bitlines'', il égalise les courants dans les deux bitlines. Il existe plusieurs manières de créer un tel miroir de courant avec des transistors MOS/CMOS, la plus simple étant illustrée ci-dessous. On pourrait aborder le fonctionnement d'un tel circuit, pourquoi il fonctionne, mais nous n'en parlerons pas ici. Cela relèverait plus d'un cours d'électronique analogique, et demanderait de connaître en détail le fonctionnement d'un transistor, les équations associées, etc.
[[File:Einfacher Stromspiegel MOSFET1.svg|centre|vignette|Miroir de courant fabriqué avec des transistors MOS.]]
===L'amplificateur de lecture à verrou===
Le second type d'amplificateur de lecture est l''''amplificateur à verrou'''. Il amplifie une différence de tension entre les deux lignes de bit d'une colonne différentielle. Les deux colonnes doivent être préchargées à Vdd/2, à savoir la moitié de la tension d'alimentation. La raison à cela deviendra évidente dans les explications qui vont suivre. Toujours est-il que ce circuit a besoin qu'un circuit dit de précharge s'occupe de placer la tension adéquate sur les lignes de bit, avant toute lecture ou écriture. Nous reparlerons de ce circuit de précharge dans les sections suivantes, vers la fin de ce chapitre. Cela peu paraître peu pédagogique, mais à notre décharge, sachez que le circuit de précharge et l'amplificateur de lecture sont intimement liés. Il est difficile de parler de l'un sans parler de l'autre et réciproquement. Pour le moment, tout ce que vous avez à retenir est qu'avant toute lecture, les lignes de bit sont chargées à Vdd/2, ce qui permet à l'amplificateur à verrou de fonctionner correctement.
====Le circuit de l'amplificateur de lecture à verrou====
L'amplificateur à verrou est composé de deux portes NON reliées tête-bêche, comme dans une cellule de SRAM. Chaque ligne de bit est reliée à l'autre à travers une porte NON. Sauf que cette fois-ci, il n'y a pas toujours de transistors de sélection, ou alors ceux-ci sont placés autrement.
[[File:Latch-type sense amplifier.png|centre|vignette|upright=1|Amplificateur de lecture à bascule.]]
Le circuit complet est illustré ci-dessous, de même qu'une version plus détaillée avec des transistors. Du fait de son câblage, l'amplificateur à verrou a pour particularité d'avoir des broches d'entrées qui se confondent avec celles de sortie : l'entrée et la sortie pour une ligne de bit sont fusionnées en une seule broche. L'utilisation d'inverseurs explique intuitivement pourquoi il faut précharger les lignes de bit à Vdd/2 : cela place la tension dans la zone de sécurité des deux inverseurs, là où la tension ne correspond ni à un 0, ni à un 1. Le fonctionnement du circuit dépend donc du fonctionnement des transistors, qui servent alors d'amplificateurs.
[[File:Amplificateur de lecture à bascule, version détaillée.png|centre|vignette|upright=2|Amplificateur de lecture à bascule, version détaillée.]]
On peut noter que cet amplificateur est parfois fabriqué avec des transistors bipolaires, qui consomment beaucoup de courant. Mais même avec des transistors MOS, il est préférable de réduire la consommation électrique du circuit, quand bien même ceux-ci consomment peu. Pour cela, on peut désactiver l’amplificateur quand on ne l'utilise pas. Pour cela, on entoure l'amplificateur avec des transistors qui le débranchent, le déconnectent si besoin.
[[File:Amplificateur de lecture à bascule, avec transistors d'activation.png|centre|vignette|upright=2|Amplificateur de lecture à bascule, avec transistors d'activation.]]
====Le fonctionnement de l'amplificateur à verrou====
Expliquer en détail le fonctionnement de l'amplificateur à verrou demanderait de faire de l'électronique assez poussée. Il nous faudrait détailler le fonctionnement d'un transistor quand il est utilisé en tant qu'amplificateur, donner des équations, et bien d'autres joyeusetés. À la place, je vais donner une explication très simplifiée, que certains pourraient considérer comme fausse (ce qui est vrai, dans une certaine mesure).
Avant toute chose, précisons que les seuils pour coder un 0 ou un 1 ne sont pas les mêmes entre l’entrée d'une porte NON et sa sortie. Ils sont beaucoup plus resserrés sur l'entrée, la marge de sécurité entre 1 et 0 étant plus faible. Un signal qui ne correspondrait pas à un 0 ou un 1 en sortie peut l'être en entrée.
Le fonctionnement du circuit ne peut s'expliquer correctement qu'à partir du rapport entre tension à l'entrée et tension de sortie d'une porte NON. Le schéma ci-dessous illustre cette relation. On voit que la porte logique amplifie le signal d'entrée en plus de l'inverser. Pour caricaturer, on peut décomposer cette caractéristique en trois parties : deux zones dites de saturation et une zone d'amplification. Dans la zone de saturation, la tension est approximativement égale à la tension maximale ou minimale, ce qui fait qu'elle code pour un 0 ou un 1. Entre ces deux zones extrêmes, la tension de sortie dépend linéairement de la tension d'entrée (si on omet l'inversion).
[[File:Caractéristique tension d'entrée-tension de sortie d'un inverseru CMOS.png|centre|vignette|upright=2|Caractéristique tension d'entrée-tension de sortie d'un inverseur CMOS.]]
Quand on place deux portes NON l'une à la suite de l'autre, le résultat est un circuit amplificateur, dont la caractéristique est illustrée dans le second schéma. On voit que l'amplificateur amplifie la différence de tension entre VDD/2 et la tension d'entrée (sur la ligne de bit).
[[File:Utilisation de deux portes NON comme amplificateur de tension.png|centre|vignette|upright=2|Utilisation de deux portes NON comme amplificateur de tension.]]
Si on regarde le circuit complet, on s’aperçoit que chaque ligne de bit est bouclée sur elle-même, à travers cet amplificateur. Cela fait boucler la sortie de l'amplificateur sur son entrée : la tension de base est alors amplifiée une fois, puis encore amplifiée, et ainsi de suite. Au final, les seuls points stables du montage sont la tension maximale ou la tension minimale, soit un 0 ou un 1, ou la tension VDD/2.
Ceci étant dit, on peut enfin comprendre le fonctionnement complet du circuit d'amplification. Commençons l'explication par la situation initiale : la ligne de bit est préchargée à VDD/2, et la cellule mémoire est déconnectée des lignes de bit. La ligne de bit est préchargée à VDD/2, l'amplificateur a sa sortie comme son entrée égales à VDD/2 et le circuit est parfaitement stable. Ensuite, la cellule mémoire à lire est connectée à la ligne de bit et la tension va passer au-dessous ou au-dessus de VDD/2. Nous allons supposer que celle-ci contenait un 1, ce qui fait que sa connexion entraîne une montée de la tension de la ligne de bit. La tension ne va cependant pas monter de beaucoup, mais seulement de quelques millivolts. Cette différence de tension va être amplifiée par les deux portes logiques, ce qui amplifie la différence de tension. Et rebelote : cette différence amplifiée est ré-amplifiée par le montage, et ainsi de suite jusqu’à ce que le circuit se stabilise soit à 0 soit à 1.
[[File:Fonctionnement très simplifié de l'amplificateur à verrou.png|centre|vignette|upright=2|Fonctionnement très simplifié de l'amplificateur à verrou.]]
==L'optimisation du temps de charge/décharge des lignes de bit==
Si les lignes de bit sont de simples fils conducteurs passifs, cela ne veut pas dire qu'ils n'ont pas d'influence sur les lectures et écritures. En réalité, ils jouent un grand rôle dans la rapidité des accès mémoire, pour des raisons techniques. Selon leur longueur, la tension va prendre plus ou moins de temps pour s'établir dans la ligne de bit, ce qui impacte directement les performances de la mémoire. Diverses techniques ont étés inventées pour résoudre ce problème, la plus importante étant l'utilisation d'un circuit dit de pré-charge, que nous allons étudier maintenant.
Les lignes de bit ne sont pas des fils parfaits : non seulement ils ont une résistance électrique, mais ils se comportent aussi comme des condensateurs (dans une certaine mesure). Nous n'expliquerons pas dans la physique de ce phénomène, mais allons simplement admettre qu'un fil électrique se modélise bien en mettant une résistance R en série avec un condensateur C : le circuit obtenu est un circuit RC. Le condensateur, appelée '''capacité parasite''', n’apparaît que lorsque la tension de la ligne de bit change en passant de 0 à 1 ou inversement. Ce qui n'arrive que lors d'une lecture ou écriture, cela va de soit. Lorsque l'on change la tension en entrée d'un tel montage, la tension de sortie met un certain temps avant d'atteindre la valeur d'entrée. Ce qui est illustré dans les deux schémas ci-dessous, pour la charge (passage de 0 à 1) et la décharge (passage de 1 à 0). La variation est d'ailleurs exponentielle. On estime qu'il faut un temps égal <math>t \approx 3 \times R \cdot C</math>, avec R la valeur de la résistance et C celle du condensateur. En clair : la ligne de bit met un certain temps avant que la tension atteigne celle qui correspond au bit lu ou à écrire.
{|class="wikitable flexible"
|[[File:RC Series Filter (with V&I Labels).svg|300px|class=transparent|Circuit RC série.]]
|[[File:Series RC capacitor voltage.svg|300px|class=transparent|Tension aux bornes d'un circuit RC en charge.]]
|[[File:Series RC resistor voltage.svg|300px|class=transparent|Tension aux bornes d'un circuit RC en cours de décharge.]]
|}
===L'organisation du plan mémoire===
Une première idée pour optimiser le temps RC est de diminuer la résistance de la ligne. Il se trouve que celle-ci est proportionnelle à la longueur de la ligne de bit : plus la ligne de bit est longue, plus la résistance R sera élevée. On voit donc une première solution pour réduire la résistance, et donc le temps RC : réduire la taille des lignes de bit. Les petites mémoires, avec peu de cellules sur une colonne, ont des lignes de bit plus petites et sont donc plus rapides. Cela explique en partie pourquoi les temps d'accès des mémoires varient selon la capacité, chose que nous avons abordé il y a quelques chapitres. De même, à capacité égale, il vaut mieux utiliser des bytes large, pour réduire la taille des colonnes.
====L'agencement en colonne de donnée ouvertes====
Mais d'autres optimisations du plan mémoire permettent d'obtenir des lignes de bit plus petites, à capacité et largeur de byte inchangée. Par exemple, on peut placer l'amplificateur de lecture au milieu du plan mémoire, et non au bout. En faisant ainsi, on doit couper la ligne de bit en deux, chaque moitié étant placée d'un côté ou de l'autre de l’amplificateur. La colonne contient ainsi deux lignes de bits séparées, chacune ayant une longueur réduite de moitié. Cette organisation est dite '''en colonne de donnée ouvertes'''. Mais cette organisation a un défaut : il est difficile d'implémenter l'amplificateur au milieu de la mémoire. Le nombre de fils qui doivent passer par le milieu de la RAM est important, rendant le câblage compliqué. De plus, les perturbations électromagnétiques ne touchent pas de la même manière chaque côté de la mémoire et l'amplificateur peut donner des résultats problématiques à cause d'elles.
[[File:Optimisations du plan mémoire pour réduire la taille des bitlines.png|centre|vignette|upright=2.5|Optimisations du plan mémoire pour réduire la taille des bitlines.]]
Il est aussi possible de répartir les amplificateurs de tension autrement. On peut mélanger les organisations en colonne de données ouvertes et "normales", en mettant les amplificateurs à la fois au milieu de la RAM et sur les bords. Une moitié des amplificateurs est placée au milieu du plan mémoire, l'autre moitié est placée sur les bords. On alterne les lignes de bits connectée entre amplificateurs selon qu’ils sont sur les bords ou au milieu. L'organisation est illustrée ci-dessous.
[[File:Organisation en colonnes de données ouvertes, avec répartition des amplificateur sur les bords du plan mémoire.png|centre|vignette|upright=2.5|Organisation en colonnes de données ouvertes, avec répartition des amplificateur sur les bords du plan mémoire.]]
===La pré-charge des lignes de bit===
[[File:Sense Amp position.jpg|vignette|Aperçu d'une ligne de bit conçue pour être préchargée. On voit qu'il s'agit d'une ligne de bit "normale", à laquelle a été ajouté un circuit qui permet de charger la ligne à partir de la tension d'alimentation. L'amplificateur de tension est situé du côté opposé au circuit de charge.]]
Une autre solution, beaucoup plus ingénieuse, ne demande pas de modifier la longueur des lignes de bit. À la place, on rend leur charge plus rapide en les pré-chargeant. Sans pré-charge, la ligne de bit est à 0 Volts avant la lecture et la lecture altère cette tension, que ce soit pour la laisser à 0 (lecture d'un 0), ou pour la faire monter à la tension maximale Vdd (lecture d'un 1). Le temps de réaction de la ligne de bit dépend alors du temps qu'il faut pour la faire monter à Vdd. Avec la pré-charge, la ligne de bit est chargée avant la lecture, de manière à la mettre à la moitié de Vdd. La lecture du bit fera descendre celle-ci à 0 (lecture d'un 0) ou la faire grimper à Vdd (lecture d'un 1). Le temps de charge ou de décharge est alors beaucoup plus faible, vu qu'on part du milieu.
: Il faut noter que la pré-charge à Vdd/2 est un cas certes simple à comprendre, mais qui n'a pas valeur de généralité. Certaines mémoires pré-chargent leurs lignes de bit à une autre valeur, qui peut être Vdd, à 60% de celui-ci, ou une autre valeur. En fait, tout dépend de la technologie utilisée. Par exemple, Les mémoires de type CMOS pré-chargent à Vdd/2, alors que les mémoires TTL, NMOS ou PMOS pré-chargent à une autre valeur (le plus souvent Vdd).
On peut penser qu'il faudra deux fois moins de temps, mais la réalité est plus complexe (regardez les graphes de charge/décharge situés plus haut). De plus, il faut ajouter le temps mis pour précharger la ligne de bit, qui est à ajouter au temps de lecture proprement dit. Sur la plupart des mémoires, la pré-charge n'est pas problématique. Il faut dire qu'il est rare que la mémoire soit accédé en permanence et il y a toujours quelques temps morts pour pré-charger la ligne de bit. On verra que c'est notamment le cas sur les mémoires DRAM synchrones modernes, comme les SDRAM et les mémoires DDR. Mais passons...
====Les circuits de précharge====
La pré-charge d'une ligne de bit se fait assez facilement : il suffit de connecter la ligne de bit à une source de tension qui a la valeur adéquate. Par exemple, une mémoire qui se pré-charge à Vdd a juste à relier la ligne de bit à la tension d'alimentation. Mais attention : cette connexion doit disparaître quand on lit ou écrit un bit dans les cellules mémoire. Sans cela, le bit envoyé sur la ligne de bit sera perturbé par la tension ajoutée. Il faut donc déconnecter la ligne de bit de la source d'alimentation lors d'une lecture écriture. On devine rapidement que le circuit de pré-charge est composé d'un simple interrupteur commandable, placé entre la tension d'alimentation (Vdd ou Vdd/2) et la ligne de bit. Le contrôleur mémoire commande cet interrupteur pour précharger la ligne de bit ou stopper la pré-charge lors d'un accès mémoire. Si un seul transistor suffit pour les lignes de bit simples, deux sont nécessaires pour les lignes de bit différentielles ou croisées. Ils doivent être ouvert et fermés en même temps, ce qui fait qu'ils sont commandés par un même signal.
[[File:Circuits de précharge.png|centre|vignette|upright=2.5|Circuits de précharge]]
====L'égaliseur de tension====
Pour les lignes de bit différentielles et croisées, il se peut que les deux lignes de bit complémentaires n'aient pas tout à fait la même tension suite à la pré-charge. Pour éviter cela, il est préférable d'ajouter un '''circuit d'égalisation''' qui égalise la tension sur les deux lignes. Celui-ci est assez simple : c'est un interrupteur commandable qui connecte les deux lignes de bit lors de la pré-charge. Là encore, un simple transistor suffit. L'égalisation et la pré-charge ayant lieu en même temps, ce transistor est commandé par le même signal que celui qui active le circuit de précharge. Le circuit complet, qui fait à la fois pré-charge et égalisation des tensions, est représenté ci-dessous.
[[File:Circuits de précharge et d'égalisation pour des lignes de bit différentielles.png|centre|vignette|upright=2.5|Circuits de précharge et d'égalisation pour des lignes de bit différentielles.]]
==Annexe : l'attaque ''rowhammer''==
Vous connaissez maintenant comment fonctionnent les cellules mémoires et le plan mémoire, ce qui fait que vous avez les armes nécessaires pour aborder des sujets assez originaux. Profitons-en pour aborder une faille de sécurité présente dans la plupart des mémoires DRAM actuelles : l'attaque ''row hammer''. Vous avez bien entendu : il s'agit d'une faille de sécurité matérielle, qui implique les mémoires RAM, qui plus est. Voilà qui est bien étrange. D'ordinaire, quand on parle de sécurité informatique, on parle surtout de failles logicielles ou de problèmes d'interface chaise-clavier. La plupart des attaques informatiques sont des attaques d’ingénierie sociale où on profite de failles humaines pour obtenir un mot de passe ou toute autre information confidentielle, suivies par les failles logicielles, les virus, malwares et autres méthodes purement logicielles. Mais certaines failles de sécurités sont purement matérielles et profitent de bugs présents dans le matériel pour fonctionner. Car oui, les processeurs, mémoires, bus et périphériques peuvent avoir des bugs matériels qui sont généralement bénins, mais que des virus, logiciels ou autres malware peuvent exploiter pour commettre leur méfaits.
L'attaque ''row hammer'', aussi appelée attaque par '''martèlement de mémoire''', utilise un bug de conception des mémoires DRAM. Le bug en question tient dans le fait que les cellules mémoires ne sont pas parfaites et que leur charge électrique tend à fuir. Ces fuites de courant se dispersent autour de la cellule mémoire et tendent à affecter les cellules mémoires voisines. En temps normal, cela ne pose aucun problème : les fuites sont limitées et l'interaction électrique est limitée. Pas de quoi changer le contenu des cellules mémoires voisines. Cependant, des hackers ont réussit à exploiter ce comportement pour copier le contenu d'une cellule mémoire dans une autre. En accédant d'une manière bien précise à une cellule mémoire, on peut garantir que les fuites de courant deviennent signifiantes, suffisamment pour recopier le contenu d'une cellule mémoire dans les cellules mémoires voisines. Pour cela, il faut accèder un très grand nombre de fois à la cellule mémoire en question, ce qui explique pourquoi cette attaque s'appelle le ''martélement'' de mémoire.
L'intérêt est de contourner les protections mémoires liées au système d'exploitation. Sur les systèmes d'exploitation modernes, chaque programme se voit attribuer certaines portions de la mémoire, auxquelles il est le seul à pouvoir accéder. Des mécanismes de protection mémoire intégré dans le processeur permettent d'isoler la mémoire de chaque programme, comme nous le verrons dans le chapitre sur la mémoire virtuelle. Mais avec ''row hammer'', les accès à un byte attribué à un programme peuvent déborder sur les bytes d'un autre programme, avec des conséquences assez variables. Par exemple, un virus présent en mémoire pourrait interagir avec le byte qui mémorise un mot de passe ou une clé de sécurité RSA, ou toute donnée confidentielle. Il pourrait récupérer cette information, ou alors la modifier pour la remplacer par une valeur connue et l'attaquant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les cellules mémoires
| prevText=Les cellules mémoires
| next=Contrôleur mémoire interne
| nextText=Le contrôleur mémoire interne
}}
</noinclude>
ap875pbpwjw4sywj6rpzjwl7akcrgot
682030
682028
2022-07-20T13:37:50Z
Mewtow
31375
/* Annexe : l'attaque rowhammer */
wikitext
text/x-wiki
Avec le chapitre précédent, on sait que les RAM et ROM contiennent des cellules mémoires, qui mémorisent chacune un bit. On pourrait croire que cela suffit à créer une mémoire, mais il n'en est rien. Il faut aussi des circuits pour gérer l'adressage, le sens de transfert (lecture ou écriture), et bien d'autres choses. Schématiquement, on peut subdiviser toute mémoire en plusieurs circuits principaux.
* La mémorisation des informations est prise en charge par le '''plan mémoire'''. Il est composé d'un regroupement de cellules mémoires, auxquelles on a ajouté quelques fils pour communiquer avec le bus.
* La gestion de l'adressage et de la communication avec le bus sont assurées par un circuit spécialisé : le '''contrôleur mémoire''', composé d'un décodeur et de circuits de contrôle.
* L''''interface avec le bus''' relie le plan mémoire au bus de données. C'est le plus souvent ici qu'est géré le sens de transfert des données, ainsi que tout ce qui se rapporte aux lectures et écritures.
[[File:Td6bfig1.png|centre|vignette|upright=2|Organisation interne d'une mémoire adressable.]]
Nous allons étudier le plan mémoire dans ce chapitre, le contrôleur mémoire et l'interface avec le bus seront vu dans les deux chapitres suivants. Cela peut paraitre bizarre de dédier un chapitre complet au plan mémoire, mais il y a de quoi. Celui-ci n'est pas qu'un simple amoncellement de cellules mémoire et de connexions vaguement organisées. On y trouve aussi des circuits électroniques aux noms barbares : amplificateur de tension, égaliseur de ligne de bit, circuits de pré-charge, etc. L'organisation des fils dans le plan mémoire est aussi intéressante à étudier, celle-ci étant bien plus complexe qu'on peut le croire.
==Les fils et signaux reliés aux cellules==
[[File:Transparent Latch Symbol.svg|vignette|upright=0.5|Interface d'une bascule D.]]
Le plan mémoire est surtout composé de fils, sur lesquels on connecte des cellules mémoires. Rappelons que les cellules mémoires se présentent avec une interface simple, qui contient des broches pour le transfert des données et d'autres broches pour les commandes de lecture/écriture. Reste à voir comment toutes ses broches sont reliées aux différents bus et au contrôleur mémoire. Ce qui va nous amener à parler des lignes de bit et des signaux de sélection de ligne. Il faut préciser que la distinction entre broches de commande et de données est ici très importante : les broches de données sont connectées indirectement au bus, alors que les broches de commande sont reliées au contrôleur mémoire. Aussi, nous allons devoir parler des deux types de broches dans des sections séparées.
===La connexion des broches de données : les lignes de bit===
Afin de simplifier l'exposé, nous allons étudier une mémoire série dont le byte est de 1 bit. Une telle mémoire est dite '''bit-adressable''', c’est-à-dire que chaque bit de la mémoire a sa propre adresse. Nous étudierons le cas d'une mémoire quelconque plus loin, et ce pour une raison : on peut construire une mémoire quelconque en améliorant le plan mémoire d'une mémoire bit-adressable, d'une manière assez simple qui plus est. Parler de ces dernières est donc un bon marche-pied pour aboutir au cas général.
====Le cas d'une mémoire bit-adressable====
Une mémoire bit-adressable est de loin celle qui a le plan mémoire le plus rudimentaire. Quand on sélectionne un bit, avec son adresse, son contenu va se retrouver sur le bus de données. Dit autrement, la cellule mémoire va se connecter sur ce fils pour y placer son contenu. On devine donc comment est organisé le plan mémoire : il est composé d'un fil directement relié au bus de donnée, sur lequel les cellules mémoire se connectent si besoin. Le plan mémoire se résume donc à un ensemble de cellules mémoires dont l'entrée/sortie est connectée à un unique fil. Ce fil s'appelle la '''ligne de bit''' (''bitline'' en anglais).
[[File:Plan mémoire simplifié d'une mémoire bit-adressable.png|centre|vignette|upright=1|Plan mémoire simplifié d'une mémoire bit-adressable.]]
En réalité, peu de mémoires suivent actuellement le principe précédent. Les mémoires assez évoluées utilisent deux lignes de bit par colonne ! La première transmet le bit lu et l'autre son inverse, ce qui se marie bien avec le fait que certaines bascules fournissent le bit et son inverse sur deux broches distinctes. La mémoire utilise la différence de tension entre ces deux fils pour représenter le bit lu ou à écrire. Un tel codage est appelé un '''codage différentiel'''. L'utilité d'un tel codage assez difficile à expliquer sans faire intervenir des connaissances en électricité, mais tout est une histoire de fiabilité et de résistance aux parasites électriques.
[[File:Bitlines différentielles.png|centre|vignette|upright=1|Bitlines différentielles.]]
Certaines mémoires ont amélioré les lignes de bit différentielles en interchangeant leur place à chaque cellule mémoire. La ligne de bit change donc de côté à chaque passage d'une cellule mémoire. Cette organisation porte le nom de '''lignes de bit croisées'''.
[[File:Bitlines croisées.png|centre|vignette|upright=1|Bitlines croisées.]]
Les lignes de bit différentielles se marient assez mal avec les cellules mémoire de DRAM, qui n'ont pas de seconde sortie pour lire l'inverse du bit stocké. Malgré tout, l'usage de lignes de bit différentielle est possible, bien que compliqué, grâce à des techniques comme l'usage de cellules factices (nous en reparlerons plus bas). Cependant, il est possible d'utiliser une organisation intermédiaire entre des lignes de bit simple et des lignes différentielles, qui connecte des cellules consécutives comme illustré ci-dessous. On voit qu'il y a deux lignes de bit : la moitié des cellules est connectée à la première ligne, l'autre moitié à la seconde, avec une alternance entre cellules consécutives. Cela permet d'avoir moins de cellules mémoires connectées sur le même fil, ce qui améliore certains paramètres électriques des lignes de bit. Cette organisation porte le nom de '''ligne de bit repliée'''.
[[File:Lignes de bit repliées.png|centre|vignette|upright=1|Lignes de bit repliées]]
====Le cas d'une mémoire quelconque (avec byte > 1)====
Après avoir vu le cas des mémoires bit-adressables, il est temps d'étudier les mémoires quelconques, celles où un byte contient plus que 1 bit. Surprenamment, ces mémoires peuvent être conçues en utilisant plusieurs mémoires bit-adressables. Par exemple, prenons une mémoire dont le byte fait deux bits (ce qui est rare, convenons-en). On peut l'émuler à partir de deux mémoires de 1 bit : la première stocke le bit de poids faible de chaque byte, alors que l'autre stock le bit de poids fort. Et on peut élargir le raisonnement pour des bytes de 3, 4, 8, 16 bits, et autres. Par exemple, pour une mémoire dont le byte fait 64 bits, il suffit de mettre en parallèle 64 mémoires de 1 bit.
Mais cette technique n'est pas appliquée à la lettre, car il y a moyen d'optimiser le tout. En effet, on ne va pas mettre effectivement plusieurs mémoires bit-adressables en parallèle, car seuls les plans mémoires doivent être dupliqués. Si on utilisait effectivement plusieurs mémoires, chacune aurait son propre plan mémoire, mais aussi son propre contrôleur mémoire, ses propres circuits de communication avec le bus, etc. Or, ces circuits sont en fait redondants dans le cas qui nous intéresse.
Prenons le cas du contrôleur mémoire, qui reçoit l'adresse à lire/écrire et qui envoie les signaux de commande au plan mémoire. Avec N mémoires en parallèle, N contrôleurs mémoire recevront l'adresse et généreront les N mêmes signaux, qui seront envoyés à N plans mémoire distincts. Au lieu de cela, il est préférable d'utiliser un seul contrôleur mémoire, mais de dupliquer les signaux de commande en autant N exemplaires (autant qu'il y a de plan mémoire). Et c'est ainsi que sont conçues les mémoires quelconques : pour un byte de N bits, il faut prendre N plans mémoires de 1 bit. Cela demande donc d'utiliser N lignes de bits, reliée convenablement aux cellules mémoires. Le résultat est un rectangle de cellules mémoires, où chaque colonne est traversée par une ligne de bit. Chaque ligne du tableau/rectangle, correspond à un byte, c'est-à-dire une case mémoire.
Là encore, chaque colonne peut utiliser des lignes de bits différentielles ou croisées.
[[File:Plan mémoire, avec les bitlines.png|centre|vignette|upright=1.5|Plan mémoire, avec les bitlines.]]
===La connexion des broches de commande : le transistor et le signal de sélection===
Évidemment, les cellules mémoires ne doivent pas envoyer leur contenu sur la ligne de bit en permanence. En réalité, chaque cellule est connectée sur la ligne de bit selon les besoins. Les cellules correspondant au mot adressé se connectent sur la ligne de bit, alors que les autres ne doivent pas le faire. La connexion des cellules mémoire à la ligne de bit est réalisée par un interrupteur commandable, c’est-à-dire par un transistor appelé '''transistor de sélection'''. Quand la cellule mémoire est sélectionnée, le transistor se ferme, ce qui connecte la cellule mémoire à la ligne de bit. À l'inverse, quand une cellule mémoire n'est pas sélectionnée, le transistor de sélection se comporte comme un interrupteur ouvert : la cellule mémoire est déconnectée du bus.
La commande du transistor de sélection est effectuée par le contrôleur mémoire. Pour chaque ligne de bit, le contrôleur mémoire n'ouvre qu'un seul transistor à la fois (celui qui correspond à l'adresse voulue) et ferme tous les autres. La correspondance entre un transistor de sélection et l'adresse est réalisée dans le contrôleur mémoire, par des moyens que nous étudierons dans les prochains chapitres. Toujours est-il que le contrôleur mémoire génère, pour chaque octet, un bit qui dit si celui-ci est adressé ou non. Ce bit est appelé le '''signal de sélection'''. Le signal de sélection est envoyé à toutes les cellules mémoire qui correspondent au byte adressé. Vu que tous les bits d'un byte sont lus ou écrits en même temps, toutes les cellules correspondantes doivent être connectées à la ligne de bit en même temps, et donc tous les transistors de sélection associés doivent se fermer en même temps. En clair, le signal de sélection est partagé par toutes les cellules d'un même mot mémoire.
[[File:Signal row line.png|centre|vignette|upright=2.5|Signal de sélection et Byte.]]
====Le cas des lignes de bit simples et repliées====
Voyons comment les bitlines simples sont reliées aux cellules mémoires. Les mémoires 1T-DRAM n'ont qu'une seule broche entrée/sortie, sur laquelle on effectue à la fois les lectures et les écritures. Cela se marie très bien avec des bitlines simples, mais ça les rend incompatibles avec des bitlines différentielles. Le cas des DRAM à bitlines simples, avec une seule sortie, un seul transistor de sélection, est illustré ci-dessous. On peut noter que les cellules de SRAM peuvent malgré tout s’accommoder de bitlines simples : il suffit de connecter la sortie Q sur la bitline simple et ne pas connecter la sortie <math>\overline{Q}</math> à quoi que ce soit. On peut même en profiter pour supprimer le transistor de sélection de cette sortie, ce qui réduit le nombre de transistors à seulement 5.
[[File:Plan mémoire d'une mémoire bit-adressable.png|centre|vignette|upright=1.5|Plan mémoire d'une mémoire bit-adressable.]]
La connexion des transistors de sélection pour des lignes de bit repliée n’est pas très différente de celle des lignes de bit simple. Elle est illustrée ci-dessous.
[[File:Ligne de bit repliée.png|centre|vignette|upright=1.5|Ligne de bit repliée.]]
====Le cas des lignes de bit différentielles====
Le cas des mémoires SRAM est de loin le plus simple à comprendre. Celles-ci utilisent toutes (ou presque) des bitlines différentielles, chose qui se marie très bien avec l'interface des cellules SRAM. Rappelons que celle-ci possèdent deux broches pour les données : une broche Q sur laquelle on peut lire ou écrire un bit, et une broche complémentaire sur laquelle on récupère l'inverse du bit lu. À chaque broche correspond un transistor de sélection différents. Dans ce cas, la sortie Q est connectée sur une bitline, alors que l'autre sortie complémentaire <math>\overline{Q}</math> l'est sur l'autre bitline.
[[File:Connexion d'une cellule mémoire de SRAM à une bitline différentielle.png|centre|vignette|upright=2|Connexion d'une cellule mémoire de SRAM à une bitline différentielle.]]
Précisons que les DRAM se marient assez mal avec l'usage de lignes de bit différentielle, vu qu'elles n'ont pas de sortie complémentaire <math>\overline{Q}</math>, mais n'ont qu'une seule sortie Q. Mais quelques astuces permettent d'utiliser des lignes de bit différentielles sur ces mémoires. La plus connue est de loin l'utilisation de '''cellules factices''' (''dummy cells''), des cellules mémoires vides placées aux bouts des lignes de bit. Lors d'une lecture, ces cellules vides se remplissent avec l'inverse du bit à lire. La ligne de bit inverse (celle qui contient l'inverse du bit) est alors remplie avec le contenu de la cellule factice, ce qui donne bien un signal différentiel. Le bit inversé est fournit par une porte logique qui inverse la tension fournie par la cellule mémoire. Cette tension remplis alors la cellule factice, avec l'inverse du bit lu.
====Le cas des cellules mémoires double port====
Après avoir vu les cellules mémoire "normales" plus haut, il est temps de passer aux cellules mémoire de type double port. Pour simplifier, les cellules double port possèdent une sortie pour les lectures et une entrée pour les écritures, toutes deux d'un bit. On peut les utiliser pour concevoir des mémoires double port, mais aussi des mémoires simple port.
Cette particularité est exploitée pour créer des mémoires double-port bit-adressables, qui ont une broche pour les lectures et une autre pour les écritures. Le plan mémoire contient alors deux lignes de bit, une pour la broche de lecture et une autre pour la broche d'écriture. Le transistor de lecture est connecté à la ligne de bit de lecture, alors que celui pour l'écriture est relié à la ligne de bit d'écriture.
[[File:Plan mémoire d'une SRAM double port.png|centre|vignette|upright=2|Plan mémoire d'une SRAM double port.]]
Pour les mémoires simple port, c'est-à-dire avec une seule broche qui sert à la fois pour les lectures et écritures, les deux transistors sont reliés à la même ligne de bit. Ils vont s'ouvrir ou se fermer selon les besoins, sous commande du contrôleur mémoire.
[[File:Plan mémoire d'une SRAM simple port.png|centre|vignette|upright=2|Plan mémoire d'une SRAM simple port.]]
Dans les deux cas, le contrôleur mémoire est relié directement aux transistors de sélection. Il doit générer à la fois les signaux d'autorisation de lecture que ceux pour l'écriture. Ces deux signaux peuvent être déduit du bit de sélection et du bit R/W, comme vu dans le chapitre précédent.
[[File:Circuit d'interface entre contrôleur mémoire et cellule mémoire.png|centre|vignette|upright=1.5|Circuit d'interface entre contrôleur mémoire et cellule mémoire.]]
==L'amplificateur de tension==
Quand on connecte une cellule mémoire à une ligne de bit, c'est à la cellule mémoire de fournir le courant pour mettre la ligne à 0 ou à 1. Mais dans certains cas, la cellule mémoire ne peut pas fournir assez courant pour cela. Cela arrive surtout sur les mémoires DRAM, basées sur un condensateur. Ces condensateurs ont une faible capacité et ne peuvent pas conserver beaucoup d'électrons, surtout sur les mémoires modernes. Du fait de la miniaturisation, les condensateurs des DRAM actuelles ne peuvent stocker que quelques centaines d'électrons, parfois beaucoup moins. Autant dire que la vidange du condensateur dans la ligne de bit ne suffit pas à la mettre à 1, même si la cellule mémorisait bien un 1. La lecture crée à peine une tension de quelques millivolts dans la ligne de bit, pas plus. Mais il y a une solution : amplifier la tension de quelques millivolts induite par la vidange du condensateur sur le bus. Pour cela, il faut donc placer un dispositif capable d'amplifier cette tension, bien nommé '''amplificateur de lecture'''.
[[File:Differential amplifier.svg|vignette|Amplificateur différentiel.]]
L’amplificateur utilisé n'est pas le même avec des lignes de bit simples et des lignes de bit différentielles. Dans le cas différentiel, l'amplificateur doit faire la différence entre les tensions sur les deux lignes de bit et traduire cela en un niveau logique. C'est l'amplificateur lui-même qui fait la conversion entre codage différentiel (sur deux lignes de bit) et codage binaire. Pour le distinguer des autres amplificateurs, il porte le nom d''''amplificateur différentiel'''. L'amplificateur différentiel possède deux entrées, une pour chaque ligne de bit, et une sortie. Dans ce qui va suivre, les entrées seront notées <math>V_{in}^+</math> et <math>V_{in}^-</math>, la sortie sera notée <math>V_{out}</math>. L’amplificateur différentiel fait la différence entre ces deux entrées et amplifie celle-ci. En clair :
: <math>V_{out} = A \times ( V_{in}^+ - V_{in}^- )</math>
Il faut noter qu'un amplificateur différentiel peut fonctionner aussi bien avec des lignes de bit différentielles qu'avec des lignes de bit simples. Avec des lignes de bit simples, il suffit de placer l'autre entrée à la masse, au 0 Volts, et de n'utiliser qu'une seule sortie.
Il existe de nombreuses manières de concevoir un amplificateur différentiel, mais nous n'allons aborder que les circuits les plus simples. Dans les grandes lignes, il existe deux types d'amplificateurs de lecture : ceux basés sur des bascules et ceux basés sur une paire différentielle. Bizarrement, vous verrez que les deux ont une certaine ressemblance avec les cellules de SRAM ! Il faut dire qu'une porte NON, fabriquée avec des transistors, est en réalité un petit amplificateur spécialisé, chose qui tient au fonctionnement de son circuit.
===L'amplificateur de lecture à paire différentielle===
[[File:Long tailed pair.svg|vignette|Paire différentielle. Le générateur de courant est en jaune, la charge est en bleu. Ici, la charge est un miroir de courant. Les transistors ne sont pas des transistors MOS, mais le circuit fonctionne de la même manière que si c'était le cas.]]
Le premier type d'amplificateur différentiel est la '''paire différentielle''', composée de deux transistors mis en série avec une charge et un générateur de courant. La charge est placée entre la tension d'alimentation et le transistor, alors que le générateur de courant est placé entre le transistor et la tension basse (la masse, ou l'opposé de la tension d'alimentation, selon le montage). Le circuit ci-contre illustre le circuit de la paire différentielle.
Précisons que la charge mentionnée précédemment varie selon le circuit, de même que le générateur de courant. Dans le cas le plus simple, une simple résistance suffit pour les deux. Mais ce n'est pas cette solution qui est utilisée dans les mémoires actuelles. En effet, intégrer des résistances est compliqué dans les circuits à semi-conducteurs modernes, et les mémoires RAM en sont. Si on peut y intégrer des résistances, des condensateurs ou des inductances/bobines, c'est une chose très complexe et qui ne vaut généralement pas le coup. Aussi, les résistances et condensateurs sont généralement remplacés par des circuits équivalents, qui ont le même rôle ou qui peuvent remplacer une résistance dans le montage voulu.
[[File:Differential amplifier long-tailed pair.svg|centre|vignette|Paire différentielle, avec des résistances.]]
Le générateur de courant et la charge doivent être fabriqués avec des transistors MOS, voire CMOS, ce qui n'est pas un problème. Chacun de ces circuits est remplacée par un ''miroir de courant'', à savoir un circuit qui crée un courant constant sur une sortie et le recopie sur une seconde sortie. L'avantage est que le miroir de courant fournit le même courant aux deux ''bitlines'', il égalise les courants dans les deux bitlines. Il existe plusieurs manières de créer un tel miroir de courant avec des transistors MOS/CMOS, la plus simple étant illustrée ci-dessous. On pourrait aborder le fonctionnement d'un tel circuit, pourquoi il fonctionne, mais nous n'en parlerons pas ici. Cela relèverait plus d'un cours d'électronique analogique, et demanderait de connaître en détail le fonctionnement d'un transistor, les équations associées, etc.
[[File:Einfacher Stromspiegel MOSFET1.svg|centre|vignette|Miroir de courant fabriqué avec des transistors MOS.]]
===L'amplificateur de lecture à verrou===
Le second type d'amplificateur de lecture est l''''amplificateur à verrou'''. Il amplifie une différence de tension entre les deux lignes de bit d'une colonne différentielle. Les deux colonnes doivent être préchargées à Vdd/2, à savoir la moitié de la tension d'alimentation. La raison à cela deviendra évidente dans les explications qui vont suivre. Toujours est-il que ce circuit a besoin qu'un circuit dit de précharge s'occupe de placer la tension adéquate sur les lignes de bit, avant toute lecture ou écriture. Nous reparlerons de ce circuit de précharge dans les sections suivantes, vers la fin de ce chapitre. Cela peu paraître peu pédagogique, mais à notre décharge, sachez que le circuit de précharge et l'amplificateur de lecture sont intimement liés. Il est difficile de parler de l'un sans parler de l'autre et réciproquement. Pour le moment, tout ce que vous avez à retenir est qu'avant toute lecture, les lignes de bit sont chargées à Vdd/2, ce qui permet à l'amplificateur à verrou de fonctionner correctement.
====Le circuit de l'amplificateur de lecture à verrou====
L'amplificateur à verrou est composé de deux portes NON reliées tête-bêche, comme dans une cellule de SRAM. Chaque ligne de bit est reliée à l'autre à travers une porte NON. Sauf que cette fois-ci, il n'y a pas toujours de transistors de sélection, ou alors ceux-ci sont placés autrement.
[[File:Latch-type sense amplifier.png|centre|vignette|upright=1|Amplificateur de lecture à bascule.]]
Le circuit complet est illustré ci-dessous, de même qu'une version plus détaillée avec des transistors. Du fait de son câblage, l'amplificateur à verrou a pour particularité d'avoir des broches d'entrées qui se confondent avec celles de sortie : l'entrée et la sortie pour une ligne de bit sont fusionnées en une seule broche. L'utilisation d'inverseurs explique intuitivement pourquoi il faut précharger les lignes de bit à Vdd/2 : cela place la tension dans la zone de sécurité des deux inverseurs, là où la tension ne correspond ni à un 0, ni à un 1. Le fonctionnement du circuit dépend donc du fonctionnement des transistors, qui servent alors d'amplificateurs.
[[File:Amplificateur de lecture à bascule, version détaillée.png|centre|vignette|upright=2|Amplificateur de lecture à bascule, version détaillée.]]
On peut noter que cet amplificateur est parfois fabriqué avec des transistors bipolaires, qui consomment beaucoup de courant. Mais même avec des transistors MOS, il est préférable de réduire la consommation électrique du circuit, quand bien même ceux-ci consomment peu. Pour cela, on peut désactiver l’amplificateur quand on ne l'utilise pas. Pour cela, on entoure l'amplificateur avec des transistors qui le débranchent, le déconnectent si besoin.
[[File:Amplificateur de lecture à bascule, avec transistors d'activation.png|centre|vignette|upright=2|Amplificateur de lecture à bascule, avec transistors d'activation.]]
====Le fonctionnement de l'amplificateur à verrou====
Expliquer en détail le fonctionnement de l'amplificateur à verrou demanderait de faire de l'électronique assez poussée. Il nous faudrait détailler le fonctionnement d'un transistor quand il est utilisé en tant qu'amplificateur, donner des équations, et bien d'autres joyeusetés. À la place, je vais donner une explication très simplifiée, que certains pourraient considérer comme fausse (ce qui est vrai, dans une certaine mesure).
Avant toute chose, précisons que les seuils pour coder un 0 ou un 1 ne sont pas les mêmes entre l’entrée d'une porte NON et sa sortie. Ils sont beaucoup plus resserrés sur l'entrée, la marge de sécurité entre 1 et 0 étant plus faible. Un signal qui ne correspondrait pas à un 0 ou un 1 en sortie peut l'être en entrée.
Le fonctionnement du circuit ne peut s'expliquer correctement qu'à partir du rapport entre tension à l'entrée et tension de sortie d'une porte NON. Le schéma ci-dessous illustre cette relation. On voit que la porte logique amplifie le signal d'entrée en plus de l'inverser. Pour caricaturer, on peut décomposer cette caractéristique en trois parties : deux zones dites de saturation et une zone d'amplification. Dans la zone de saturation, la tension est approximativement égale à la tension maximale ou minimale, ce qui fait qu'elle code pour un 0 ou un 1. Entre ces deux zones extrêmes, la tension de sortie dépend linéairement de la tension d'entrée (si on omet l'inversion).
[[File:Caractéristique tension d'entrée-tension de sortie d'un inverseru CMOS.png|centre|vignette|upright=2|Caractéristique tension d'entrée-tension de sortie d'un inverseur CMOS.]]
Quand on place deux portes NON l'une à la suite de l'autre, le résultat est un circuit amplificateur, dont la caractéristique est illustrée dans le second schéma. On voit que l'amplificateur amplifie la différence de tension entre VDD/2 et la tension d'entrée (sur la ligne de bit).
[[File:Utilisation de deux portes NON comme amplificateur de tension.png|centre|vignette|upright=2|Utilisation de deux portes NON comme amplificateur de tension.]]
Si on regarde le circuit complet, on s’aperçoit que chaque ligne de bit est bouclée sur elle-même, à travers cet amplificateur. Cela fait boucler la sortie de l'amplificateur sur son entrée : la tension de base est alors amplifiée une fois, puis encore amplifiée, et ainsi de suite. Au final, les seuls points stables du montage sont la tension maximale ou la tension minimale, soit un 0 ou un 1, ou la tension VDD/2.
Ceci étant dit, on peut enfin comprendre le fonctionnement complet du circuit d'amplification. Commençons l'explication par la situation initiale : la ligne de bit est préchargée à VDD/2, et la cellule mémoire est déconnectée des lignes de bit. La ligne de bit est préchargée à VDD/2, l'amplificateur a sa sortie comme son entrée égales à VDD/2 et le circuit est parfaitement stable. Ensuite, la cellule mémoire à lire est connectée à la ligne de bit et la tension va passer au-dessous ou au-dessus de VDD/2. Nous allons supposer que celle-ci contenait un 1, ce qui fait que sa connexion entraîne une montée de la tension de la ligne de bit. La tension ne va cependant pas monter de beaucoup, mais seulement de quelques millivolts. Cette différence de tension va être amplifiée par les deux portes logiques, ce qui amplifie la différence de tension. Et rebelote : cette différence amplifiée est ré-amplifiée par le montage, et ainsi de suite jusqu’à ce que le circuit se stabilise soit à 0 soit à 1.
[[File:Fonctionnement très simplifié de l'amplificateur à verrou.png|centre|vignette|upright=2|Fonctionnement très simplifié de l'amplificateur à verrou.]]
==L'optimisation du temps de charge/décharge des lignes de bit==
Si les lignes de bit sont de simples fils conducteurs passifs, cela ne veut pas dire qu'ils n'ont pas d'influence sur les lectures et écritures. En réalité, ils jouent un grand rôle dans la rapidité des accès mémoire, pour des raisons techniques. Selon leur longueur, la tension va prendre plus ou moins de temps pour s'établir dans la ligne de bit, ce qui impacte directement les performances de la mémoire. Diverses techniques ont étés inventées pour résoudre ce problème, la plus importante étant l'utilisation d'un circuit dit de pré-charge, que nous allons étudier maintenant.
Les lignes de bit ne sont pas des fils parfaits : non seulement ils ont une résistance électrique, mais ils se comportent aussi comme des condensateurs (dans une certaine mesure). Nous n'expliquerons pas dans la physique de ce phénomène, mais allons simplement admettre qu'un fil électrique se modélise bien en mettant une résistance R en série avec un condensateur C : le circuit obtenu est un circuit RC. Le condensateur, appelée '''capacité parasite''', n’apparaît que lorsque la tension de la ligne de bit change en passant de 0 à 1 ou inversement. Ce qui n'arrive que lors d'une lecture ou écriture, cela va de soit. Lorsque l'on change la tension en entrée d'un tel montage, la tension de sortie met un certain temps avant d'atteindre la valeur d'entrée. Ce qui est illustré dans les deux schémas ci-dessous, pour la charge (passage de 0 à 1) et la décharge (passage de 1 à 0). La variation est d'ailleurs exponentielle. On estime qu'il faut un temps égal <math>t \approx 3 \times R \cdot C</math>, avec R la valeur de la résistance et C celle du condensateur. En clair : la ligne de bit met un certain temps avant que la tension atteigne celle qui correspond au bit lu ou à écrire.
{|class="wikitable flexible"
|[[File:RC Series Filter (with V&I Labels).svg|300px|class=transparent|Circuit RC série.]]
|[[File:Series RC capacitor voltage.svg|300px|class=transparent|Tension aux bornes d'un circuit RC en charge.]]
|[[File:Series RC resistor voltage.svg|300px|class=transparent|Tension aux bornes d'un circuit RC en cours de décharge.]]
|}
===L'organisation du plan mémoire===
Une première idée pour optimiser le temps RC est de diminuer la résistance de la ligne. Il se trouve que celle-ci est proportionnelle à la longueur de la ligne de bit : plus la ligne de bit est longue, plus la résistance R sera élevée. On voit donc une première solution pour réduire la résistance, et donc le temps RC : réduire la taille des lignes de bit. Les petites mémoires, avec peu de cellules sur une colonne, ont des lignes de bit plus petites et sont donc plus rapides. Cela explique en partie pourquoi les temps d'accès des mémoires varient selon la capacité, chose que nous avons abordé il y a quelques chapitres. De même, à capacité égale, il vaut mieux utiliser des bytes large, pour réduire la taille des colonnes.
====L'agencement en colonne de donnée ouvertes====
Mais d'autres optimisations du plan mémoire permettent d'obtenir des lignes de bit plus petites, à capacité et largeur de byte inchangée. Par exemple, on peut placer l'amplificateur de lecture au milieu du plan mémoire, et non au bout. En faisant ainsi, on doit couper la ligne de bit en deux, chaque moitié étant placée d'un côté ou de l'autre de l’amplificateur. La colonne contient ainsi deux lignes de bits séparées, chacune ayant une longueur réduite de moitié. Cette organisation est dite '''en colonne de donnée ouvertes'''. Mais cette organisation a un défaut : il est difficile d'implémenter l'amplificateur au milieu de la mémoire. Le nombre de fils qui doivent passer par le milieu de la RAM est important, rendant le câblage compliqué. De plus, les perturbations électromagnétiques ne touchent pas de la même manière chaque côté de la mémoire et l'amplificateur peut donner des résultats problématiques à cause d'elles.
[[File:Optimisations du plan mémoire pour réduire la taille des bitlines.png|centre|vignette|upright=2.5|Optimisations du plan mémoire pour réduire la taille des bitlines.]]
Il est aussi possible de répartir les amplificateurs de tension autrement. On peut mélanger les organisations en colonne de données ouvertes et "normales", en mettant les amplificateurs à la fois au milieu de la RAM et sur les bords. Une moitié des amplificateurs est placée au milieu du plan mémoire, l'autre moitié est placée sur les bords. On alterne les lignes de bits connectée entre amplificateurs selon qu’ils sont sur les bords ou au milieu. L'organisation est illustrée ci-dessous.
[[File:Organisation en colonnes de données ouvertes, avec répartition des amplificateur sur les bords du plan mémoire.png|centre|vignette|upright=2.5|Organisation en colonnes de données ouvertes, avec répartition des amplificateur sur les bords du plan mémoire.]]
===La pré-charge des lignes de bit===
[[File:Sense Amp position.jpg|vignette|Aperçu d'une ligne de bit conçue pour être préchargée. On voit qu'il s'agit d'une ligne de bit "normale", à laquelle a été ajouté un circuit qui permet de charger la ligne à partir de la tension d'alimentation. L'amplificateur de tension est situé du côté opposé au circuit de charge.]]
Une autre solution, beaucoup plus ingénieuse, ne demande pas de modifier la longueur des lignes de bit. À la place, on rend leur charge plus rapide en les pré-chargeant. Sans pré-charge, la ligne de bit est à 0 Volts avant la lecture et la lecture altère cette tension, que ce soit pour la laisser à 0 (lecture d'un 0), ou pour la faire monter à la tension maximale Vdd (lecture d'un 1). Le temps de réaction de la ligne de bit dépend alors du temps qu'il faut pour la faire monter à Vdd. Avec la pré-charge, la ligne de bit est chargée avant la lecture, de manière à la mettre à la moitié de Vdd. La lecture du bit fera descendre celle-ci à 0 (lecture d'un 0) ou la faire grimper à Vdd (lecture d'un 1). Le temps de charge ou de décharge est alors beaucoup plus faible, vu qu'on part du milieu.
: Il faut noter que la pré-charge à Vdd/2 est un cas certes simple à comprendre, mais qui n'a pas valeur de généralité. Certaines mémoires pré-chargent leurs lignes de bit à une autre valeur, qui peut être Vdd, à 60% de celui-ci, ou une autre valeur. En fait, tout dépend de la technologie utilisée. Par exemple, Les mémoires de type CMOS pré-chargent à Vdd/2, alors que les mémoires TTL, NMOS ou PMOS pré-chargent à une autre valeur (le plus souvent Vdd).
On peut penser qu'il faudra deux fois moins de temps, mais la réalité est plus complexe (regardez les graphes de charge/décharge situés plus haut). De plus, il faut ajouter le temps mis pour précharger la ligne de bit, qui est à ajouter au temps de lecture proprement dit. Sur la plupart des mémoires, la pré-charge n'est pas problématique. Il faut dire qu'il est rare que la mémoire soit accédé en permanence et il y a toujours quelques temps morts pour pré-charger la ligne de bit. On verra que c'est notamment le cas sur les mémoires DRAM synchrones modernes, comme les SDRAM et les mémoires DDR. Mais passons...
====Les circuits de précharge====
La pré-charge d'une ligne de bit se fait assez facilement : il suffit de connecter la ligne de bit à une source de tension qui a la valeur adéquate. Par exemple, une mémoire qui se pré-charge à Vdd a juste à relier la ligne de bit à la tension d'alimentation. Mais attention : cette connexion doit disparaître quand on lit ou écrit un bit dans les cellules mémoire. Sans cela, le bit envoyé sur la ligne de bit sera perturbé par la tension ajoutée. Il faut donc déconnecter la ligne de bit de la source d'alimentation lors d'une lecture écriture. On devine rapidement que le circuit de pré-charge est composé d'un simple interrupteur commandable, placé entre la tension d'alimentation (Vdd ou Vdd/2) et la ligne de bit. Le contrôleur mémoire commande cet interrupteur pour précharger la ligne de bit ou stopper la pré-charge lors d'un accès mémoire. Si un seul transistor suffit pour les lignes de bit simples, deux sont nécessaires pour les lignes de bit différentielles ou croisées. Ils doivent être ouvert et fermés en même temps, ce qui fait qu'ils sont commandés par un même signal.
[[File:Circuits de précharge.png|centre|vignette|upright=2.5|Circuits de précharge]]
====L'égaliseur de tension====
Pour les lignes de bit différentielles et croisées, il se peut que les deux lignes de bit complémentaires n'aient pas tout à fait la même tension suite à la pré-charge. Pour éviter cela, il est préférable d'ajouter un '''circuit d'égalisation''' qui égalise la tension sur les deux lignes. Celui-ci est assez simple : c'est un interrupteur commandable qui connecte les deux lignes de bit lors de la pré-charge. Là encore, un simple transistor suffit. L'égalisation et la pré-charge ayant lieu en même temps, ce transistor est commandé par le même signal que celui qui active le circuit de précharge. Le circuit complet, qui fait à la fois pré-charge et égalisation des tensions, est représenté ci-dessous.
[[File:Circuits de précharge et d'égalisation pour des lignes de bit différentielles.png|centre|vignette|upright=2.5|Circuits de précharge et d'égalisation pour des lignes de bit différentielles.]]
==Annexe : l'attaque ''rowhammer''==
Vous connaissez maintenant comment fonctionnent les cellules mémoires et le plan mémoire, ce qui fait que vous avez les armes nécessaires pour aborder des sujets assez originaux. Profitons-en pour aborder une faille de sécurité présente dans la plupart des mémoires DRAM actuelles : l'attaque ''row hammer''. Vous avez bien entendu : il s'agit d'une faille de sécurité matérielle, qui implique les mémoires RAM, qui plus est. Voilà qui est bien étrange. D'ordinaire, quand on parle de sécurité informatique, on parle surtout de failles logicielles ou de problèmes d'interface chaise-clavier. La plupart des attaques informatiques sont des attaques d’ingénierie sociale où on profite de failles humaines pour obtenir un mot de passe ou toute autre information confidentielle, suivies par les failles logicielles, les virus, malwares et autres méthodes purement logicielles. Mais certaines failles de sécurités sont purement matérielles et profitent de bugs présents dans le matériel pour fonctionner. Car oui, les processeurs, mémoires, bus et périphériques peuvent avoir des bugs matériels qui sont généralement bénins, mais que des virus, logiciels ou autres malware peuvent exploiter pour commettre leur méfaits.
[[File:Row hammer.svg|vignette|Attaque Row hammer - la ligne violette est accédée un grand nombre de fois à la suite, les lignes voisines en jaune sont altérées.]]
L'attaque ''row hammer'', aussi appelée attaque par '''martèlement de mémoire''', utilise un bug de conception des mémoires DRAM. Le bug en question tient dans le fait que les cellules mémoires ne sont pas parfaites et que leur charge électrique tend à fuir. Ces fuites de courant se dispersent autour de la cellule mémoire et tendent à affecter les cellules mémoires voisines. En temps normal, cela ne pose aucun problème : les fuites sont limitées et l'interaction électrique est limitée. Pas de quoi changer le contenu des cellules mémoires voisines. Cependant, des hackers ont réussit à exploiter ce comportement pour copier le contenu d'une cellule mémoire dans une autre. En accédant d'une manière bien précise à une ligne de la mémoire, on peut garantir que les fuites de courant deviennent signifiantes, suffisamment pour recopier le contenu d'une ligne mémoire dans les lignes mémoires voisines. Et modifier plusieurs bytes sans y accéder, mais en accédant à leurs voisins est une faille exploitable par les pirates informatiques. Pour cela, il faut accéder un très grand nombre de fois à la cellule mémoire en question, ce qui explique pourquoi cette attaque s'appelle le ''martèlement'' de mémoire.
L'intérêt est de contourner les protections mémoires liées au système d'exploitation. Sur les systèmes d'exploitation modernes, chaque programme se voit attribuer certaines portions de la mémoire, auxquelles il est le seul à pouvoir accéder. Des mécanismes de protection mémoire intégré dans le processeur permettent d'isoler la mémoire de chaque programme, comme nous le verrons dans le chapitre sur la mémoire virtuelle. Mais avec ''row hammer'', les accès à un byte attribué à un programme peuvent déborder sur les bytes d'un autre programme, avec des conséquences assez variables. Par exemple, un virus présent en mémoire pourrait interagir avec le byte qui mémorise un mot de passe ou une clé de sécurité RSA, ou toute donnée confidentielle. Il pourrait récupérer cette information, ou alors la modifier pour la remplacer par une valeur connue et l'attaquant.
Exploiter cette attaque est cependant compliqué, car il faut savoir à quelle adresse se situe a donnée à altérer, sans compter qu'il faut avoir des informations sur l'adresse des cellules voisines. Rappelons que la répartition physique des adresses/bytes dépend de comment la mémoire est organisée en interne, avec des banques, rangées et autres. Deux adresses consécutives ne sont pas forcément voisines sur la barrette de mémoire et l relation entre deux adresses de cellules mémoires voisines n'est pas connue avec certitude tant elle varie d'un système mémoire à l'autre.
La faille ''row hammer'' est d'autant plus simple que la physique des cellules mémoire est médiocre. Les progrès de la miniaturisation rendent cette attaque de plus en plus facilement exploitable, les fuites étant d'autant plus importantes que les cellules mémoires sont petites.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les cellules mémoires
| prevText=Les cellules mémoires
| next=Contrôleur mémoire interne
| nextText=Le contrôleur mémoire interne
}}
</noinclude>
mq3v9namtblr3pm8nfbcdtrslslptsx
682035
682030
2022-07-20T13:46:32Z
Mewtow
31375
/* Annexe : l'attaque rowhammer */
wikitext
text/x-wiki
Avec le chapitre précédent, on sait que les RAM et ROM contiennent des cellules mémoires, qui mémorisent chacune un bit. On pourrait croire que cela suffit à créer une mémoire, mais il n'en est rien. Il faut aussi des circuits pour gérer l'adressage, le sens de transfert (lecture ou écriture), et bien d'autres choses. Schématiquement, on peut subdiviser toute mémoire en plusieurs circuits principaux.
* La mémorisation des informations est prise en charge par le '''plan mémoire'''. Il est composé d'un regroupement de cellules mémoires, auxquelles on a ajouté quelques fils pour communiquer avec le bus.
* La gestion de l'adressage et de la communication avec le bus sont assurées par un circuit spécialisé : le '''contrôleur mémoire''', composé d'un décodeur et de circuits de contrôle.
* L''''interface avec le bus''' relie le plan mémoire au bus de données. C'est le plus souvent ici qu'est géré le sens de transfert des données, ainsi que tout ce qui se rapporte aux lectures et écritures.
[[File:Td6bfig1.png|centre|vignette|upright=2|Organisation interne d'une mémoire adressable.]]
Nous allons étudier le plan mémoire dans ce chapitre, le contrôleur mémoire et l'interface avec le bus seront vu dans les deux chapitres suivants. Cela peut paraitre bizarre de dédier un chapitre complet au plan mémoire, mais il y a de quoi. Celui-ci n'est pas qu'un simple amoncellement de cellules mémoire et de connexions vaguement organisées. On y trouve aussi des circuits électroniques aux noms barbares : amplificateur de tension, égaliseur de ligne de bit, circuits de pré-charge, etc. L'organisation des fils dans le plan mémoire est aussi intéressante à étudier, celle-ci étant bien plus complexe qu'on peut le croire.
==Les fils et signaux reliés aux cellules==
[[File:Transparent Latch Symbol.svg|vignette|upright=0.5|Interface d'une bascule D.]]
Le plan mémoire est surtout composé de fils, sur lesquels on connecte des cellules mémoires. Rappelons que les cellules mémoires se présentent avec une interface simple, qui contient des broches pour le transfert des données et d'autres broches pour les commandes de lecture/écriture. Reste à voir comment toutes ses broches sont reliées aux différents bus et au contrôleur mémoire. Ce qui va nous amener à parler des lignes de bit et des signaux de sélection de ligne. Il faut préciser que la distinction entre broches de commande et de données est ici très importante : les broches de données sont connectées indirectement au bus, alors que les broches de commande sont reliées au contrôleur mémoire. Aussi, nous allons devoir parler des deux types de broches dans des sections séparées.
===La connexion des broches de données : les lignes de bit===
Afin de simplifier l'exposé, nous allons étudier une mémoire série dont le byte est de 1 bit. Une telle mémoire est dite '''bit-adressable''', c’est-à-dire que chaque bit de la mémoire a sa propre adresse. Nous étudierons le cas d'une mémoire quelconque plus loin, et ce pour une raison : on peut construire une mémoire quelconque en améliorant le plan mémoire d'une mémoire bit-adressable, d'une manière assez simple qui plus est. Parler de ces dernières est donc un bon marche-pied pour aboutir au cas général.
====Le cas d'une mémoire bit-adressable====
Une mémoire bit-adressable est de loin celle qui a le plan mémoire le plus rudimentaire. Quand on sélectionne un bit, avec son adresse, son contenu va se retrouver sur le bus de données. Dit autrement, la cellule mémoire va se connecter sur ce fils pour y placer son contenu. On devine donc comment est organisé le plan mémoire : il est composé d'un fil directement relié au bus de donnée, sur lequel les cellules mémoire se connectent si besoin. Le plan mémoire se résume donc à un ensemble de cellules mémoires dont l'entrée/sortie est connectée à un unique fil. Ce fil s'appelle la '''ligne de bit''' (''bitline'' en anglais).
[[File:Plan mémoire simplifié d'une mémoire bit-adressable.png|centre|vignette|upright=1|Plan mémoire simplifié d'une mémoire bit-adressable.]]
En réalité, peu de mémoires suivent actuellement le principe précédent. Les mémoires assez évoluées utilisent deux lignes de bit par colonne ! La première transmet le bit lu et l'autre son inverse, ce qui se marie bien avec le fait que certaines bascules fournissent le bit et son inverse sur deux broches distinctes. La mémoire utilise la différence de tension entre ces deux fils pour représenter le bit lu ou à écrire. Un tel codage est appelé un '''codage différentiel'''. L'utilité d'un tel codage assez difficile à expliquer sans faire intervenir des connaissances en électricité, mais tout est une histoire de fiabilité et de résistance aux parasites électriques.
[[File:Bitlines différentielles.png|centre|vignette|upright=1|Bitlines différentielles.]]
Certaines mémoires ont amélioré les lignes de bit différentielles en interchangeant leur place à chaque cellule mémoire. La ligne de bit change donc de côté à chaque passage d'une cellule mémoire. Cette organisation porte le nom de '''lignes de bit croisées'''.
[[File:Bitlines croisées.png|centre|vignette|upright=1|Bitlines croisées.]]
Les lignes de bit différentielles se marient assez mal avec les cellules mémoire de DRAM, qui n'ont pas de seconde sortie pour lire l'inverse du bit stocké. Malgré tout, l'usage de lignes de bit différentielle est possible, bien que compliqué, grâce à des techniques comme l'usage de cellules factices (nous en reparlerons plus bas). Cependant, il est possible d'utiliser une organisation intermédiaire entre des lignes de bit simple et des lignes différentielles, qui connecte des cellules consécutives comme illustré ci-dessous. On voit qu'il y a deux lignes de bit : la moitié des cellules est connectée à la première ligne, l'autre moitié à la seconde, avec une alternance entre cellules consécutives. Cela permet d'avoir moins de cellules mémoires connectées sur le même fil, ce qui améliore certains paramètres électriques des lignes de bit. Cette organisation porte le nom de '''ligne de bit repliée'''.
[[File:Lignes de bit repliées.png|centre|vignette|upright=1|Lignes de bit repliées]]
====Le cas d'une mémoire quelconque (avec byte > 1)====
Après avoir vu le cas des mémoires bit-adressables, il est temps d'étudier les mémoires quelconques, celles où un byte contient plus que 1 bit. Surprenamment, ces mémoires peuvent être conçues en utilisant plusieurs mémoires bit-adressables. Par exemple, prenons une mémoire dont le byte fait deux bits (ce qui est rare, convenons-en). On peut l'émuler à partir de deux mémoires de 1 bit : la première stocke le bit de poids faible de chaque byte, alors que l'autre stock le bit de poids fort. Et on peut élargir le raisonnement pour des bytes de 3, 4, 8, 16 bits, et autres. Par exemple, pour une mémoire dont le byte fait 64 bits, il suffit de mettre en parallèle 64 mémoires de 1 bit.
Mais cette technique n'est pas appliquée à la lettre, car il y a moyen d'optimiser le tout. En effet, on ne va pas mettre effectivement plusieurs mémoires bit-adressables en parallèle, car seuls les plans mémoires doivent être dupliqués. Si on utilisait effectivement plusieurs mémoires, chacune aurait son propre plan mémoire, mais aussi son propre contrôleur mémoire, ses propres circuits de communication avec le bus, etc. Or, ces circuits sont en fait redondants dans le cas qui nous intéresse.
Prenons le cas du contrôleur mémoire, qui reçoit l'adresse à lire/écrire et qui envoie les signaux de commande au plan mémoire. Avec N mémoires en parallèle, N contrôleurs mémoire recevront l'adresse et généreront les N mêmes signaux, qui seront envoyés à N plans mémoire distincts. Au lieu de cela, il est préférable d'utiliser un seul contrôleur mémoire, mais de dupliquer les signaux de commande en autant N exemplaires (autant qu'il y a de plan mémoire). Et c'est ainsi que sont conçues les mémoires quelconques : pour un byte de N bits, il faut prendre N plans mémoires de 1 bit. Cela demande donc d'utiliser N lignes de bits, reliée convenablement aux cellules mémoires. Le résultat est un rectangle de cellules mémoires, où chaque colonne est traversée par une ligne de bit. Chaque ligne du tableau/rectangle, correspond à un byte, c'est-à-dire une case mémoire.
Là encore, chaque colonne peut utiliser des lignes de bits différentielles ou croisées.
[[File:Plan mémoire, avec les bitlines.png|centre|vignette|upright=1.5|Plan mémoire, avec les bitlines.]]
===La connexion des broches de commande : le transistor et le signal de sélection===
Évidemment, les cellules mémoires ne doivent pas envoyer leur contenu sur la ligne de bit en permanence. En réalité, chaque cellule est connectée sur la ligne de bit selon les besoins. Les cellules correspondant au mot adressé se connectent sur la ligne de bit, alors que les autres ne doivent pas le faire. La connexion des cellules mémoire à la ligne de bit est réalisée par un interrupteur commandable, c’est-à-dire par un transistor appelé '''transistor de sélection'''. Quand la cellule mémoire est sélectionnée, le transistor se ferme, ce qui connecte la cellule mémoire à la ligne de bit. À l'inverse, quand une cellule mémoire n'est pas sélectionnée, le transistor de sélection se comporte comme un interrupteur ouvert : la cellule mémoire est déconnectée du bus.
La commande du transistor de sélection est effectuée par le contrôleur mémoire. Pour chaque ligne de bit, le contrôleur mémoire n'ouvre qu'un seul transistor à la fois (celui qui correspond à l'adresse voulue) et ferme tous les autres. La correspondance entre un transistor de sélection et l'adresse est réalisée dans le contrôleur mémoire, par des moyens que nous étudierons dans les prochains chapitres. Toujours est-il que le contrôleur mémoire génère, pour chaque octet, un bit qui dit si celui-ci est adressé ou non. Ce bit est appelé le '''signal de sélection'''. Le signal de sélection est envoyé à toutes les cellules mémoire qui correspondent au byte adressé. Vu que tous les bits d'un byte sont lus ou écrits en même temps, toutes les cellules correspondantes doivent être connectées à la ligne de bit en même temps, et donc tous les transistors de sélection associés doivent se fermer en même temps. En clair, le signal de sélection est partagé par toutes les cellules d'un même mot mémoire.
[[File:Signal row line.png|centre|vignette|upright=2.5|Signal de sélection et Byte.]]
====Le cas des lignes de bit simples et repliées====
Voyons comment les bitlines simples sont reliées aux cellules mémoires. Les mémoires 1T-DRAM n'ont qu'une seule broche entrée/sortie, sur laquelle on effectue à la fois les lectures et les écritures. Cela se marie très bien avec des bitlines simples, mais ça les rend incompatibles avec des bitlines différentielles. Le cas des DRAM à bitlines simples, avec une seule sortie, un seul transistor de sélection, est illustré ci-dessous. On peut noter que les cellules de SRAM peuvent malgré tout s’accommoder de bitlines simples : il suffit de connecter la sortie Q sur la bitline simple et ne pas connecter la sortie <math>\overline{Q}</math> à quoi que ce soit. On peut même en profiter pour supprimer le transistor de sélection de cette sortie, ce qui réduit le nombre de transistors à seulement 5.
[[File:Plan mémoire d'une mémoire bit-adressable.png|centre|vignette|upright=1.5|Plan mémoire d'une mémoire bit-adressable.]]
La connexion des transistors de sélection pour des lignes de bit repliée n’est pas très différente de celle des lignes de bit simple. Elle est illustrée ci-dessous.
[[File:Ligne de bit repliée.png|centre|vignette|upright=1.5|Ligne de bit repliée.]]
====Le cas des lignes de bit différentielles====
Le cas des mémoires SRAM est de loin le plus simple à comprendre. Celles-ci utilisent toutes (ou presque) des bitlines différentielles, chose qui se marie très bien avec l'interface des cellules SRAM. Rappelons que celle-ci possèdent deux broches pour les données : une broche Q sur laquelle on peut lire ou écrire un bit, et une broche complémentaire sur laquelle on récupère l'inverse du bit lu. À chaque broche correspond un transistor de sélection différents. Dans ce cas, la sortie Q est connectée sur une bitline, alors que l'autre sortie complémentaire <math>\overline{Q}</math> l'est sur l'autre bitline.
[[File:Connexion d'une cellule mémoire de SRAM à une bitline différentielle.png|centre|vignette|upright=2|Connexion d'une cellule mémoire de SRAM à une bitline différentielle.]]
Précisons que les DRAM se marient assez mal avec l'usage de lignes de bit différentielle, vu qu'elles n'ont pas de sortie complémentaire <math>\overline{Q}</math>, mais n'ont qu'une seule sortie Q. Mais quelques astuces permettent d'utiliser des lignes de bit différentielles sur ces mémoires. La plus connue est de loin l'utilisation de '''cellules factices''' (''dummy cells''), des cellules mémoires vides placées aux bouts des lignes de bit. Lors d'une lecture, ces cellules vides se remplissent avec l'inverse du bit à lire. La ligne de bit inverse (celle qui contient l'inverse du bit) est alors remplie avec le contenu de la cellule factice, ce qui donne bien un signal différentiel. Le bit inversé est fournit par une porte logique qui inverse la tension fournie par la cellule mémoire. Cette tension remplis alors la cellule factice, avec l'inverse du bit lu.
====Le cas des cellules mémoires double port====
Après avoir vu les cellules mémoire "normales" plus haut, il est temps de passer aux cellules mémoire de type double port. Pour simplifier, les cellules double port possèdent une sortie pour les lectures et une entrée pour les écritures, toutes deux d'un bit. On peut les utiliser pour concevoir des mémoires double port, mais aussi des mémoires simple port.
Cette particularité est exploitée pour créer des mémoires double-port bit-adressables, qui ont une broche pour les lectures et une autre pour les écritures. Le plan mémoire contient alors deux lignes de bit, une pour la broche de lecture et une autre pour la broche d'écriture. Le transistor de lecture est connecté à la ligne de bit de lecture, alors que celui pour l'écriture est relié à la ligne de bit d'écriture.
[[File:Plan mémoire d'une SRAM double port.png|centre|vignette|upright=2|Plan mémoire d'une SRAM double port.]]
Pour les mémoires simple port, c'est-à-dire avec une seule broche qui sert à la fois pour les lectures et écritures, les deux transistors sont reliés à la même ligne de bit. Ils vont s'ouvrir ou se fermer selon les besoins, sous commande du contrôleur mémoire.
[[File:Plan mémoire d'une SRAM simple port.png|centre|vignette|upright=2|Plan mémoire d'une SRAM simple port.]]
Dans les deux cas, le contrôleur mémoire est relié directement aux transistors de sélection. Il doit générer à la fois les signaux d'autorisation de lecture que ceux pour l'écriture. Ces deux signaux peuvent être déduit du bit de sélection et du bit R/W, comme vu dans le chapitre précédent.
[[File:Circuit d'interface entre contrôleur mémoire et cellule mémoire.png|centre|vignette|upright=1.5|Circuit d'interface entre contrôleur mémoire et cellule mémoire.]]
==L'amplificateur de tension==
Quand on connecte une cellule mémoire à une ligne de bit, c'est à la cellule mémoire de fournir le courant pour mettre la ligne à 0 ou à 1. Mais dans certains cas, la cellule mémoire ne peut pas fournir assez courant pour cela. Cela arrive surtout sur les mémoires DRAM, basées sur un condensateur. Ces condensateurs ont une faible capacité et ne peuvent pas conserver beaucoup d'électrons, surtout sur les mémoires modernes. Du fait de la miniaturisation, les condensateurs des DRAM actuelles ne peuvent stocker que quelques centaines d'électrons, parfois beaucoup moins. Autant dire que la vidange du condensateur dans la ligne de bit ne suffit pas à la mettre à 1, même si la cellule mémorisait bien un 1. La lecture crée à peine une tension de quelques millivolts dans la ligne de bit, pas plus. Mais il y a une solution : amplifier la tension de quelques millivolts induite par la vidange du condensateur sur le bus. Pour cela, il faut donc placer un dispositif capable d'amplifier cette tension, bien nommé '''amplificateur de lecture'''.
[[File:Differential amplifier.svg|vignette|Amplificateur différentiel.]]
L’amplificateur utilisé n'est pas le même avec des lignes de bit simples et des lignes de bit différentielles. Dans le cas différentiel, l'amplificateur doit faire la différence entre les tensions sur les deux lignes de bit et traduire cela en un niveau logique. C'est l'amplificateur lui-même qui fait la conversion entre codage différentiel (sur deux lignes de bit) et codage binaire. Pour le distinguer des autres amplificateurs, il porte le nom d''''amplificateur différentiel'''. L'amplificateur différentiel possède deux entrées, une pour chaque ligne de bit, et une sortie. Dans ce qui va suivre, les entrées seront notées <math>V_{in}^+</math> et <math>V_{in}^-</math>, la sortie sera notée <math>V_{out}</math>. L’amplificateur différentiel fait la différence entre ces deux entrées et amplifie celle-ci. En clair :
: <math>V_{out} = A \times ( V_{in}^+ - V_{in}^- )</math>
Il faut noter qu'un amplificateur différentiel peut fonctionner aussi bien avec des lignes de bit différentielles qu'avec des lignes de bit simples. Avec des lignes de bit simples, il suffit de placer l'autre entrée à la masse, au 0 Volts, et de n'utiliser qu'une seule sortie.
Il existe de nombreuses manières de concevoir un amplificateur différentiel, mais nous n'allons aborder que les circuits les plus simples. Dans les grandes lignes, il existe deux types d'amplificateurs de lecture : ceux basés sur des bascules et ceux basés sur une paire différentielle. Bizarrement, vous verrez que les deux ont une certaine ressemblance avec les cellules de SRAM ! Il faut dire qu'une porte NON, fabriquée avec des transistors, est en réalité un petit amplificateur spécialisé, chose qui tient au fonctionnement de son circuit.
===L'amplificateur de lecture à paire différentielle===
[[File:Long tailed pair.svg|vignette|Paire différentielle. Le générateur de courant est en jaune, la charge est en bleu. Ici, la charge est un miroir de courant. Les transistors ne sont pas des transistors MOS, mais le circuit fonctionne de la même manière que si c'était le cas.]]
Le premier type d'amplificateur différentiel est la '''paire différentielle''', composée de deux transistors mis en série avec une charge et un générateur de courant. La charge est placée entre la tension d'alimentation et le transistor, alors que le générateur de courant est placé entre le transistor et la tension basse (la masse, ou l'opposé de la tension d'alimentation, selon le montage). Le circuit ci-contre illustre le circuit de la paire différentielle.
Précisons que la charge mentionnée précédemment varie selon le circuit, de même que le générateur de courant. Dans le cas le plus simple, une simple résistance suffit pour les deux. Mais ce n'est pas cette solution qui est utilisée dans les mémoires actuelles. En effet, intégrer des résistances est compliqué dans les circuits à semi-conducteurs modernes, et les mémoires RAM en sont. Si on peut y intégrer des résistances, des condensateurs ou des inductances/bobines, c'est une chose très complexe et qui ne vaut généralement pas le coup. Aussi, les résistances et condensateurs sont généralement remplacés par des circuits équivalents, qui ont le même rôle ou qui peuvent remplacer une résistance dans le montage voulu.
[[File:Differential amplifier long-tailed pair.svg|centre|vignette|Paire différentielle, avec des résistances.]]
Le générateur de courant et la charge doivent être fabriqués avec des transistors MOS, voire CMOS, ce qui n'est pas un problème. Chacun de ces circuits est remplacée par un ''miroir de courant'', à savoir un circuit qui crée un courant constant sur une sortie et le recopie sur une seconde sortie. L'avantage est que le miroir de courant fournit le même courant aux deux ''bitlines'', il égalise les courants dans les deux bitlines. Il existe plusieurs manières de créer un tel miroir de courant avec des transistors MOS/CMOS, la plus simple étant illustrée ci-dessous. On pourrait aborder le fonctionnement d'un tel circuit, pourquoi il fonctionne, mais nous n'en parlerons pas ici. Cela relèverait plus d'un cours d'électronique analogique, et demanderait de connaître en détail le fonctionnement d'un transistor, les équations associées, etc.
[[File:Einfacher Stromspiegel MOSFET1.svg|centre|vignette|Miroir de courant fabriqué avec des transistors MOS.]]
===L'amplificateur de lecture à verrou===
Le second type d'amplificateur de lecture est l''''amplificateur à verrou'''. Il amplifie une différence de tension entre les deux lignes de bit d'une colonne différentielle. Les deux colonnes doivent être préchargées à Vdd/2, à savoir la moitié de la tension d'alimentation. La raison à cela deviendra évidente dans les explications qui vont suivre. Toujours est-il que ce circuit a besoin qu'un circuit dit de précharge s'occupe de placer la tension adéquate sur les lignes de bit, avant toute lecture ou écriture. Nous reparlerons de ce circuit de précharge dans les sections suivantes, vers la fin de ce chapitre. Cela peu paraître peu pédagogique, mais à notre décharge, sachez que le circuit de précharge et l'amplificateur de lecture sont intimement liés. Il est difficile de parler de l'un sans parler de l'autre et réciproquement. Pour le moment, tout ce que vous avez à retenir est qu'avant toute lecture, les lignes de bit sont chargées à Vdd/2, ce qui permet à l'amplificateur à verrou de fonctionner correctement.
====Le circuit de l'amplificateur de lecture à verrou====
L'amplificateur à verrou est composé de deux portes NON reliées tête-bêche, comme dans une cellule de SRAM. Chaque ligne de bit est reliée à l'autre à travers une porte NON. Sauf que cette fois-ci, il n'y a pas toujours de transistors de sélection, ou alors ceux-ci sont placés autrement.
[[File:Latch-type sense amplifier.png|centre|vignette|upright=1|Amplificateur de lecture à bascule.]]
Le circuit complet est illustré ci-dessous, de même qu'une version plus détaillée avec des transistors. Du fait de son câblage, l'amplificateur à verrou a pour particularité d'avoir des broches d'entrées qui se confondent avec celles de sortie : l'entrée et la sortie pour une ligne de bit sont fusionnées en une seule broche. L'utilisation d'inverseurs explique intuitivement pourquoi il faut précharger les lignes de bit à Vdd/2 : cela place la tension dans la zone de sécurité des deux inverseurs, là où la tension ne correspond ni à un 0, ni à un 1. Le fonctionnement du circuit dépend donc du fonctionnement des transistors, qui servent alors d'amplificateurs.
[[File:Amplificateur de lecture à bascule, version détaillée.png|centre|vignette|upright=2|Amplificateur de lecture à bascule, version détaillée.]]
On peut noter que cet amplificateur est parfois fabriqué avec des transistors bipolaires, qui consomment beaucoup de courant. Mais même avec des transistors MOS, il est préférable de réduire la consommation électrique du circuit, quand bien même ceux-ci consomment peu. Pour cela, on peut désactiver l’amplificateur quand on ne l'utilise pas. Pour cela, on entoure l'amplificateur avec des transistors qui le débranchent, le déconnectent si besoin.
[[File:Amplificateur de lecture à bascule, avec transistors d'activation.png|centre|vignette|upright=2|Amplificateur de lecture à bascule, avec transistors d'activation.]]
====Le fonctionnement de l'amplificateur à verrou====
Expliquer en détail le fonctionnement de l'amplificateur à verrou demanderait de faire de l'électronique assez poussée. Il nous faudrait détailler le fonctionnement d'un transistor quand il est utilisé en tant qu'amplificateur, donner des équations, et bien d'autres joyeusetés. À la place, je vais donner une explication très simplifiée, que certains pourraient considérer comme fausse (ce qui est vrai, dans une certaine mesure).
Avant toute chose, précisons que les seuils pour coder un 0 ou un 1 ne sont pas les mêmes entre l’entrée d'une porte NON et sa sortie. Ils sont beaucoup plus resserrés sur l'entrée, la marge de sécurité entre 1 et 0 étant plus faible. Un signal qui ne correspondrait pas à un 0 ou un 1 en sortie peut l'être en entrée.
Le fonctionnement du circuit ne peut s'expliquer correctement qu'à partir du rapport entre tension à l'entrée et tension de sortie d'une porte NON. Le schéma ci-dessous illustre cette relation. On voit que la porte logique amplifie le signal d'entrée en plus de l'inverser. Pour caricaturer, on peut décomposer cette caractéristique en trois parties : deux zones dites de saturation et une zone d'amplification. Dans la zone de saturation, la tension est approximativement égale à la tension maximale ou minimale, ce qui fait qu'elle code pour un 0 ou un 1. Entre ces deux zones extrêmes, la tension de sortie dépend linéairement de la tension d'entrée (si on omet l'inversion).
[[File:Caractéristique tension d'entrée-tension de sortie d'un inverseru CMOS.png|centre|vignette|upright=2|Caractéristique tension d'entrée-tension de sortie d'un inverseur CMOS.]]
Quand on place deux portes NON l'une à la suite de l'autre, le résultat est un circuit amplificateur, dont la caractéristique est illustrée dans le second schéma. On voit que l'amplificateur amplifie la différence de tension entre VDD/2 et la tension d'entrée (sur la ligne de bit).
[[File:Utilisation de deux portes NON comme amplificateur de tension.png|centre|vignette|upright=2|Utilisation de deux portes NON comme amplificateur de tension.]]
Si on regarde le circuit complet, on s’aperçoit que chaque ligne de bit est bouclée sur elle-même, à travers cet amplificateur. Cela fait boucler la sortie de l'amplificateur sur son entrée : la tension de base est alors amplifiée une fois, puis encore amplifiée, et ainsi de suite. Au final, les seuls points stables du montage sont la tension maximale ou la tension minimale, soit un 0 ou un 1, ou la tension VDD/2.
Ceci étant dit, on peut enfin comprendre le fonctionnement complet du circuit d'amplification. Commençons l'explication par la situation initiale : la ligne de bit est préchargée à VDD/2, et la cellule mémoire est déconnectée des lignes de bit. La ligne de bit est préchargée à VDD/2, l'amplificateur a sa sortie comme son entrée égales à VDD/2 et le circuit est parfaitement stable. Ensuite, la cellule mémoire à lire est connectée à la ligne de bit et la tension va passer au-dessous ou au-dessus de VDD/2. Nous allons supposer que celle-ci contenait un 1, ce qui fait que sa connexion entraîne une montée de la tension de la ligne de bit. La tension ne va cependant pas monter de beaucoup, mais seulement de quelques millivolts. Cette différence de tension va être amplifiée par les deux portes logiques, ce qui amplifie la différence de tension. Et rebelote : cette différence amplifiée est ré-amplifiée par le montage, et ainsi de suite jusqu’à ce que le circuit se stabilise soit à 0 soit à 1.
[[File:Fonctionnement très simplifié de l'amplificateur à verrou.png|centre|vignette|upright=2|Fonctionnement très simplifié de l'amplificateur à verrou.]]
==L'optimisation du temps de charge/décharge des lignes de bit==
Si les lignes de bit sont de simples fils conducteurs passifs, cela ne veut pas dire qu'ils n'ont pas d'influence sur les lectures et écritures. En réalité, ils jouent un grand rôle dans la rapidité des accès mémoire, pour des raisons techniques. Selon leur longueur, la tension va prendre plus ou moins de temps pour s'établir dans la ligne de bit, ce qui impacte directement les performances de la mémoire. Diverses techniques ont étés inventées pour résoudre ce problème, la plus importante étant l'utilisation d'un circuit dit de pré-charge, que nous allons étudier maintenant.
Les lignes de bit ne sont pas des fils parfaits : non seulement ils ont une résistance électrique, mais ils se comportent aussi comme des condensateurs (dans une certaine mesure). Nous n'expliquerons pas dans la physique de ce phénomène, mais allons simplement admettre qu'un fil électrique se modélise bien en mettant une résistance R en série avec un condensateur C : le circuit obtenu est un circuit RC. Le condensateur, appelée '''capacité parasite''', n’apparaît que lorsque la tension de la ligne de bit change en passant de 0 à 1 ou inversement. Ce qui n'arrive que lors d'une lecture ou écriture, cela va de soit. Lorsque l'on change la tension en entrée d'un tel montage, la tension de sortie met un certain temps avant d'atteindre la valeur d'entrée. Ce qui est illustré dans les deux schémas ci-dessous, pour la charge (passage de 0 à 1) et la décharge (passage de 1 à 0). La variation est d'ailleurs exponentielle. On estime qu'il faut un temps égal <math>t \approx 3 \times R \cdot C</math>, avec R la valeur de la résistance et C celle du condensateur. En clair : la ligne de bit met un certain temps avant que la tension atteigne celle qui correspond au bit lu ou à écrire.
{|class="wikitable flexible"
|[[File:RC Series Filter (with V&I Labels).svg|300px|class=transparent|Circuit RC série.]]
|[[File:Series RC capacitor voltage.svg|300px|class=transparent|Tension aux bornes d'un circuit RC en charge.]]
|[[File:Series RC resistor voltage.svg|300px|class=transparent|Tension aux bornes d'un circuit RC en cours de décharge.]]
|}
===L'organisation du plan mémoire===
Une première idée pour optimiser le temps RC est de diminuer la résistance de la ligne. Il se trouve que celle-ci est proportionnelle à la longueur de la ligne de bit : plus la ligne de bit est longue, plus la résistance R sera élevée. On voit donc une première solution pour réduire la résistance, et donc le temps RC : réduire la taille des lignes de bit. Les petites mémoires, avec peu de cellules sur une colonne, ont des lignes de bit plus petites et sont donc plus rapides. Cela explique en partie pourquoi les temps d'accès des mémoires varient selon la capacité, chose que nous avons abordé il y a quelques chapitres. De même, à capacité égale, il vaut mieux utiliser des bytes large, pour réduire la taille des colonnes.
====L'agencement en colonne de donnée ouvertes====
Mais d'autres optimisations du plan mémoire permettent d'obtenir des lignes de bit plus petites, à capacité et largeur de byte inchangée. Par exemple, on peut placer l'amplificateur de lecture au milieu du plan mémoire, et non au bout. En faisant ainsi, on doit couper la ligne de bit en deux, chaque moitié étant placée d'un côté ou de l'autre de l’amplificateur. La colonne contient ainsi deux lignes de bits séparées, chacune ayant une longueur réduite de moitié. Cette organisation est dite '''en colonne de donnée ouvertes'''. Mais cette organisation a un défaut : il est difficile d'implémenter l'amplificateur au milieu de la mémoire. Le nombre de fils qui doivent passer par le milieu de la RAM est important, rendant le câblage compliqué. De plus, les perturbations électromagnétiques ne touchent pas de la même manière chaque côté de la mémoire et l'amplificateur peut donner des résultats problématiques à cause d'elles.
[[File:Optimisations du plan mémoire pour réduire la taille des bitlines.png|centre|vignette|upright=2.5|Optimisations du plan mémoire pour réduire la taille des bitlines.]]
Il est aussi possible de répartir les amplificateurs de tension autrement. On peut mélanger les organisations en colonne de données ouvertes et "normales", en mettant les amplificateurs à la fois au milieu de la RAM et sur les bords. Une moitié des amplificateurs est placée au milieu du plan mémoire, l'autre moitié est placée sur les bords. On alterne les lignes de bits connectée entre amplificateurs selon qu’ils sont sur les bords ou au milieu. L'organisation est illustrée ci-dessous.
[[File:Organisation en colonnes de données ouvertes, avec répartition des amplificateur sur les bords du plan mémoire.png|centre|vignette|upright=2.5|Organisation en colonnes de données ouvertes, avec répartition des amplificateur sur les bords du plan mémoire.]]
===La pré-charge des lignes de bit===
[[File:Sense Amp position.jpg|vignette|Aperçu d'une ligne de bit conçue pour être préchargée. On voit qu'il s'agit d'une ligne de bit "normale", à laquelle a été ajouté un circuit qui permet de charger la ligne à partir de la tension d'alimentation. L'amplificateur de tension est situé du côté opposé au circuit de charge.]]
Une autre solution, beaucoup plus ingénieuse, ne demande pas de modifier la longueur des lignes de bit. À la place, on rend leur charge plus rapide en les pré-chargeant. Sans pré-charge, la ligne de bit est à 0 Volts avant la lecture et la lecture altère cette tension, que ce soit pour la laisser à 0 (lecture d'un 0), ou pour la faire monter à la tension maximale Vdd (lecture d'un 1). Le temps de réaction de la ligne de bit dépend alors du temps qu'il faut pour la faire monter à Vdd. Avec la pré-charge, la ligne de bit est chargée avant la lecture, de manière à la mettre à la moitié de Vdd. La lecture du bit fera descendre celle-ci à 0 (lecture d'un 0) ou la faire grimper à Vdd (lecture d'un 1). Le temps de charge ou de décharge est alors beaucoup plus faible, vu qu'on part du milieu.
: Il faut noter que la pré-charge à Vdd/2 est un cas certes simple à comprendre, mais qui n'a pas valeur de généralité. Certaines mémoires pré-chargent leurs lignes de bit à une autre valeur, qui peut être Vdd, à 60% de celui-ci, ou une autre valeur. En fait, tout dépend de la technologie utilisée. Par exemple, Les mémoires de type CMOS pré-chargent à Vdd/2, alors que les mémoires TTL, NMOS ou PMOS pré-chargent à une autre valeur (le plus souvent Vdd).
On peut penser qu'il faudra deux fois moins de temps, mais la réalité est plus complexe (regardez les graphes de charge/décharge situés plus haut). De plus, il faut ajouter le temps mis pour précharger la ligne de bit, qui est à ajouter au temps de lecture proprement dit. Sur la plupart des mémoires, la pré-charge n'est pas problématique. Il faut dire qu'il est rare que la mémoire soit accédé en permanence et il y a toujours quelques temps morts pour pré-charger la ligne de bit. On verra que c'est notamment le cas sur les mémoires DRAM synchrones modernes, comme les SDRAM et les mémoires DDR. Mais passons...
====Les circuits de précharge====
La pré-charge d'une ligne de bit se fait assez facilement : il suffit de connecter la ligne de bit à une source de tension qui a la valeur adéquate. Par exemple, une mémoire qui se pré-charge à Vdd a juste à relier la ligne de bit à la tension d'alimentation. Mais attention : cette connexion doit disparaître quand on lit ou écrit un bit dans les cellules mémoire. Sans cela, le bit envoyé sur la ligne de bit sera perturbé par la tension ajoutée. Il faut donc déconnecter la ligne de bit de la source d'alimentation lors d'une lecture écriture. On devine rapidement que le circuit de pré-charge est composé d'un simple interrupteur commandable, placé entre la tension d'alimentation (Vdd ou Vdd/2) et la ligne de bit. Le contrôleur mémoire commande cet interrupteur pour précharger la ligne de bit ou stopper la pré-charge lors d'un accès mémoire. Si un seul transistor suffit pour les lignes de bit simples, deux sont nécessaires pour les lignes de bit différentielles ou croisées. Ils doivent être ouvert et fermés en même temps, ce qui fait qu'ils sont commandés par un même signal.
[[File:Circuits de précharge.png|centre|vignette|upright=2.5|Circuits de précharge]]
====L'égaliseur de tension====
Pour les lignes de bit différentielles et croisées, il se peut que les deux lignes de bit complémentaires n'aient pas tout à fait la même tension suite à la pré-charge. Pour éviter cela, il est préférable d'ajouter un '''circuit d'égalisation''' qui égalise la tension sur les deux lignes. Celui-ci est assez simple : c'est un interrupteur commandable qui connecte les deux lignes de bit lors de la pré-charge. Là encore, un simple transistor suffit. L'égalisation et la pré-charge ayant lieu en même temps, ce transistor est commandé par le même signal que celui qui active le circuit de précharge. Le circuit complet, qui fait à la fois pré-charge et égalisation des tensions, est représenté ci-dessous.
[[File:Circuits de précharge et d'égalisation pour des lignes de bit différentielles.png|centre|vignette|upright=2.5|Circuits de précharge et d'égalisation pour des lignes de bit différentielles.]]
==Annexe : l'attaque ''rowhammer''==
Vous connaissez maintenant comment fonctionnent les cellules mémoires et le plan mémoire, ce qui fait que vous avez les armes nécessaires pour aborder des sujets assez originaux. Profitons-en pour aborder une faille de sécurité présente dans la plupart des mémoires DRAM actuelles : l'attaque ''row hammer''. Vous avez bien entendu : il s'agit d'une faille de sécurité matérielle, qui implique les mémoires RAM, qui plus est. Voilà qui est bien étrange. D'ordinaire, quand on parle de sécurité informatique, on parle surtout de failles logicielles ou de problèmes d'interface chaise-clavier. La plupart des attaques informatiques sont des attaques d’ingénierie sociale où on profite de failles humaines pour obtenir un mot de passe ou toute autre information confidentielle, suivies par les failles logicielles, les virus, malwares et autres méthodes purement logicielles. Mais certaines failles de sécurités sont purement matérielles et profitent de bugs présents dans le matériel pour fonctionner. Car oui, les processeurs, mémoires, bus et périphériques peuvent avoir des bugs matériels qui sont généralement bénins, mais que des virus, logiciels ou autres malware peuvent exploiter pour commettre leur méfaits.
[[File:Row hammer.svg|vignette|Attaque Row hammer - les lignes voisines en jaune sont accédées un grand nombre de fois à la suite, la ligne violette est altérée.]]
L'attaque ''row hammer'', aussi appelée attaque par '''martèlement de mémoire''', utilise un bug de conception des mémoires DRAM. Le bug en question tient dans le fait que les cellules mémoires ne sont pas parfaites et que leur charge électrique tend à fuir. Ces fuites de courant se dispersent autour de la cellule mémoire et tendent à affecter les cellules mémoires voisines. En temps normal, cela ne pose aucun problème : les fuites sont petites et l'interaction électrique est limitée. Cependant, des hackers ont réussit à exploiter ce comportement pour modifier le contenu d'une cellule mémoire sans y accèder. En accédant d'une manière bien précise à une ligne de la mémoire, on peut garantir que les fuites de courant deviennent signifiantes, suffisamment pour recopier le contenu d'une ligne mémoire dans les lignes mémoires voisines. Pour cela, il faut accéder un très grand nombre de fois à la cellule mémoire en question, ce qui explique pourquoi cette attaque s'appelle le ''martèlement'' de mémoire. Une autre méthode, plus fiable, est d’accéder à deux lignes de mémoires, qui prennent en sandwich la ligne mémoire à altérer. On accède successivement à la première, puis la seconde, avant de reprendre au début, et cela un très grand nombre de fois par secondes.
Modifier plusieurs bytes sans y accéder, mais en accédant à leurs voisins est une faille exploitable par les pirates informatiques. L'intérêt est de contourner les protections mémoires liées au système d'exploitation. Sur les systèmes d'exploitation modernes, chaque programme se voit attribuer certaines portions de la mémoire, auxquelles il est le seul à pouvoir accéder. Des mécanismes de protection mémoire intégré dans le processeur permettent d'isoler la mémoire de chaque programme, comme nous le verrons dans le chapitre sur la mémoire virtuelle. Mais avec ''row hammer'', les accès à un byte attribué à un programme peuvent déborder sur les bytes d'un autre programme, avec des conséquences assez variables. Par exemple, un virus présent en mémoire pourrait interagir avec le byte qui mémorise un mot de passe ou une clé de sécurité RSA, ou toute donnée confidentielle. Il pourrait récupérer cette information, ou alors la modifier pour la remplacer par une valeur connue et l'attaquant.
La faille ''row hammer'' est d'autant plus simple que la physique des cellules mémoire est médiocre. Les progrès de la miniaturisation rendent cette attaque de plus en plus facilement exploitable, les fuites étant d'autant plus importantes que les cellules mémoires sont petites. Mais exploiter cette attaque est compliqué, car il faut savoir à quelle adresse se situe a donnée à altérer, sans compter qu'il faut avoir des informations sur l'adresse des cellules voisines. Rappelons que la répartition physique des adresses/bytes dépend de comment la mémoire est organisée en interne, avec des banques, rangées et autres. Deux adresses consécutives ne sont pas forcément voisines sur la barrette de mémoire et l relation entre deux adresses de cellules mémoires voisines n'est pas connue avec certitude tant elle varie d'un système mémoire à l'autre.
Les solutions pour mitiger l'attaque ''row hammer'' sont assez limitées. Une première solution est d'utiliser les techniques de correction et de détection d'erreur comme l'ECC, mais là l'effet est limité. Une autre solution est de rafraichir la mémoire plus fréquemment, mais cela a un effet assez limité, sans compter que cela a un impact sur les performance et la consommation d'énergie de la RAM. Les concepteurs de matériel ont dû inventer des techniques spécialisées, comme le '' pseudo target row refresh'' d'Intel ou le ''target row refresh'' des mémoires LPDDR4. Ces techniques consistent, pour simplifier, à détecter quand une ligne mémoire est accédée très souvent, à rafraichir les lignes de mémoire voisines assez régulièrement. L'effet sur les performances est limité, mais cela demande d'intégrer cette technique dans le contrôleur mémoire externe/interne.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les cellules mémoires
| prevText=Les cellules mémoires
| next=Contrôleur mémoire interne
| nextText=Le contrôleur mémoire interne
}}
</noinclude>
4rutaa1t5q2tlr26q77q8c97qrqfj4t
Wikilivres:Maintenance/Page d'accueil
4
74644
682029
680621
2022-07-20T13:31:00Z
DavidL
1746
wikitext
text/x-wiki
__NOTOC__ __NOEDITSECTION__
La [[Accueil|page d'accueil]] présente trois livres et pages :
* [[Accueil/Vitrine]] : Un livre de [[Wikilivres:Vitrine|la vitrine]],
* [[Accueil/Wikijunior]] : un livre de [[Wikijunior]],
* [[Accueil/Recette]] : une recette du [[livre de cuisine]].
La présentation change chaque jour et reboucle automatiquement (basé sur un index calculé selon la date du jour) sur un nombre de pages devant être mis à jour à l'ajout d'une nouvelle présentation.
Voir [[Spécial:Index/Wikilivres:Vitrine/|la liste des présentations existantes.]]
== Modifier le nombre de livres ==
La modification du nombre de livres présenté est gérée par un bouton qui apparaît au dessus de la zone d'édition des 3 sous-pages listées ci-dessus.
Ce bouton affiche le nombre actuel et le facteur multiplicateur entre parenthèses :
<div style="margin:auto;display:block;" class="mw-ui-button">
Modifier le nombre de livres : 79 (facteur 8)
</div>
Le facteur indique le saut effectué chaque jour.
Par exemple, pour 79 livres ([[Wikilivres:Vitrine/0]] à [[Wikilivres:Vitrine/78]]) avec un facteur 8, quand un jour [[Wikilivres:Vitrine/70]] est présenté, le lendemain, [[Wikilivres:Vitrine/78]] (+8) est présenté, ensuite [[Wikilivres:Vitrine/7]] le surlendemain (78 + 8 = 86, mais modulo 79, donc 7).
Le facteur permet d'éviter de présenter les livres à la suite chaque jour pour éviter un même thème trop longtemps.
Le clic sur le bouton ouvre une fenêtre demandant le nouveau nombre de livres, éventuellement suivi du caractère étoile et du nouveau facteur.
Par défaut, le facteur est inchangé sauf si nécessaire c'est à dire quand le nouveau nombre a un multiple commun avec le facteur ; par exemple passer le nombre à 80 fera augmenter le facteur à 9.
[[Catégorie:Maintenance Wikilivres|*]]
tj8gu6286lrhenrrrsdwzcga34gv33o
MediaWiki:Common-OutilsEdition.js
8
78089
682026
680990
2022-07-20T12:28:00Z
DavidL
1746
javascript
text/javascript
/* Outils pour l'édition des pages */
/************************************************************/
/*
Ajoute un bouton pour modifier facilement le nombre de livres en vitrine.
* Accueil/Vitrine
* Accueil/Wikijunior
* Accueil/Recette
*/
var modifierNombre;
function boutonModifierIndex(){
// ----
var itemname='livres';
var b_modif;
function pgcd(a,b)
{
for(;;)
{
if (a==0) return b;
b %= a;
if (b==1) return 1;
if (b==0) return a;
a %= b;
if (a==1) return 1;
}
}
function ajusterParametres(nvparams, nparams, jour)
{
var ntotal = parseInt(nvparams[0]);
if (ntotal<1) ntotal = 1;
var nmul = parseInt(nvparams[1]);
if (nmul<1) nmul = 1;
var total = parseInt(nparams[0]);
var mul = parseInt(nparams[1]);
var ofs = parseInt(nparams[2]);
var per = parseInt(nparams[3]);
var num = 0;
var d=new Date();
if (jour)
{
var z = d.getTime()-d.getTimezoneOffset()*60000;
num = Math.floor((719562 + z/86400000) / per);
}
else num = d.getYear()*12+d.getMonth()+1;
var n = (num+ofs)*mul % total;
ofs %= ntotal;
mul = nmul % ntotal;
if (mul==0) mul = 1;
else while (pgcd(mul, ntotal)!=1) { if (++mul >= ntotal) mul=1; };
if (n < ntotal)
{
while ((num+ofs)*mul % ntotal != n) ofs = (ofs+1)%ntotal;
}
nparams[0] = ntotal;
nparams[1] = mul;
nparams[2] = ofs;
nparams[3] = per;
return per==1 ? ofs==0 ? mul==1 ?
[ntotal]
: [ntotal, mul]
: [ntotal, mul, ofs]
: [ntotal, mul, ofs, per];
}
var params = [], nparams = [], prefix, suffix;
modifierNombre = function()
{
var nouv = prompt('Entrez le nouveau nombre * facteur', nparams[0]+'*'+nparams[1]);
if (nouv==null) return false;
var nvp = nouv.split('*');
var nvparams=[nparams[0],nparams[1]];
var a = parseInt(nvp[0].trim());
var modif = false;
if (!isNaN(a)) { modif = nvparams[0] != a; nvparams[0] = a; }
if (nvp.length>1)
{
a = parseInt(nvp[1].trim());
if (!isNaN(a)) { if (!modif) modif = nvparams[1] != a; nvparams[1] = a; }
}
if (modif)
{
var res = ajusterParametres(nvparams, nparams, params[0]=='Jour');
var str = prefix + '|' + res.join('|') + suffix;
var v = field.value;
v = v.replace(/\{\{Index([a-zA-Z]+)(\|[0-9]+)+(}}|\|d)/gs, '{{Index'+str+'$3');
field.value = v;
b_modif.innerText = 'Modifier le nombre de '+itemname+' : '+nparams[0]+' (facteur '+nparams[1]+')';
}
return false;
}
var field = document.getElementById('wpTextbox1');
if (field)
{
var s = field.value;
var m = s.match(/^(.*?)\{\{Index((Jour|Mensuel)[^\}]*)}}(.*)$/s);
if (m)
{
params = m[2].split('|');
prefix = params[0]; suffix = '';
nparams = [];
for(var i=1 ; i<params.length ; i++)
{
var n = parseInt(params[i].trim());
if (isNaN(n)) suffix = '|'+params[i]+suffix;
else nparams.push(n);
}
if (nparams.length<2) nparams.push(1);
if (nparams.length<3) nparams.push(0);
if (nparams.length<4) nparams.push(1);
b_modif = genDOM(['button',{'class':'mw-ui-button','onClick':'modifierNombre();return false;'},'Modifier le nombre de '+itemname+' : '+nparams[0]+' (facteur '+nparams[1]+')']);
field.parentElement.insertBefore(b_modif, field)
}
}
// ----
}
$(boutonModifierIndex)
mxu6v6poyq51czx5799bk4vtv1v1qgx
682027
682026
2022-07-20T12:47:19Z
DavidL
1746
javascript
text/javascript
/* Outils pour l'édition des pages */
/************************************************************/
/*
Ajoute un bouton pour modifier facilement le nombre de livres en vitrine.
* Accueil/Vitrine
* Accueil/Wikijunior
* Accueil/Recette
*/
var modifierNombre;
function boutonModifierIndex(){
// ----
var itemname='livres';
var b_modif;
function pgcd(a,b)
{
for(;;)
{
if (a==0) return b;
b %= a;
if (b==1) return 1;
if (b==0) return a;
a %= b;
if (a==1) return 1;
}
}
function ajusterParametres(nvparams, nparams, jour)
{
var ntotal = parseInt(nvparams[0]);
if (ntotal<1) ntotal = 1;
var nmul = parseInt(nvparams[1]);
if (nmul<1) nmul = 1;
var total = parseInt(nparams[0]);
var mul = parseInt(nparams[1]);
var ofs = parseInt(nparams[2]);
var per = parseInt(nparams[3]);
var num = 0;
var d=new Date();
if (jour)
{
var z = d.getTime()-d.getTimezoneOffset()*60000;
num = Math.floor((719562 + z/86400000) / per);
}
else num = d.getYear()*12+d.getMonth()+1;
var n = (num+ofs)*mul % total;
ofs %= ntotal;
mul = nmul % ntotal;
if (mul==0) mul = 1;
else while (pgcd(mul, ntotal)!=1) { if (++mul >= ntotal) mul=1; };
if (n < ntotal)
{
while ((num+ofs)*mul % ntotal != n) ofs = (ofs+1)%ntotal;
}
nparams[0] = ntotal;
nparams[1] = mul;
nparams[2] = ofs;
nparams[3] = per;
return per==1 ? ofs==0 ? mul==1 ?
[ntotal]
: [ntotal, mul]
: [ntotal, mul, ofs]
: [ntotal, mul, ofs, per];
}
var params = [], nparams = [], prefix, suffix;
modifierNombre = function()
{
var nouv = prompt('Entrez le nouveau nombre * facteur', nparams[0]+'*'+nparams[1]);
if (nouv==null) return false;
var nvp = nouv.split('*');
var nvparams=[nparams[0],nparams[1]];
var a = parseInt(nvp[0].trim());
var modif = false;
if (!isNaN(a)) { modif = nvparams[0] != a; nvparams[0] = a; }
if (nvp.length>1)
{
a = parseInt(nvp[1].trim());
if (!isNaN(a)) { if (!modif) modif = nvparams[1] != a; nvparams[1] = a; }
}
if (modif)
{
var res = ajusterParametres(nvparams, nparams, params[0]=='Jour');
var str = prefix + '|' + res.join('|') + suffix;
var v = field.value;
v = v.replace(/\{\{Index([a-zA-Z]+)(\|[0-9]+)+(}}|\|d)/gs, '{{Index'+str+'$3');
field.value = v;
b_modif.innerText = 'Modifier le nombre de '+itemname+' : '+nparams[0]+' (facteur '+nparams[1]+')';
}
return false;
}
var page_ns = mw.config.get('wgNamespaceNumber');
var field = document.getElementById('wpTextbox1');
// Limité à l'espace principal et Wikilivres
if ([0,4].includes(page_ns) && field)
{
var s = field.value;
var m = s.match(/^(.*?)\{\{Index((Jour|Mensuel)[^\}]*)}}(.*)$/s);
if (m)
{
params = m[2].split('|');
prefix = params[0]; suffix = '';
nparams = [];
for(var i=1 ; i<params.length ; i++)
{
var n = parseInt(params[i].trim());
if (isNaN(n)) suffix = '|'+params[i]+suffix;
else nparams.push(n);
}
if (nparams.length<2) nparams.push(1);
if (nparams.length<3) nparams.push(0);
if (nparams.length<4) nparams.push(1);
b_modif = genDOM(['button',{'class':'mw-ui-button','onClick':'modifierNombre();return false;'},'Modifier le nombre de '+itemname+' : '+nparams[0]+' (facteur '+nparams[1]+')']);
field.parentElement.insertBefore(b_modif, field)
}
}
// ----
}
$(boutonModifierIndex)
s7rkuneecba8or356yakm0md7pqdk6i
Mathc initiation/a78
0
78505
682080
681993
2022-07-20T20:48:53Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a79| Sommaire]]
----{{Partie{{{type|}}}|La trigonométrie hyperbolique g|fond={{{fond|}}<nowiki>}</nowiki>}}
:
En mathématiques, on appelle fonctions hyperboliques les fonctions cosinus hyperbolique, sinus hyperbolique et tangente hyperbolique... [https://fr.wikipedia.org/wiki/Fonction_hyperbolique Wikipédia]
:
<br>
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/Fichiers h : c78a1|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c78a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/Fichiers h : c78a5|Liste des équations étudiées]]
** [[Mathc initiation/Fichiers h : c78a3|Liste des fonctions trigonométriques hyperbolique du c]]
* [[Mathc initiation/Fichiers h : c78de|Les formes : ]]
** '''cosh(x)**2-sinh(x)**2''' = 1
* [[Mathc initiation/Fichiers h : c78bs|Les formes : ]]
** '''sinh(x+y)''' = cosh(x) sinh(y) + sinh(x) cosh(y)
* [[Mathc initiation/Fichiers h : c78dd|Les formes : ]]
** '''sinh(2x)''' = 2 cosh(x) sinh(x)
* [[Mathc initiation/Fichiers h : c78fx|Les formes :]]
** '''sinh(x)**2''' = 1/2 cosh(2x) + 1/2
* [[Mathc initiation/Fichiers h : c78gx|Les formes :]]
** '''sinh(x)sinh(y)''' = 1/2 [cosh(x+y) - cosh(x-y)]
* [[Mathc initiation/Fichiers h : c78hx|Vérifions avec la règle d'Osborn1 :]]
** '''sinh(x)+sinh(y)''' = 2 sinh( (x+y)/2 ) cosh( (x-y)/2 )
* [[Mathc initiation/Fichiers h : c78dc|Les formes : ]]
** '''sinh(acosh(x))''' = sqrt(x**2 -1)
:
----
{{AutoCat}}
joud557no3bm5teei0tv5pydi1l6abc
682113
682080
2022-07-21T11:21:18Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a79| Sommaire]]
----{{Partie{{{type|}}}|La trigonométrie hyperbolique g|fond={{{fond|}}<nowiki>}</nowiki>}}
:
En mathématiques, on appelle fonctions hyperboliques les fonctions cosinus hyperbolique, sinus hyperbolique et tangente hyperbolique... [https://fr.wikipedia.org/wiki/Fonction_hyperbolique Wikipédia]
:
<br>
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/Fichiers h : c78a1|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c78a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/Fichiers h : c78a5|Liste des équations étudiées]]
** [[Mathc initiation/Fichiers h : c78a3|Liste des fonctions trigonométriques hyperbolique du c]]
* [[Mathc initiation/Fichiers h : c78de|Les formes : ]]
** '''cosh(x)**2-sinh(x)**2''' = 1
* [[Mathc initiation/Fichiers h : c78bs|Les formes : ]]
** '''sinh(x+y)''' = cosh(x) sinh(y) + sinh(x) cosh(y)
* [[Mathc initiation/Fichiers h : c78dd|Les formes : ]]
** '''sinh(2x)''' = 2 cosh(x) sinh(x)
* [[Mathc initiation/Fichiers h : c78fx|Les formes :]]
** '''sinh(x)**2''' = 1/2 cosh(2x) + 1/2
* [[Mathc initiation/Fichiers h : c78gx|Les formes :]]
** '''sinh(x)sinh(y)''' = 1/2 [cosh(x+y) - cosh(x-y)]
* [[Mathc initiation/Fichiers h : c78hx|Les formes : ]]
** '''sinh(x)+sinh(y)''' = 2 sinh( (x+y)/2 ) cosh( (x-y)/2 )
* [[Mathc initiation/Fichiers h : c78dc|Les formes : ]]
** '''sinh(acosh(x))''' = sqrt(x**2 -1)
:
----
{{AutoCat}}
72djstblpstqo5aw2q4jd3aigkx97su
Mathc initiation/Fichiers h : c78dd
0
78598
682031
682023
2022-07-20T13:44:56Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a78| Sommaire]]
:
:
----{{Partie{{{type|}}}|Les formes sinh(2x)|fond={{{fond|}}<nowiki>}</nowiki>}}
:
<br>
:
* [[Mathc initiation/Fichiers h : c78ea|fa.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ea2|c1a.c]]
** [[Mathc initiation/Fichiers c : c78ea| Vérifions avec la règle d'Osborn1]]
** '''cosh(2*x)''' = cosh(x)**2 - sinh(x)**2
** ................. = 1 + 2*sinh(x)**2
** ................. = 2*cosh(x)**2 - 1
* [[Mathc initiation/Fichiers h : c78eb|fb.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78eb2|c1b.c]]
** [[Mathc initiation/Fichiers c : c78eb|Vérifions avec la règle d'Osborn1]]
** '''cosh(3*x)''' = 4*cosh(x)**3 - 3*cosh(x)
* [[Mathc initiation/Fichiers h : c78ec|fc.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ec2|c1c.c]]
** [[Mathc initiation/Fichiers c : c78ec|Vérifions avec la règle d'Osborn1]]
** '''cosh(4x)''' = 8 cosh(x)**4 - 8 cosh(x)**2 + 1
* [[Mathc initiation/Fichiers h : c78ed|fd.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ed2|c1d.c]]
** [[Mathc initiation/Fichiers c : c78ed|Vérifions avec la règle d'Osborn1]]
** '''sinh(2*x)''' = 2*cosh(x)*sinh(x)
* [[Mathc initiation/Fichiers c : c78ee|Vérifions avec la règle d'Osborn1]]
** '''sinh(3*x)''' = 3*sinh(x) + 4*sinh(x)**3
* [[Mathc initiation/Fichiers c : c78ef|Vérifions avec la règle d'Osborn1]]
** '''sinh(4x)''' = 4 sinh(x) cos(x) + 8 sinh(x)**3 cos(x)
* [[Mathc initiation/Fichiers c : c78eg|Vérifions avec la règle d'Osborn1]]
** '''tanh(2x)''' = (2*tanh(x))/(1+tanh(x)**2)
* [[Mathc initiation/Fichiers c : c78eh|Vérifions avec la règle d'Osborn1]]
** '''tanh(3x)''' = (3tanh(x)+tanh(x)**3) / (1+3tan(x)**2)
:
----
{{AutoCat}}
6rl5qkopgotrzeop33ynzwkw9aupdbq
682036
682031
2022-07-20T14:00:47Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a78| Sommaire]]
:
:
----{{Partie{{{type|}}}|Les formes sinh(2x)|fond={{{fond|}}<nowiki>}</nowiki>}}
:
<br>
:
* [[Mathc initiation/Fichiers h : c78ea|fa.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ea2|c1a.c]]
** [[Mathc initiation/Fichiers c : c78ea| Vérifions avec la règle d'Osborn1]]
** '''cosh(2*x)''' = cosh(x)**2 - sinh(x)**2
** ................. = 1 + 2*sinh(x)**2
** ................. = 2*cosh(x)**2 - 1
* [[Mathc initiation/Fichiers h : c78eb|fb.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78eb2|c1b.c]]
** [[Mathc initiation/Fichiers c : c78eb|Vérifions avec la règle d'Osborn1]]
** '''cosh(3*x)''' = 4*cosh(x)**3 - 3*cosh(x)
* [[Mathc initiation/Fichiers h : c78ec|fc.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ec2|c1c.c]]
** [[Mathc initiation/Fichiers c : c78ec|Vérifions avec la règle d'Osborn1]]
** '''cosh(4x)''' = 8 cosh(x)**4 - 8 cosh(x)**2 + 1
* [[Mathc initiation/Fichiers h : c78ed|fd.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ed2|c1d.c]]
** [[Mathc initiation/Fichiers c : c78ed|Vérifions avec la règle d'Osborn1]]
** '''sinh(2*x)''' = 2*cosh(x)*sinh(x)
* [[Mathc initiation/Fichiers h : c78ee|fe.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ee2|c1e.c]]
** [[Mathc initiation/Fichiers c : c78ee|Vérifions avec la règle d'Osborn1]]
** '''sinh(3*x)''' = 3*sinh(x) + 4*sinh(x)**3
* [[Mathc initiation/Fichiers c : c78ef|Vérifions avec la règle d'Osborn1]]
** '''sinh(4x)''' = 4 sinh(x) cos(x) + 8 sinh(x)**3 cos(x)
* [[Mathc initiation/Fichiers c : c78eg|Vérifions avec la règle d'Osborn1]]
** '''tanh(2x)''' = (2*tanh(x))/(1+tanh(x)**2)
* [[Mathc initiation/Fichiers c : c78eh|Vérifions avec la règle d'Osborn1]]
** '''tanh(3x)''' = (3tanh(x)+tanh(x)**3) / (1+3tan(x)**2)
:
----
{{AutoCat}}
o3j6dpzzf43dvozhntsi6cv0mur1cj7
682041
682036
2022-07-20T15:22:11Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a78| Sommaire]]
:
:
----{{Partie{{{type|}}}|Les formes sinh(2x)|fond={{{fond|}}<nowiki>}</nowiki>}}
:
<br>
:
* [[Mathc initiation/Fichiers h : c78ea|fa.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ea2|c1a.c]]
** [[Mathc initiation/Fichiers c : c78ea| Vérifions avec la règle d'Osborn1]]
** '''cosh(2*x)''' = cosh(x)**2 - sinh(x)**2
** ................. = 1 + 2*sinh(x)**2
** ................. = 2*cosh(x)**2 - 1
* [[Mathc initiation/Fichiers h : c78eb|fb.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78eb2|c1b.c]]
** [[Mathc initiation/Fichiers c : c78eb|Vérifions avec la règle d'Osborn1]]
** '''cosh(3*x)''' = 4*cosh(x)**3 - 3*cosh(x)
* [[Mathc initiation/Fichiers h : c78ec|fc.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ec2|c1c.c]]
** [[Mathc initiation/Fichiers c : c78ec|Vérifions avec la règle d'Osborn1]]
** '''cosh(4x)''' = 8 cosh(x)**4 - 8 cosh(x)**2 + 1
* [[Mathc initiation/Fichiers h : c78ed|fd.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ed2|c1d.c]]
** [[Mathc initiation/Fichiers c : c78ed|Vérifions avec la règle d'Osborn1]]
** '''sinh(2*x)''' = 2*cosh(x)*sinh(x)
* [[Mathc initiation/Fichiers h : c78ee|fe.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ee2|c1e.c]]
** [[Mathc initiation/Fichiers c : c78ee|Vérifions avec la règle d'Osborn1]]
** '''sinh(3*x)''' = 3*sinh(x) + 4*sinh(x)**3
* [[Mathc initiation/Fichiers h : c78ef|ff.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ef2|c1f.c]]
** [[Mathc initiation/Fichiers c : c78ef|Vérifions avec la règle d'Osborn1]]
** '''sinh(4x)''' = 4 sinh(x) cos(x) + 8 sinh(x)**3 cos(x)
* [[Mathc initiation/Fichiers c : c78eg|Vérifions avec la règle d'Osborn1]]
** '''tanh(2x)''' = (2*tanh(x))/(1+tanh(x)**2)
* [[Mathc initiation/Fichiers c : c78eh|Vérifions avec la règle d'Osborn1]]
** '''tanh(3x)''' = (3tanh(x)+tanh(x)**3) / (1+3tan(x)**2)
:
----
{{AutoCat}}
78pa5xbtyj76kmzyhivg267uot01nji
682044
682041
2022-07-20T15:32:03Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a78| Sommaire]]
:
:
----{{Partie{{{type|}}}|Les formes sinh(2x)|fond={{{fond|}}<nowiki>}</nowiki>}}
:
<br>
:
* [[Mathc initiation/Fichiers h : c78ea|fa.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ea2|c1a.c]]
** [[Mathc initiation/Fichiers c : c78ea| Vérifions avec la règle d'Osborn1]]
** '''cosh(2*x)''' = cosh(x)**2 - sinh(x)**2
** ................. = 1 + 2*sinh(x)**2
** ................. = 2*cosh(x)**2 - 1
* [[Mathc initiation/Fichiers h : c78eb|fb.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78eb2|c1b.c]]
** [[Mathc initiation/Fichiers c : c78eb|Vérifions avec la règle d'Osborn1]]
** '''cosh(3*x)''' = 4*cosh(x)**3 - 3*cosh(x)
* [[Mathc initiation/Fichiers h : c78ec|fc.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ec2|c1c.c]]
** [[Mathc initiation/Fichiers c : c78ec|Vérifions avec la règle d'Osborn1]]
** '''cosh(4x)''' = 8 cosh(x)**4 - 8 cosh(x)**2 + 1
* [[Mathc initiation/Fichiers h : c78ed|fd.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ed2|c1d.c]]
** [[Mathc initiation/Fichiers c : c78ed|Vérifions avec la règle d'Osborn1]]
** '''sinh(2*x)''' = 2*cosh(x)*sinh(x)
* [[Mathc initiation/Fichiers h : c78ee|fe.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ee2|c1e.c]]
** [[Mathc initiation/Fichiers c : c78ee|Vérifions avec la règle d'Osborn1]]
** '''sinh(3*x)''' = 3*sinh(x) + 4*sinh(x)**3
* [[Mathc initiation/Fichiers h : c78ef|ff.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ef2|c1f.c]]
** [[Mathc initiation/Fichiers c : c78ef|Vérifions avec la règle d'Osborn1]]
** '''sinh(4x)''' = 4 sinh(x) cos(x) + 8 sinh(x)**3 cos(x)
* [[Mathc initiation/Fichiers h : c78eg|fg.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78eg2|c1g.c]]
** [[Mathc initiation/Fichiers c : c78eg|Vérifions avec la règle d'Osborn1]]
** '''tanh(2x)''' = (2*tanh(x))/(1+tanh(x)**2)
* [[Mathc initiation/Fichiers c : c78eh|Vérifions avec la règle d'Osborn1]]
** '''tanh(3x)''' = (3tanh(x)+tanh(x)**3) / (1+3tan(x)**2)
:
----
{{AutoCat}}
c32pd2yozb8p88vxgoduri16cmpygg8
682076
682044
2022-07-20T20:43:53Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a78| Sommaire]]
:
:
----{{Partie{{{type|}}}|Les formes sinh(2x)|fond={{{fond|}}<nowiki>}</nowiki>}}
:
<br>
:
* [[Mathc initiation/Fichiers h : c78ea|fa.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ea2|c1a.c]]
** [[Mathc initiation/Fichiers c : c78ea| Vérifions avec la règle d'Osborn1]]
** '''cosh(2*x)''' = cosh(x)**2 - sinh(x)**2
** ................. = 1 + 2*sinh(x)**2
** ................. = 2*cosh(x)**2 - 1
* [[Mathc initiation/Fichiers h : c78eb|fb.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78eb2|c1b.c]]
** [[Mathc initiation/Fichiers c : c78eb|Vérifions avec la règle d'Osborn1]]
** '''cosh(3*x)''' = 4*cosh(x)**3 - 3*cosh(x)
* [[Mathc initiation/Fichiers h : c78ec|fc.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ec2|c1c.c]]
** [[Mathc initiation/Fichiers c : c78ec|Vérifions avec la règle d'Osborn1]]
** '''cosh(4x)''' = 8 cosh(x)**4 - 8 cosh(x)**2 + 1
* [[Mathc initiation/Fichiers h : c78ed|fd.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ed2|c1d.c]]
** [[Mathc initiation/Fichiers c : c78ed|Vérifions avec la règle d'Osborn1]]
** '''sinh(2*x)''' = 2*cosh(x)*sinh(x)
* [[Mathc initiation/Fichiers h : c78ee|fe.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ee2|c1e.c]]
** [[Mathc initiation/Fichiers c : c78ee|Vérifions avec la règle d'Osborn1]]
** '''sinh(3*x)''' = 3*sinh(x) + 4*sinh(x)**3
* [[Mathc initiation/Fichiers h : c78ef|ff.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ef2|c1f.c]]
** [[Mathc initiation/Fichiers c : c78ef|Vérifions avec la règle d'Osborn1]]
** '''sinh(4x)''' = 4 sinh(x) cos(x) + 8 sinh(x)**3 cos(x)
* [[Mathc initiation/Fichiers h : c78eg|fg.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78eg2|c1g.c]]
** [[Mathc initiation/Fichiers c : c78eg|Vérifions avec la règle d'Osborn1]]
** '''tanh(2x)''' = (2*tanh(x))/(1+tanh(x)**2)
* [[Mathc initiation/Fichiers h : c78eh|fh.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78eh2|c1h.c]]
** [[Mathc initiation/Fichiers c : c78eh|Vérifions avec la règle d'Osborn1]]
** '''tanh(3x)''' = (3tanh(x)+tanh(x)**3) / (1+3tan(x)**2)
:
----
{{AutoCat}}
l9ctbld3jenlan7qrop3jopmxurri8s
Mathc initiation/Fichiers h : c78hx
0
78622
682101
681732
2022-07-21T10:56:33Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a78| Sommaire]]
:
:
----{{Partie{{{type|}}}|Les formes sinh(x)sinh(y)|fond={{{fond|}}<nowiki>}</nowiki>}}
:
<br>
:
* [[Mathc initiation/Fichiers h : c78ha|fa.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ha2|c1a.c]]
** [[Mathc initiation/Fichiers c : c78ha|Vérifions avec la règle d'Osborn1]]
** '''sinh(x) + sinh(y)''' = 2 sinh( (x+y)/2 ) cosh( (x-y)/2
* [[Mathc initiation/Fichiers c : c78hb|Vérifions avec la règle d'Osborn1]]
** '''sinh(x) - sinh(y)''' = 2 sinh( (x-y)/2 ) cosh( (x+y)/2 )
* [[Mathc initiation/Fichiers c : c78hc|Vérifions avec la règle d'Osborn1]]
** ''' cosh(x) + cosh(y)''' = 2 cosh( (x+y)/2 ) cosh( (x-y)/2 )
* [[Mathc initiation/Fichiers c : c78hd|Vérifions avec la règle d'Osborn1]]
** '''cosh(x) - cosh(y)''' = 2 sinh( (x+y)/2 ) sinh( (x-y)/2 )
----
{{AutoCat}}
infuiurhi47zk6fix7ib44fpsi59r97
682104
682101
2022-07-21T11:05:11Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a78| Sommaire]]
:
:
----{{Partie{{{type|}}}|Les formes sinh(x)sinh(y)|fond={{{fond|}}<nowiki>}</nowiki>}}
:
<br>
:
* [[Mathc initiation/Fichiers h : c78ha|fa.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ha2|c1a.c]]
** [[Mathc initiation/Fichiers c : c78ha|Vérifions avec la règle d'Osborn1]]
** '''sinh(x) + sinh(y)''' = 2 sinh( (x+y)/2 ) cosh( (x-y)/2
* [[Mathc initiation/Fichiers c : c78hb|Vérifions avec la règle d'Osborn1]]
** '''sinh(x) - sinh(y)''' = 2 sinh( (x-y)/2 ) cosh( (x+y)/2 )
* [[Mathc initiation/Fichiers h : c78hc|fc.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78hc2|c1c.c]]
** [[Mathc initiation/Fichiers c : c78hc|Vérifions avec la règle d'Osborn1]]
** ''' cosh(x) + cosh(y)''' = 2 cosh( (x+y)/2 ) cosh( (x-y)/2 )
* [[Mathc initiation/Fichiers c : c78hd|Vérifions avec la règle d'Osborn1]]
** '''cosh(x) - cosh(y)''' = 2 sinh( (x+y)/2 ) sinh( (x-y)/2 )
----
{{AutoCat}}
5z7o0xuoy01dnwdw1klw62vpi48qho2
682107
682104
2022-07-21T11:12:19Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a78| Sommaire]]
:
:
----{{Partie{{{type|}}}|Les formes sinh(x)sinh(y)|fond={{{fond|}}<nowiki>}</nowiki>}}
:
<br>
:
* [[Mathc initiation/Fichiers h : c78ha|fa.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ha2|c1a.c]]
** [[Mathc initiation/Fichiers c : c78ha|Vérifions avec la règle d'Osborn1]]
** '''sinh(x) + sinh(y)''' = 2 sinh( (x+y)/2 ) cosh( (x-y)/2
* [[Mathc initiation/Fichiers h : c78hb|fb.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78hb2|c1b.c]]
** [[Mathc initiation/Fichiers c : c78hb|Vérifions avec la règle d'Osborn1]]
** '''sinh(x) - sinh(y)''' = 2 sinh( (x-y)/2 ) cosh( (x+y)/2 )
* [[Mathc initiation/Fichiers h : c78hc|fc.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78hc2|c1c.c]]
** [[Mathc initiation/Fichiers c : c78hc|Vérifions avec la règle d'Osborn1]]
** ''' cosh(x) + cosh(y)''' = 2 cosh( (x+y)/2 ) cosh( (x-y)/2 )
* [[Mathc initiation/Fichiers c : c78hd|Vérifions avec la règle d'Osborn1]]
** '''cosh(x) - cosh(y)''' = 2 sinh( (x+y)/2 ) sinh( (x-y)/2 )
----
{{AutoCat}}
oduj5qh17p63qvgi319cmpxplkxale9
682110
682107
2022-07-21T11:18:51Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a78| Sommaire]]
:
:
----{{Partie{{{type|}}}|Les formes sinh(x)sinh(y)|fond={{{fond|}}<nowiki>}</nowiki>}}
:
<br>
:
* [[Mathc initiation/Fichiers h : c78ha|fa.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78ha2|c1a.c]]
** [[Mathc initiation/Fichiers c : c78ha|Vérifions avec la règle d'Osborn1]]
** '''sinh(x) + sinh(y)''' = 2 sinh( (x+y)/2 ) cosh( (x-y)/2
* [[Mathc initiation/Fichiers h : c78hb|fb.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78hb2|c1b.c]]
** [[Mathc initiation/Fichiers c : c78hb|Vérifions avec la règle d'Osborn1]]
** '''sinh(x) - sinh(y)''' = 2 sinh( (x-y)/2 ) cosh( (x+y)/2 )
* [[Mathc initiation/Fichiers h : c78hc|fc.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78hc2|c1c.c]]
** [[Mathc initiation/Fichiers c : c78hc|Vérifions avec la règle d'Osborn1]]
** ''' cosh(x) + cosh(y)''' = 2 cosh( (x+y)/2 ) cosh( (x-y)/2 )
* [[Mathc initiation/Fichiers h : c78hd|fd.h ]] < ------------------ > [[Mathc initiation/Fichiers c : c78hd2|c1d.c]]
** [[Mathc initiation/Fichiers c : c78hd|Vérifions avec la règle d'Osborn1]]
** '''cosh(x) - cosh(y)''' = 2 sinh( (x+y)/2 ) sinh( (x-y)/2 )
----
{{AutoCat}}
7qnurk71b1kpncu8tki0m77skojstdj
Wikilivres:GUS2Wiki
4
78643
682068
682008
2022-07-20T20:39:19Z
Alexis Jazz
81580
Updating gadget usage statistics from [[Special:GadgetUsage]] ([[phab:T121049]])
wikitext
text/x-wiki
{{#ifexist:Project:GUS2Wiki/top|{{/top}}|This page provides a historical record of [[Special:GadgetUsage]] through its page history. To get the data in CSV format, see wikitext. To customize this message or add categories, create [[/top]].}}
Les données suivantes sont en cache et ont été mises à jour pour la dernière fois le 2022-07-20T02:18:30Z. {{PLURAL:5000|1=Un seul|5000}} résultat{{PLURAL:5000||s}} au maximum {{PLURAL:5000|est|sont}} disponible{{PLURAL:5000||s}} dans le cache.
{| class="sortable wikitable"
! Gadget !! data-sort-type="number" | Nombre d'utilisateurs !! data-sort-type="number" | Utilisateurs actifs
|-
|Barre de luxe || 35 || 1
|-
|CoinsArrondis || 99 || 1
|-
|DeluxeAdmin || 5 || 1
|-
|DeluxeEdit || 34 || 1
|-
|DeluxeHistory || 45 || 1
|-
|DeluxeImport || 19 || 1
|-
|DeluxeRename || 15 || 1
|-
|DeluxeSummary || 32 || 1
|-
|DirectPageLink || 24 || 1
|-
|FastRevert || 35 || 1
|-
|FixArrayAltLines || 22 || 1
|-
|FlecheHaut || 66 || 1
|-
|HotCats || 79 || 2
|-
|ListeABordure || 29 || 1
|-
|LocalLiveClock || 25 || 1
|-
|MobileView || 16 || 1
|-
|RenommageCategorie || 6 || 2
|-
|RestaurationDeluxe || 12 || 1
|-
|RevertDiff || 33 || 3
|-
|ScriptAutoVersion || 16 || 1
|-
|SkinPreview || 2 || 1
|-
|Smart patrol || 2 || 1
|-
|SousPages || 53 || 1
|-
|Tableau || 67 || 1
|-
|UnicodeEditRendering || 28 || 1
|-
|massblock || 3 || 1
|}
* [[Spécial:GadgetUsage]]
* [[w:en:Wikipedia:GUS2Wiki/Script|GUS2Wiki]]
<!-- data in CSV format:
Barre de luxe,35,1
CoinsArrondis,99,1
DeluxeAdmin,5,1
DeluxeEdit,34,1
DeluxeHistory,45,1
DeluxeImport,19,1
DeluxeRename,15,1
DeluxeSummary,32,1
DirectPageLink,24,1
FastRevert,35,1
FixArrayAltLines,22,1
FlecheHaut,66,1
HotCats,79,2
ListeABordure,29,1
LocalLiveClock,25,1
MobileView,16,1
RenommageCategorie,6,2
RestaurationDeluxe,12,1
RevertDiff,33,3
ScriptAutoVersion,16,1
SkinPreview,2,1
Smart patrol,2,1
SousPages,53,1
Tableau,67,1
UnicodeEditRendering,28,1
massblock,3,1
-->
lr4jjzby6930e3xx9hygm4jhgynx55m
Mathc initiation/Fichiers h : c78ed
0
78671
682032
2022-07-20T13:45:15Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer ce fichier dans votre répertoire de travail.
{{Fichier|fd.h|largeur=70%|info=utilitaire|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as fd.h */
/* --------------------------------- */
double f1(
double x)
{
return( sinh(2.*x) );
}
char f1eq[] = "sinh(2x)";
/* --------------------------------- */
double f2(
double x)
{
return(2.*cosh(x)*sinh(x));
}
char f2eq[] = "2 cosh(x) sinh(x)";
/* --------------------------------- */
/* --------------------------------- */
</syntaxhighlight>
{{AutoCat}}
9jl9hw55wx65t7hx02n7pvupk3au2q8
Mathc initiation/Fichiers c : c78ed2
0
78672
682033
2022-07-20T13:45:35Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01d.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1d.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "fd.h"
/* --------------------------------- */
int main(void)
{
double x = 1.8;
clrscrn();
printf(" x = %0.1f \n\n\n",x);
printf(" %s \t\t= %0.8f \n", f1eq, f1(x));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
x = 1.8
sinh(2x) = 18.28545536
2 cosh(x) sinh(x) = 18.28545536
Press return to continue.
.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
Nous avons vu que :
sinh(x+y) = cosh(x) sinh(y) + sinh(x) cosh(y)
posons x = y
sinh(x+x) = cosh(x) sinh(x) + sinh(x) cosh(x)
sinh(2x) = 2 cosh(x) sinh(x)
</syntaxhighlight>
{{AutoCat}}
p1moj0zi0lsku1udp4erf3s86607z4r
682034
682033
2022-07-20T13:46:06Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01d.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1d.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "fd.h"
/* --------------------------------- */
int main(void)
{
double x = 1.8;
clrscrn();
printf(" x = %0.1f \n\n\n",x);
printf(" %s \t\t= %0.8f \n", f1eq, f1(x));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
x = 1.8
sinh(2x) = 18.28545536
2 cosh(x) sinh(x) = 18.28545536
Press return to continue.
.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
Nous avons vu que :
sinh(x+y) = cosh(x) sinh(y) + sinh(x) cosh(y)
posons x = y
sinh(x+x) = cosh(x) sinh(x) + sinh(x) cosh(x)
sinh(2x) = 2 cosh(x) sinh(x)
</syntaxhighlight>
{{AutoCat}}
5is7a563xtzx6abjob5b87o363i3gta
Mathc initiation/Fichiers h : c78ee
0
78673
682037
2022-07-20T14:01:04Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer ce fichier dans votre répertoire de travail.
{{Fichier|fe.h|largeur=70%|info=utilitaire|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as fe.h */
/* --------------------------------- */
double f1(
double x)
{
return( sinh(3.*x) );
}
char f1eq[] = "sinh(3x)";
/* --------------------------------- */
double f2(
double x)
{
return(4.*sinh(x)*sinh(x)*sinh(x) + 3.*sinh(x));
}
char f2eq[] = "4*sinh(x)**3 + 3*sinh(x)";
/* --------------------------------- */
/* --------------------------------- */
</syntaxhighlight>
{{AutoCat}}
8qu8kndo3actyjhyyshlft9wxgqlcbb
Mathc initiation/Fichiers c : c78ee2
0
78674
682038
2022-07-20T14:02:08Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01e.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1e.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "fe.h"
/* --------------------------------- */
int main(void)
{
double x = 1.8;
clrscrn();
printf(" x = %0.1f \n\n\n",x);
printf(" %s \t\t\t= %0.8f \n", f1eq, f1(x));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
x = 1.8
sinh(2x) = 18.28545536
2 cosh(x) sinh(x) = 18.28545536
Press return to continue.
.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
Nous avons vu que :
sinh(x+y) = cosh(x) sinh(y) + sinh(x) cosh(y)
posons :
sinh(2x+y) = cosh(2x) sinh(y) + sinh(2x) cosh(y)
posons x = y
sinh(3x) = cosh(2x) sinh(x) + sinh(2x) cosh(x) cosh(2*x) = 1 + 2*sinh(x)**2
sinh(2*x) = 2*cosh(x)*sinh(x)
sinh(3x) = [1 + 2*sinh(x)**2] sinh(x) + [2*cosh(x)*sinh(x)] cosh(x)
sinh(3x) = [sinh(x) + 2*sinh(x)**3] + [2*cosh(x)**2 *sinh(x)]
cosh(x)**2-sinh(x)**2 = 1
cosh(x)**2 = 1+sinh(x)**2
sinh(3x) = [sinh(x) + 2*sinh(x)**3] + [2*(1+sinh(x)**2) *sinh(x)]
sinh(3x) = [sinh(x) + 2*sinh(x)**3] + [2*(sinh(x)+sinh(x)**3)]
sinh(3x) = sinh(x) + 2*sinh(x)**3 + 2*sinh(x) + 2*sinh(x)**3
sinh(3x) = 4*sinh(x)**3 + 3*sinh(x)
</syntaxhighlight>
{{AutoCat}}
222iblekkka7ifok40o5ss9srs2zced
682039
682038
2022-07-20T14:03:29Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01e.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1e.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "fe.h"
/* --------------------------------- */
int main(void)
{
double x = 1.8;
clrscrn();
printf(" x = %0.1f \n\n\n",x);
printf(" %s \t\t\t= %0.8f \n", f1eq, f1(x));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
x = 1.8
sinh(3x) = 110.70094981
4*sinh(x)**3 + 3*sinh(x) = 110.70094981
Press return to continue.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
Nous avons vu que :
sinh(x+y) = cosh(x) sinh(y) + sinh(x) cosh(y)
posons :
sinh(2x+y) = cosh(2x) sinh(y) + sinh(2x) cosh(y)
posons x = y
sinh(3x) = cosh(2x) sinh(x) + sinh(2x) cosh(x) cosh(2*x) = 1 + 2*sinh(x)**2
sinh(2*x) = 2*cosh(x)*sinh(x)
sinh(3x) = [1 + 2*sinh(x)**2] sinh(x) + [2*cosh(x)*sinh(x)] cosh(x)
sinh(3x) = [sinh(x) + 2*sinh(x)**3] + [2*cosh(x)**2 *sinh(x)]
cosh(x)**2-sinh(x)**2 = 1
cosh(x)**2 = 1+sinh(x)**2
sinh(3x) = [sinh(x) + 2*sinh(x)**3] + [2*(1+sinh(x)**2) *sinh(x)]
sinh(3x) = [sinh(x) + 2*sinh(x)**3] + [2*(sinh(x)+sinh(x)**3)]
sinh(3x) = sinh(x) + 2*sinh(x)**3 + 2*sinh(x) + 2*sinh(x)**3
sinh(3x) = 4*sinh(x)**3 + 3*sinh(x)
</syntaxhighlight>
{{AutoCat}}
5u20da6557apol7n3b0brnatdlj2n81
Mathc initiation/Fichiers h : c78ef
0
78675
682042
2022-07-20T15:22:26Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer ce fichier dans votre répertoire de travail.
{{Fichier|ff.h|largeur=70%|info=utilitaire|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as ff.h */
/* --------------------------------- */
double f1(
double x)
{
return( sinh(4.*x) );
}
char f1eq[] = "sinh(4x)";
/* --------------------------------- */
double f2(
double x)
{
return(4.*sinh(x)*cosh(x) + 8.* sinh(x)*sinh(x)*sinh(x)*cosh(x));
}
char f2eq[] = "4 sinh(x) cosh(x) + 8 sinh(x)**3 cosh(x)";
/* --------------------------------- */
/* --------------------------------- */
</syntaxhighlight>
{{AutoCat}}
9j8gwor5coqvxo5f9xbyd2uyif4hm6u
Mathc initiation/Fichiers c : c78ef2
0
78676
682043
2022-07-20T15:23:06Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01e.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1f.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "ff.h"
/* --------------------------------- */
int main(void)
{
double x = 1.8;
clrscrn();
printf(" x = %0.1f \n\n\n",x);
printf(" %s \t\t\t\t\t= %0.8f \n", f1eq, f1(x));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
x = 1.8
sinh(4x) = 669.71500890
4 sinh(x) cosh(x) + 8 sinh(x)**3 cosh(x) = 669.71500890
Press return to continue.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
Nous avons vu que :
sinh(x+y) = cosh(x) sinh(y) + sinh(x) cosh(y)
posons :
sinh(3x+y) = cosh(3x) sinh(y) + sinh(3x) cosh(y)
posons x = y
sinh(4x) = cosh(3x) sinh(x) + sinh(3x) cosh(x) cosh(3*x) = 4*cosh(x)**3 - 3*cosh(x)
sinh(3*x) = 3*sinh(x) + 4*sinh(x)**3
sinh(4x) = [4*cosh(x)**3 - 3*cosh(x)] sinh(x) + [3*sinh(x) + 4*sinh(x)**3] cosh(x)
sinh(4x) = [4*cosh(x)**3 sinh(x)-3*cosh(x)sinh(x)] + [3*sinh(x)cosh(x)+4*sinh(x)**3 cosh(x)]
sinh(4x) = [4*cosh(x)**3 sinh(x) ] + [ 4*sinh(x)**3 cosh(x)]
sinh(4x) = sinh(x) cosh(x) [4*cosh(x)**2 + 4*sinh(x)**2 ]
cosh(x)**2-sinh(x)**2 = 1
cosh(x)**2 = 1+sinh(x)**2
sinh(4x) = sinh(x) cosh(x) [4*(1+sinh(x)**2) + 4*sinh(x)**2 ]
sinh(4x) = sinh(x) cosh(x) [4 +4 sinh(x)**2 + 4*sinh(x)**2 ]
sinh(4x) = sinh(x) cosh(x) [4 +8 sinh(x)**2]
sinh(4x) = 4 sinh(x) cosh(x) + 8 sinh(x)**3 cosh(x)
</syntaxhighlight>
{{AutoCat}}
bfok16jlhu1n799d14tehftwzlysskg
Mathc initiation/Fichiers h : c78eg
0
78677
682045
2022-07-20T15:32:20Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer ce fichier dans votre répertoire de travail.
{{Fichier|fg.h|largeur=70%|info=utilitaire|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as fg.h */
/* --------------------------------- */
double f1(
double x)
{
return( tanh(2.*x) );
}
char f1eq[] = "tanh(2x)";
/* --------------------------------- */
double f2(
double x)
{
return(2.*tanh(x)/(1 + tanh(x)*tanh(x)));
}
char f2eq[] = "2 tanh(x)/(1+tanh(x)**2)";
/* --------------------------------- */
/* --------------------------------- */
</syntaxhighlight>
{{AutoCat}}
s3qqfszecapi2vr4ly12h9r7e2ej8ji
Mathc initiation/Fichiers c : c78eg2
0
78678
682046
2022-07-20T15:32:55Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01g.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1g.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "fg.h"
/* --------------------------------- */
int main(void)
{
double x = 1.8;
clrscrn();
printf(" x = %0.1f \n\n\n",x);
printf(" %s \t\t\t= %0.8f \n", f1eq, f1(x));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
x = 1.8
tanh(2x) = 0.99850794
2 tanh(x)/(1+tanh(x)**2) = 0.99850794
Press return to continue.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
nous savons :
tanh(y) + tanh(x)
tanh(x+y) = ----------------
1 + tanh(x)tanh(y)
posons x = y
tanh(x) + tanh(x)
tanh(x+x) = ----------------
1 + tanh(x)tanh(x)
soit
2 tanh(x)
tanh(2x) = ----------------
1 + tanh(x)**2
</syntaxhighlight>
{{AutoCat}}
l10k1ctwbyfcyp3e11sgza62xdk9eoo
Discussion utilisateur:196.217.143.237
3
78679
682053
2022-07-20T16:01:17Z
DavidL
1746
Page créée avec « {{subst:Test 1}}--~~~~ »
wikitext
text/x-wiki
{|class="WSerieH" class="plainlinks" id="vandale" align="center" style="width:100%;margin-bottom:2em;border:1px solid #8888aa;border-right-width:2px;border-bottom-width:2px;background-color:#f7f8ff;padding:5px;text-align:justify"
|-
|[[Image:Nuvola apps important.svg|64px|Arrêtez de vandaliser Wikilivres !]]
|Bonjour {{BASEPAGENAME}},
Vous avez découvert combien il est facile de modifier Wikilivres. Votre modification a été '''annulée''' en raison de son caractère non constructif. Merci de ne pas réitérer ce genre de contribution. Visitez la [[Aide:Accueil|page d’aide]] afin d’en apprendre plus ou le [[Wikilivres:bac à sable|bac à sable]] afin de faire des tests.
|}
[[Catégorie:Vandales avertis]]-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 20 juillet 2022 à 18:01 (CEST)
a3ajpcj16ozkig55a8289kwmrv9l0g4
Fonctionnement d'un ordinateur/Le Translation Lookaside Buffer
0
78680
682069
2022-07-20T20:40:36Z
Mewtow
31375
Page créée avec « Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''t... »
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
===Les ''hardare page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, ce cache est parfois découpé en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rares que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes tient dans l'associativité de la TLB, un concept que nous verrons dans le chapitre suivant sur les caches, sans compter que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages. Ces deux problèmes interagissent entre eux et font que l'usage d'une TLB unique est possible, mais pas forcément optimal.
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
lvnohg16s4v2ge9jzv4teab6pn9qomt
682072
682069
2022-07-20T20:41:52Z
Mewtow
31375
/* La hiérarchie des TLB */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
===Les ''hardare page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
===La séparation des TLB de niveau 1 entre instructions et données===
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rares que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
===Les différentes TLB suivant la taille des pages===
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes tient dans l'associativité de la TLB, un concept que nous verrons dans le chapitre suivant sur les caches, sans compter que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages. Ces deux problèmes interagissent entre eux et font que l'usage d'une TLB unique est possible, mais pas forcément optimal.
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
mq5bwzuusn4mklo5tyiuxoftl2g5m79
682073
682072
2022-07-20T20:42:13Z
Mewtow
31375
/* Les différentes TLB suivant la taille des pages */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
===Les ''hardare page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
===La séparation des TLB de niveau 1 entre instructions et données===
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rares que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
===Les différentes TLB suivant la taille des pages===
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes tient dans l'associativité de la TLB, un concept que nous verrons dans le chapitre suivant sur les caches, sans compter que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages. Ces deux problèmes interagissent entre eux et font que l'usage d'une TLB unique est possible, mais pas forcément optimal.
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
3pid5g6w5jk9mvuergp2fyb80t6e0qw
682075
682073
2022-07-20T20:43:43Z
Mewtow
31375
/* Les différentes TLB suivant la taille des pages */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
===Les ''hardare page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
===La séparation des TLB de niveau 1 entre instructions et données===
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rares que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
===Les différentes TLB suivant la taille des pages===
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
e2ms0nn6oeu27zvtqi35xdf4059i5ii
682077
682075
2022-07-20T20:44:24Z
Mewtow
31375
/* La séparation des TLB de niveau 1 entre instructions et données */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
===Les ''hardare page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
===La séparation des TLB de niveau 1 entre instructions et données===
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable.
Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
===Les différentes TLB suivant la taille des pages===
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
j70n6ckpp2ptqrnmh5oupftuipjg2hj
682081
682077
2022-07-20T20:51:12Z
Mewtow
31375
/* Les hardare page table walkers */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
===La séparation des TLB de niveau 1 entre instructions et données===
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable.
Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
===Les différentes TLB suivant la taille des pages===
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
8iajnxckyvz0ewgm9t22ks1tqbxupq4
682082
682081
2022-07-20T21:01:25Z
Mewtow
31375
/* La hiérarchie des TLB */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
===La séparation des TLB de niveau 1 entre instructions et données===
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable.
Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
===Les pages larges et leur impact sur la TLB===
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
rzip5bfie0z9psf95iccqjsh98852lb
682083
682082
2022-07-20T21:04:07Z
Mewtow
31375
/* La hiérarchie des TLB */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
8dfworzufc50si7zpff9v7i0pv6ure7
682084
682083
2022-07-20T21:12:50Z
Mewtow
31375
/* Les pages larges et leur impact sur la TLB */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel de microarchitecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, par compensé.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
7d01vdbl28ms45kiqrcjla1nh3sasye
682085
682084
2022-07-20T21:14:18Z
Mewtow
31375
/* Les pages larges et leur impact sur la TLB */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel de microarchitecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, par compensé.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
lrc0fsxfjo510w8km56sx6mxel8lsyu
682086
682085
2022-07-20T21:17:58Z
Mewtow
31375
/* L'usage d'une TLB unique pour toutes les tailles de page */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel de microarchitecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, par compensé. L'allongement du temps d'accès peut être compensé par diverses méthodes.
La première méthode est celle des ''accès simultanés à la TLB''. Les TLB sont des caches assez performants, qui sont conçus pour être capables d'effectuer plusieurs accès en parallèle. On peut alors profiter de cette particularité pour lancer plusieurs accès au TLB en même temps, un par taille de page. Avec cette technique, les accès se faisant en parallèle et non l'un après l'autre, le temps d'accès est presque le même que si la TLB ne gérait qu'une seule taille de page. Le désavantage est que les multiples accès consomment de l'énergie et du courant, ce qui augmente la consommation énergétique de la TLB, qui est déjà très importante.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
eas4wy6u28tkiw9xy7yrxvvqgzwm4km
682087
682086
2022-07-20T21:18:10Z
Mewtow
31375
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les caches consomment beaucoup d'énergie et la TLB ne fait pas exception. Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel de microarchitecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, par compensé. L'allongement du temps d'accès peut être compensé par diverses méthodes.
La première méthode est celle des ''accès simultanés à la TLB''. Les TLB sont des caches assez performants, qui sont conçus pour être capables d'effectuer plusieurs accès en parallèle. On peut alors profiter de cette particularité pour lancer plusieurs accès au TLB en même temps, un par taille de page. Avec cette technique, les accès se faisant en parallèle et non l'un après l'autre, le temps d'accès est presque le même que si la TLB ne gérait qu'une seule taille de page. Le désavantage est que les multiples accès consomment de l'énergie et du courant, ce qui augmente la consommation énergétique de la TLB, qui est déjà très importante.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
otyzyg84s2vw7jwbfd0k8h6mn7n23if
682088
682087
2022-07-20T21:29:46Z
Mewtow
31375
/* L'usage d'une TLB unique pour toutes les tailles de page */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les caches consomment beaucoup d'énergie et la TLB ne fait pas exception. Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel de architecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, par compensé. L'allongement du temps d'accès peut être compensé par diverses méthodes.
La première méthode est celle des '''accès simultanés à la TLB'''. Les TLB sont des caches assez performants, qui sont conçus pour être capables d'effectuer plusieurs accès en parallèle. On peut alors profiter de cette particularité pour lancer plusieurs accès au TLB en même temps, un par taille de page. Avec cette technique, les accès se faisant en parallèle et non l'un après l'autre, le temps d'accès est presque le même que si la TLB ne gérait qu'une seule taille de page. Le désavantage est que les multiples accès consomment de l'énergie et du courant, ce qui augmente la consommation énergétique de la TLB, qui est déjà très importante.
La seconde solution est la '''prédiction de taille de page'''. L'idée est que le processeur tente de prédire quelle sera la taille de page, afin de tomber directement sur la bonne taille de page. Au lieu de tenter d'abord pour une page de 4 kibioctets, puis 2 mébioctets et enfin 1-2 Gibioctet, le processeur testera la taille de page la plus probable, puis la seconde plus probable, avant de tester la dernière taille de page possible. Le problème est alors de concevoir un circuit capable de réaliser cette prédiction.
* Une manière simple mais extrêmement inefficace de faire cela est de mémoriser la taille des pages récemment accédées dans un cache spécialisé, qui mémorise la correspondance entre le numéro de la page et sa taille. La taille de la page est mémorisée dans ce cache, après le premier accès à cette page. Mais le défaut que le temps d'accès à ce cache de prédiction d'ajoute au temps d'accès à la TLBL2, ce qui en ruine totalement l'intérêt. Aussi, il faut trouver une solution alternative.
* Une autre solution n'utilise pas le numéro de page, mais l'instruction responsable de l'accès à la TLB. Un accès à la TLB signifie un accès en mémoire RAM, qui est réalisé par une instruction. Instruction qui est identifiée par une adresse, elle-même située dans le ''program counter''. L'idée est que si une instruction est exécutée plusieurs fois, elle a tendance à accéder aux données d'une même page (localité spatiale). Ce n'est pas systématique, mais c'est une bonne supposition. On peut donc associer cette instruction à la taille de la page accédée récemment. Le cache de prédiction mémorise donc l'association entre adresse de cette instruction et taille de la page. Lors de l'accès mémoire, le le ''program counter'' est récupéré et envoyé au cache de prédiction. En cas de succès d'accès, on obtient la taille de la page prédite.
l'adresse virtuelle est connue assez tard
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
np1dyz6y3megv0mnbhb17y8wudoiw98
682089
682088
2022-07-20T21:33:40Z
Mewtow
31375
/* L'usage d'une TLB unique pour toutes les tailles de page */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les caches consomment beaucoup d'énergie et la TLB ne fait pas exception. Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel de architecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, par compensé. L'allongement du temps d'accès peut être compensé par diverses méthodes.
La première méthode est celle des '''accès simultanés à la TLB'''. Les TLB sont des caches assez performants, qui sont conçus pour être capables d'effectuer plusieurs accès en parallèle. On peut alors profiter de cette particularité pour lancer plusieurs accès au TLB en même temps, un par taille de page. Avec cette technique, les accès se faisant en parallèle et non l'un après l'autre, le temps d'accès est presque le même que si la TLB ne gérait qu'une seule taille de page. Le désavantage est que les multiples accès consomment de l'énergie et du courant, ce qui augmente la consommation énergétique de la TLB, qui est déjà très importante.
La seconde solution est la '''prédiction de taille de page'''. L'idée est que le processeur tente de prédire quelle sera la taille de page, afin de tomber directement sur la bonne taille de page. Au lieu de tenter d'abord pour une page de 4 kibioctets, puis 2 mébioctets et enfin 1-2 Gibioctet, le processeur testera la taille de page la plus probable, puis la seconde plus probable, avant de tester la dernière taille de page possible. Le problème est alors de concevoir un circuit capable de réaliser cette prédiction.
* Une manière simple mais extrêmement inefficace de faire cela est de mémoriser la taille des pages récemment accédées dans un cache spécialisé, qui mémorise la correspondance entre le numéro de la page et sa taille. La taille de la page est mémorisée dans ce cache, après le premier accès à cette page. Mais le défaut que le temps d'accès à ce cache de prédiction d'ajoute au temps d'accès à la TLBL2, ce qui en ruine totalement l'intérêt. Aussi, il faut trouver une solution alternative.
* Une autre solution n'utilise pas le numéro de page, mais l'instruction responsable de l'accès à la TLB. Un accès à la TLB signifie un accès en mémoire RAM, qui est réalisé par une instruction. Instruction qui est identifiée par une adresse, elle-même située dans le ''program counter''. L'idée est que si une instruction est exécutée plusieurs fois, elle a tendance à accéder aux données d'une même page (localité spatiale). Ce n'est pas systématique, mais c'est une bonne supposition. On peut donc associer cette instruction à la taille de la page accédée récemment. Le cache de prédiction mémorise donc l'association entre adresse de cette instruction et taille de la page. Lors de l'accès mémoire, le le ''program counter'' est récupéré et envoyé au cache de prédiction. En cas de succès d'accès, on obtient la taille de la page prédite. Cette méthode a le défaut que si plusieurs instructions accèdent à la même page, elle prendront chacune une ligne de cache dans le cache de prédiction et rien ne sera mutualisé.
* Une solution alternative est de récupérer lune adresse virtuelle proche de l'adresse lue/écrite avant que l'accès mémoire soit démarré. L'idée est que lors du décodage de l'instruction d'accès mémoire, on récupére les registres utilisés, et on y accéde pour lire en avance l'adresse virtuelle.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
4rsg00hg4nealys0gkorkryxqkompvl
682090
682089
2022-07-20T21:37:45Z
Mewtow
31375
/* L'usage d'une TLB unique pour toutes les tailles de page */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les caches consomment beaucoup d'énergie et la TLB ne fait pas exception. Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel de architecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, par compensé. L'allongement du temps d'accès peut être compensé par diverses méthodes.
La première méthode est celle des '''accès simultanés à la TLB'''. Les TLB sont des caches assez performants, qui sont conçus pour être capables d'effectuer plusieurs accès en parallèle. On peut alors profiter de cette particularité pour lancer plusieurs accès au TLB en même temps, un par taille de page. Avec cette technique, les accès se faisant en parallèle et non l'un après l'autre, le temps d'accès est presque le même que si la TLB ne gérait qu'une seule taille de page. Le désavantage est que les multiples accès consomment de l'énergie et du courant, ce qui augmente la consommation énergétique de la TLB, qui est déjà très importante.
La seconde solution est la '''prédiction de taille de page'''. L'idée est que le processeur tente de prédire quelle sera la taille de page, afin de tomber directement sur la bonne taille de page. Au lieu de tenter d'abord pour une page de 4 kibioctets, puis 2 mébioctets et enfin 1-2 Gibioctet, le processeur testera la taille de page la plus probable, puis la seconde plus probable, avant de tester la dernière taille de page possible. Le problème est alors de concevoir un circuit capable de réaliser cette prédiction.
* Une manière simple mais extrêmement inefficace de faire cela est de mémoriser la taille des pages récemment accédées dans un cache spécialisé, qui mémorise la correspondance entre le numéro de la page et sa taille. La taille de la page est mémorisée dans ce cache, après le premier accès à cette page. Mais le défaut que le temps d'accès à ce cache de prédiction d'ajoute au temps d'accès à la TLBL2, ce qui en ruine totalement l'intérêt. Aussi, il faut trouver une solution alternative.
* Une autre solution n'utilise pas le numéro de page, mais l'instruction responsable de l'accès à la TLB. Un accès à la TLB signifie un accès en mémoire RAM, qui est réalisé par une instruction. Instruction qui est identifiée par une adresse, elle-même située dans le ''program counter''. L'idée est que si une instruction est exécutée plusieurs fois, elle a tendance à accéder aux données d'une même page (localité spatiale). Ce n'est pas systématique, mais c'est une bonne supposition. On peut donc associer cette instruction à la taille de la page accédée récemment. Le cache de prédiction mémorise donc l'association entre adresse de cette instruction et taille de la page. Lors de l'accès mémoire, le le ''program counter'' est récupéré et envoyé au cache de prédiction. En cas de succès d'accès, on obtient la taille de la page prédite. Cette méthode a le défaut que si plusieurs instructions accèdent à la même page, elle prendront chacune une ligne de cache dans le cache de prédiction et rien ne sera mutualisé.
* Une solution alternative est de récupérer lune adresse virtuelle proche de l'adresse lue/écrite avant que l'accès mémoire soit démarré. L'idée est que lors du décodage de l'instruction d'accès mémoire, on récupére les registres utilisés, et on y accéde pour lire en avance l'adresse virtuelle.
Une dernière méthode consiste à amortir le temps d'accès en cas de défaut dans la LLB L2. L'idée est de démarrer un accès à la table des pages en RAM en parallèle de l'accès à la TLB L2. La raison est que le temps d'accès à la TLB L2, avec ''hash-rehashing'', est très long. Il est sensiblement proche du quart du temps de défaut de TLB. Si en plus il fallait rajouter le temps d'accès à la table des pages en cas de défaut, le temps d'accès total serait énorme. Mais en lançant une lecture spéculative de la table des pages, le temps d'accès en cas de défaut est partiellement amortit. Le temps d'accès en cas de succès de TLB L2 reste cependant le même.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
azpu9a7oxc1m6cp8u77wexm5rlqgycr
682091
682090
2022-07-20T21:38:05Z
Mewtow
31375
/* L'usage d'une TLB unique pour toutes les tailles de page */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages situé en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les caches consomment beaucoup d'énergie et la TLB ne fait pas exception. Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des page en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façon : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-même le défaut d'accès à la TLB et vont chercher d'eux-même les informations nécessaire dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-même du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaire à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB pour les instructions plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
====Le ''hash-rehashing''====
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel de architecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, par compensé. L'allongement du temps d'accès peut être compensé par diverses méthodes.
La première méthode est celle des '''accès simultanés à la TLB'''. Les TLB sont des caches assez performants, qui sont conçus pour être capables d'effectuer plusieurs accès en parallèle. On peut alors profiter de cette particularité pour lancer plusieurs accès au TLB en même temps, un par taille de page. Avec cette technique, les accès se faisant en parallèle et non l'un après l'autre, le temps d'accès est presque le même que si la TLB ne gérait qu'une seule taille de page. Le désavantage est que les multiples accès consomment de l'énergie et du courant, ce qui augmente la consommation énergétique de la TLB, qui est déjà très importante.
La seconde solution est la '''prédiction de taille de page'''. L'idée est que le processeur tente de prédire quelle sera la taille de page, afin de tomber directement sur la bonne taille de page. Au lieu de tenter d'abord pour une page de 4 kibioctets, puis 2 mébioctets et enfin 1-2 Gibioctet, le processeur testera la taille de page la plus probable, puis la seconde plus probable, avant de tester la dernière taille de page possible. Le problème est alors de concevoir un circuit capable de réaliser cette prédiction.
* Une manière simple mais extrêmement inefficace de faire cela est de mémoriser la taille des pages récemment accédées dans un cache spécialisé, qui mémorise la correspondance entre le numéro de la page et sa taille. La taille de la page est mémorisée dans ce cache, après le premier accès à cette page. Mais le défaut que le temps d'accès à ce cache de prédiction d'ajoute au temps d'accès à la TLBL2, ce qui en ruine totalement l'intérêt. Aussi, il faut trouver une solution alternative.
* Une autre solution n'utilise pas le numéro de page, mais l'instruction responsable de l'accès à la TLB. Un accès à la TLB signifie un accès en mémoire RAM, qui est réalisé par une instruction. Instruction qui est identifiée par une adresse, elle-même située dans le ''program counter''. L'idée est que si une instruction est exécutée plusieurs fois, elle a tendance à accéder aux données d'une même page (localité spatiale). Ce n'est pas systématique, mais c'est une bonne supposition. On peut donc associer cette instruction à la taille de la page accédée récemment. Le cache de prédiction mémorise donc l'association entre adresse de cette instruction et taille de la page. Lors de l'accès mémoire, le le ''program counter'' est récupéré et envoyé au cache de prédiction. En cas de succès d'accès, on obtient la taille de la page prédite. Cette méthode a le défaut que si plusieurs instructions accèdent à la même page, elle prendront chacune une ligne de cache dans le cache de prédiction et rien ne sera mutualisé.
* Une solution alternative est de récupérer lune adresse virtuelle proche de l'adresse lue/écrite avant que l'accès mémoire soit démarré. L'idée est que lors du décodage de l'instruction d'accès mémoire, on récupére les registres utilisés, et on y accéde pour lire en avance l'adresse virtuelle.
Une dernière méthode consiste à amortir le temps d'accès en cas de défaut dans la LLB L2. L'idée est de démarrer un accès à la table des pages en RAM en parallèle de l'accès à la TLB L2. La raison est que le temps d'accès à la TLB L2, avec ''hash-rehashing'', est très long. Il est sensiblement proche du quart du temps de défaut de TLB. Si en plus il fallait rajouter le temps d'accès à la table des pages en cas de défaut, le temps d'accès total serait énorme. Mais en lançant une lecture spéculative de la table des pages, le temps d'accès en cas de défaut est partiellement amortit. Le temps d'accès en cas de succès de TLB L2 reste cependant le même.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
my4h1l7so237lywie3y6x2l17mqn3q2
682092
682091
2022-07-20T21:39:25Z
Mewtow
31375
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages située en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les caches consomment beaucoup d'énergie et la TLB ne fait pas exception. Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des pages en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-mêmes du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaires à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB d'instruction plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
====Le ''hash-rehashing''====
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel d’architecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, pas compensé. L'allongement du temps d'accès peut être compensé par diverses méthodes.
La première méthode est celle des '''accès simultanés à la TLB'''. Les TLB sont des caches assez performants, qui sont conçus pour être capables d'effectuer plusieurs accès en parallèle. On peut alors profiter de cette particularité pour lancer plusieurs accès au TLB en même temps, un par taille de page. Avec cette technique, les accès se faisant en parallèle et non l'un après l'autre, le temps d'accès est presque le même que si la TLB ne gérait qu'une seule taille de page. Le désavantage est que les multiples accès consomment de l'énergie et du courant, ce qui augmente la consommation énergétique de la TLB, qui est déjà très importante.
La seconde solution est la '''prédiction de taille de page'''. L'idée est que le processeur tente de prédire quelle sera la taille de page, afin de tomber directement sur la bonne taille de page. Au lieu de tenter d'abord pour une page de 4 kibioctets, puis 2 mébioctets et enfin 1-2 Gibioctets, le processeur testera la taille de page la plus probable, puis la seconde plus probable, avant de tester la dernière taille de page possible. Le problème est alors de concevoir un circuit capable de réaliser cette prédiction.
* Une manière simple mais extrêmement inefficace de faire cela est de mémoriser la taille des pages récemment accédées dans un cache spécialisé, qui mémorise la correspondance entre le numéro de la page et sa taille. La taille de la page est mémorisée dans ce cache, après le premier accès à cette page. Mais le défaut que le temps d'accès à ce cache de prédiction s'ajoute au temps d'accès à la TLBL2, ce qui en ruine totalement l'intérêt. Aussi, il faut trouver une solution alternative.
* Une autre solution n'utilise pas le numéro de page, mais l'instruction responsable de l'accès à la TLB. Un accès à la TLB signifie un accès en mémoire RAM, qui est réalisé par une instruction. Instruction qui est identifiée par une adresse, elle-même située dans le ''program counter''. L'idée est que si une instruction est exécutée plusieurs fois, elle a tendance à accéder aux données d'une même page (localité spatiale). Ce n'est pas systématique, mais c'est une bonne supposition. On peut donc associer cette instruction à la taille de la page accédée récemment. Le cache de prédiction mémorise donc l'association entre adresse de cette instruction et taille de la page. Lors de l'accès mémoire, le ''program counter'' est récupéré et envoyé au cache de prédiction. En cas de succès d'accès, on obtient la taille de la page prédite. Cette méthode a le défaut que si plusieurs instructions accèdent à la même page, elles prendront chacune une ligne de cache dans le cache de prédiction et rien ne sera mutualisé.
* Une solution alternative est de récupérer lune adresse virtuelle proche de l'adresse lue/écrite avant que l'accès mémoire soit démarré. L'idée est que lors du décodage de l'instruction d'accès mémoire, on récupére les registres utilisés, et on y accéde pour lire en avance l'adresse virtuelle.
Une dernière méthode consiste à amortir le temps d'accès en cas de défaut dans la LLB L2. L'idée est de démarrer un accès à la table des pages en RAM en parallèle de l'accès à la TLB L2. La raison est que le temps d'accès à la TLB L2, avec ''hash-rehashing'', est très long. Il est sensiblement proche du quart du temps de défaut de TLB. Si en plus il fallait rajouter le temps d'accès à la table des pages en cas de défaut, le temps d'accès total serait énorme. Mais en lançant une lecture spéculative de la table des pages, le temps d'accès en cas de défaut est partiellement amorti. Le temps d'accès en cas de succès de TLB L2 reste cependant le même.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
6n8q8dr4eevgbvokc9fqallecuk8bl5
682093
682092
2022-07-20T21:50:28Z
Mewtow
31375
/* L'usage d'une TLB unique pour toutes les tailles de page */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages située en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les caches consomment beaucoup d'énergie et la TLB ne fait pas exception. Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des pages en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-mêmes du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaires à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB d'instruction plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
====Le ''hash-rehashing''====
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel d’architecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, pas compensé. L'allongement du temps d'accès peut être compensé par diverses méthodes.
La première méthode est celle des '''accès simultanés à la TLB'''. Les TLB sont des caches assez performants, qui sont conçus pour être capables d'effectuer plusieurs accès en parallèle. On peut alors profiter de cette particularité pour lancer plusieurs accès au TLB en même temps, un par taille de page. Avec cette technique, les accès se faisant en parallèle et non l'un après l'autre, le temps d'accès est presque le même que si la TLB ne gérait qu'une seule taille de page. Le désavantage est que les multiples accès consomment de l'énergie et du courant, ce qui augmente la consommation énergétique de la TLB, qui est déjà très importante.
La seconde solution est la '''prédiction de taille de page'''. L'idée est que le processeur tente de prédire quelle sera la taille de page, afin de tomber directement sur la bonne taille de page. Au lieu de tenter d'abord pour une page de 4 kibioctets, puis 2 mébioctets et enfin 1-2 Gibioctets, le processeur testera la taille de page la plus probable, puis la seconde plus probable, avant de tester la dernière taille de page possible. Le problème est alors de concevoir un circuit capable de réaliser cette prédiction.
* Une manière simple mais extrêmement inefficace de faire cela est de mémoriser la taille des pages récemment accédées dans un cache spécialisé, qui mémorise la correspondance entre le numéro de la page et sa taille. La taille de la page est mémorisée dans ce cache, après le premier accès à cette page. Mais le défaut que le temps d'accès à ce cache de prédiction s'ajoute au temps d'accès à la TLBL2, ce qui en ruine totalement l'intérêt. Aussi, il faut trouver une solution alternative.
* Une autre solution n'utilise pas le numéro de page, mais l'instruction responsable de l'accès à la TLB. Un accès à la TLB signifie un accès en mémoire RAM, qui est réalisé par une instruction. Instruction qui est identifiée par une adresse, elle-même située dans le ''program counter''. L'idée est que si une instruction est exécutée plusieurs fois, elle a tendance à accéder aux données d'une même page (localité spatiale). Ce n'est pas systématique, mais c'est une bonne supposition. On peut donc associer cette instruction à la taille de la page accédée récemment. Le cache de prédiction mémorise donc l'association entre adresse de cette instruction et taille de la page. Lors de l'accès mémoire, le ''program counter'' est récupéré et envoyé au cache de prédiction. En cas de succès d'accès, on obtient la taille de la page prédite. Cette méthode a le défaut que si plusieurs instructions accèdent à la même page, elles prendront chacune une ligne de cache dans le cache de prédiction et rien ne sera mutualisé.
* Une solution alternative est de récupérer lune adresse virtuelle proche de l'adresse lue/écrite avant que l'accès mémoire soit démarré. L'idée est que lors du décodage de l'instruction d'accès mémoire, on récupére les registres utilisés, et on y accéde pour lire en avance l'adresse virtuelle.
Une dernière méthode consiste à amortir le temps d'accès en cas de défaut dans la LLB L2. L'idée est de démarrer un accès à la table des pages en RAM en parallèle de l'accès à la TLB L2. La raison est que le temps d'accès à la TLB L2, avec ''hash-rehashing'', est très long. Il est sensiblement proche du quart du temps de défaut de TLB. Si en plus il fallait rajouter le temps d'accès à la table des pages en cas de défaut, le temps d'accès total serait énorme. Mais en lançant une lecture spéculative de la table des pages, le temps d'accès en cas de défaut est partiellement amorti. Le temps d'accès en cas de succès de TLB L2 reste cependant le même.
====Le ''skewing''====
La technique du ''skewing'' est assez simple à comprendre pour qui se rappelle ce qu'est un cache associatif par voie, et encore plus un cache ''skew-associative''. L'idée est d'utiliser un cache associatif par voie, mais où chaque voie est dédiée à une taille de page bien définie. Un cache associatif à trois voie pourra ainsi avoir une voie pour les pages de 4 kibioctets, une voie pour les pages de 2 mébioctets et une dernière voie pour les pages de plusieurs gibioctets. Cette méthode est conceptuellement équivalente au fait d'utiliser un cache pour chaque taille, à quelques différences près. Déjà, cela permet de mutaliser des circuits qui auraient été dupliqués en utilisant des caches séparés. Deuxièmement, les trois caches doivent avoir la même taille.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
rtdzy66dsxxsf7hsg5gjkzva469z24m
682094
682093
2022-07-20T21:50:57Z
Mewtow
31375
/* Le skewing */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages située en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les caches consomment beaucoup d'énergie et la TLB ne fait pas exception. Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des pages en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-mêmes du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaires à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB d'instruction plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
====Le ''hash-rehashing''====
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel d’architecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, pas compensé. L'allongement du temps d'accès peut être compensé par diverses méthodes.
La première méthode est celle des '''accès simultanés à la TLB'''. Les TLB sont des caches assez performants, qui sont conçus pour être capables d'effectuer plusieurs accès en parallèle. On peut alors profiter de cette particularité pour lancer plusieurs accès au TLB en même temps, un par taille de page. Avec cette technique, les accès se faisant en parallèle et non l'un après l'autre, le temps d'accès est presque le même que si la TLB ne gérait qu'une seule taille de page. Le désavantage est que les multiples accès consomment de l'énergie et du courant, ce qui augmente la consommation énergétique de la TLB, qui est déjà très importante.
La seconde solution est la '''prédiction de taille de page'''. L'idée est que le processeur tente de prédire quelle sera la taille de page, afin de tomber directement sur la bonne taille de page. Au lieu de tenter d'abord pour une page de 4 kibioctets, puis 2 mébioctets et enfin 1-2 Gibioctets, le processeur testera la taille de page la plus probable, puis la seconde plus probable, avant de tester la dernière taille de page possible. Le problème est alors de concevoir un circuit capable de réaliser cette prédiction.
* Une manière simple mais extrêmement inefficace de faire cela est de mémoriser la taille des pages récemment accédées dans un cache spécialisé, qui mémorise la correspondance entre le numéro de la page et sa taille. La taille de la page est mémorisée dans ce cache, après le premier accès à cette page. Mais le défaut que le temps d'accès à ce cache de prédiction s'ajoute au temps d'accès à la TLBL2, ce qui en ruine totalement l'intérêt. Aussi, il faut trouver une solution alternative.
* Une autre solution n'utilise pas le numéro de page, mais l'instruction responsable de l'accès à la TLB. Un accès à la TLB signifie un accès en mémoire RAM, qui est réalisé par une instruction. Instruction qui est identifiée par une adresse, elle-même située dans le ''program counter''. L'idée est que si une instruction est exécutée plusieurs fois, elle a tendance à accéder aux données d'une même page (localité spatiale). Ce n'est pas systématique, mais c'est une bonne supposition. On peut donc associer cette instruction à la taille de la page accédée récemment. Le cache de prédiction mémorise donc l'association entre adresse de cette instruction et taille de la page. Lors de l'accès mémoire, le ''program counter'' est récupéré et envoyé au cache de prédiction. En cas de succès d'accès, on obtient la taille de la page prédite. Cette méthode a le défaut que si plusieurs instructions accèdent à la même page, elles prendront chacune une ligne de cache dans le cache de prédiction et rien ne sera mutualisé.
* Une solution alternative est de récupérer lune adresse virtuelle proche de l'adresse lue/écrite avant que l'accès mémoire soit démarré. L'idée est que lors du décodage de l'instruction d'accès mémoire, on récupére les registres utilisés, et on y accéde pour lire en avance l'adresse virtuelle.
Une dernière méthode consiste à amortir le temps d'accès en cas de défaut dans la LLB L2. L'idée est de démarrer un accès à la table des pages en RAM en parallèle de l'accès à la TLB L2. La raison est que le temps d'accès à la TLB L2, avec ''hash-rehashing'', est très long. Il est sensiblement proche du quart du temps de défaut de TLB. Si en plus il fallait rajouter le temps d'accès à la table des pages en cas de défaut, le temps d'accès total serait énorme. Mais en lançant une lecture spéculative de la table des pages, le temps d'accès en cas de défaut est partiellement amorti. Le temps d'accès en cas de succès de TLB L2 reste cependant le même.
====Le ''skewing''====
La technique du ''skewing'' est assez simple à comprendre pour qui se rappelle ce qu'est un cache associatif par voie, et encore plus un cache ''skew-associative''. L'idée est d'utiliser un cache associatif par voie, mais où chaque voie est dédiée à une taille de page bien définie. Un cache associatif à trois voie pourra ainsi avoir une voie pour les pages de 4 kibioctets, une voie pour les pages de 2 mébioctets et une dernière voie pour les pages de plusieurs gibioctets. Cette méthode est conceptuellement équivalente au fait d'utiliser un cache pour chaque taille, à quelques différences près. Notamment, cela permet de mutualiser des circuits qui auraient été dupliqués en utilisant des caches séparés.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
c4e7np53bewcjv0fggce5vpb5q3xpz4
682096
682094
2022-07-21T01:17:19Z
Mewtow
31375
/* Le hash-rehashing */
wikitext
text/x-wiki
Dans le chapitre sur la mémoire virtuelle, nous avions abordé la pagination. Nous avions vu que la traduction des adresses virtuelles en adresses physiques se fait grâce à une table des pages située en mémoire RAM, qui contient les correspondances entre adresses physiques et virtuelles. Pour éviter d'avoir à lire la table des pages en mémoire RAM à chaque accès mémoire, les concepteurs de processeurs ont décidé d'implanter un cache dédié, le '''''translation lookaside buffer''''', ou TLB. Il stocke les entrées de la page des tables les plus récemment accédées.
[[File:MMU principle updated.png|centre|vignette|upright=2.0|MMU avec une TLB.]]
Les caches consomment beaucoup d'énergie et la TLB ne fait pas exception. Diverses estimations montrent que les TLB sont très consommatrices en énergie. Près de 5 à 15% de la consommation d'énergie d'un processeur provient de sa TLB et des circuits associés. Pour réduire cette consommation tout en gardant des performances très importantes, la TLB est conçue avec ces contraintes en tête. Notamment, la TLB est généralement un cache associatif par voie ou directement adressé, et non un cache totalement associatif. Les caches totalement associatifs consomment en effet beaucoup plus d'énergie pour fonctionner que les autres types de caches, pour des performances légèrement meilleures. Le cout en énergie ne vaut pas les performances gagnées, pour une LTB.
==L'accès à la TLB : succès et défauts d'accès==
À chaque accès mémoire, le processeur vérifie si le TLB contient l'adresse physique à laquelle accéder. Si c'est le cas, le processeur n'a pas à accéder à la table des pages en mémoire RAM et lit directement l'information nécessaire depuis ce TLB. On dit que l'on a un succès d'accès à la TLB. Si la TLb ne contient pas l'adresse physique demandée, on fait face à un '''défaut d'accès à la TLB'''. L'accès à la table des pages en mémoire RAM est alors inévitable.
[[File:Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Translation Lookaside Buffer]]
La traduction d'une adresse avec une TLB a lieu comme suit. Premièrement, le processeur (sa MMU) interroge la TLB : il envoie l'adresse virtuelle et la TLB répond. Si la TLB contient l'adresse physique associée, elle la fournit diretcement et le processeur au cache ou à la RAM directement. Si ce n'est pas le cas, le processeur accède à la table des pages en mémoire RAM. Si la page est en RAM, alors la table des pages renvoie l'adresse physique voulue et la TLB est mise à jour. PAr contre, si la page n'est pas en RAM, mais a été swappée sur le disque dur, la page est recopiée en mémoire RAM, la table des pages est mise à jour, puis la TLB l'est ensuite.
[[File:Steps In a Translation Lookaside Buffer.png|centre|vignette|upright=2.0|Steps In a Translation Lookaside Buffer]]
===Les ''hardware page table walkers''===
Les défauts d'accès à la TLB sont gérés de deux façons : soit le processeur gère tout seul la situation, soit il délègue cette tâche au système d’exploitation. Sur les processeurs anciens, le système d'exploitation gère le défaut d'accès à la TLB. En cas de défaut, le processeur lever une exception matérielle spécialisée, qui exécute une routine d'interruption chargée de gérer le défaut. C'est cette routine qui accède à la table des pages en RAM et gère la traduction d'adresse. Mais cette solution logicielle n'a pas de bonnes performances.
De nos jours, les processeurs gèrent eux-mêmes le défaut d'accès à la TLB et vont chercher d'eux-mêmes les informations nécessaires dans la table des pages. Ils disposent de circuits, les '''''page table walkers''''', qui s'occupent eux-mêmes du défaut. L'avantage en termes de performance est certain : plus besoin de lever une exception matérielle, plus besoin de faire une commutation de contexte pour exécuter la routine d'interruption, etc. De plus, ces circuits sont conçus pour gérer plusieurs défauts simultanés, ce qui a son utilité sur les processeurs superscalaires à exécution dans le désordre qu'on abordera dans quelques chapitres.
[[File:Page table actions.svg|centre|vignette|upright=2.0|Page table actions]]
==La hiérarchie des TLB==
Pour des raisons de performances, la TLB est parfois découpée en plusieurs sous-caches L1, L2, L3, etc. Sur les architectures Harvard et Harvard modifiées, on trouve parfois deux TLB séparés : un pour les accès aux instructions, et un pour les accès aux données. L'architecture standard pour les TLBs actuelles est la suivante : une TLB de niveau 1 (L1) pour les instructions, une autre TLB de niveau 1 (L1) pour les données, puis une TLB de niveaux 2 (L2) partagée entre instructions et données. D'autre structures matérielles sont présentes pour accélérer encore la traduction d'adresse ou la gestion de la TLB, comme des caches divers pour accélérer les défauts dans la TLB.
La séparation en une TLB L1 pour les instructions et une pour les données est nécessaire sur les architectures à hautes performances. Les processeurs modernes peuvent exécuter plusieurs instructions simultanément, ce qui a des conséquences. Il n'est pas rare que ces processeurs cherchent à accéder à des données en même temps qu'ils chargent une ou plusieurs instructions. Le nombre de données et d'instructions accédées en même temps est généralement élevé et une TLB unique devrait gérer plusieurs dizaines d'accès en même temps. La TLB doit alors être une mémoire multiports avec beaucoup de ports, beaucoup trop de ports pour être performante ou implémentable. Utiliser deux TLB séparées élimine ce problème, en limitant le nombre de ports sur chaque TLB. Un autre problème est que le programme prend moins de place que les données, ce qui se marie mal avec une TLB unique mais colle assez bien avec deux TLB spécialisées. La solution idéale est d'avoir une TLB d'instruction plus petite que celle pour les données.
==Les pages larges et leur impact sur la TLB==
Les processeurs actuels gèrent plusieurs tailles différentes pour les pages : 4 kibioctets par défaut, 2 mébioctets, voire 1 à 4 gibioctets pour les pages les plus larges. Les pages larges ont l'avantage d'utiliser la TLB de manière optimale. Pour une capacité de TLB identique, on peut adresser beaucoup plus de mémoire avec des pages larges, ce qui rend les défauts de TLB plus rares. Par contre, ces différentes tailles se marient mal avec une TLB unique. Un des problèmes est que les numéros de page physique n'ont pas le même nombre de bits suivant la taille des pages, ce qui pose des problèmes avec l'associativité de la TLB. Ce problème fait que l'usage d'une TLB unique est possible, mais pas forcément optimal.
===L'usage de plusieurs TLB, chacune dédiée à une taille de page===
L'usage d'une TLB unique pour toutes les tailles de page est possible et même courant pour la TLB de niveau 2 (L2). Mais les TLB de niveau 1 sont souvent séparées en plusieurs TLB, une par taille de page possible. Les TLB pour les pages larges sont souvent plus petites que la TLB pour les pages de 4 kibioctets, afin de profiter des économies de TLB liées aux pages larges. Un problème est que la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. Pour résoudre ce problème, toutes les TLB L1 sont accédées en parallèle lors d'un accès mémoire et le processeur sélectionne ensuite le résultat de celle qui correspond à la taille de page voulue.
===L'usage d'une TLB unique pour toutes les tailles de page===
Les TLB de niveau 2 gèrent plusieurs tailles de page en même temps. Mais les techniques pour faire cela sont assez couteuses en circuits et en performances. Aussi, les techniques utilisées pour gérer plusieurs tailles de page sont couramment implémentées sur les TLB de niveau 2, mais sont impraticables sur les TLB de niveau 1. Les deux techniques qui permettent cela sont appelées le '''''hash-rehashing''''' et le '''''skewing'''''. Elles répondent à un problème assez simple à 'expliquer : la taille de la page n'est connue du processeur qu'une fois la traduction d'adresse terminée, réalisée par la TLB. On doit accéder à la TLB en postulant que la page a une certaine taille, donc en précisant un numéro de page d'une certaine taille, pour ensuite avoir un défaut ou un succès de TLB.
====Le ''hash-rehashing''====
La première technique, celle du ''hash-rehashing'', consiste simplement à accéder la TLB en supposant que la page voulue a la taille usuelle, à savoir 4 kibioctets. En cas de succès de TLB, la page avait bien la taille supposée. Dans le cas contraire, on effectue un second accès en supposant que sa taille est de 2 mébioctets, et rebelote pour une page de 1-2 gibioctets en cas de défaut de TLB. Une fois toutes les tailles essayées, on accède à la table des pages en mémoire RAM. C'était la technique utilisée sur les processeurs Intel d’architecture Skylake et Broadwell. L'inconvénient est que les accès aux pages larges subissent une perte de performance notable, leur temps d'accès étant allongé. Cela est contrebalancé par le fait que ces tailles de page ont été inventées pour réduire le nombre de défauts de TLB, mais ces défauts sont assez rares pour l'impact soit seulement mitigé, pas compensé. L'allongement du temps d'accès peut être compensé par diverses méthodes.
La première méthode est celle des '''accès simultanés à la TLB'''. Les TLB sont des caches assez performants, qui sont conçus pour être capables d'effectuer plusieurs accès en parallèle. On peut alors profiter de cette particularité pour lancer plusieurs accès au TLB en même temps, un par taille de page. Avec cette technique, les accès se faisant en parallèle et non l'un après l'autre, le temps d'accès est presque le même que si la TLB ne gérait qu'une seule taille de page. Le désavantage est que les multiples accès consomment de l'énergie et du courant, ce qui augmente la consommation énergétique de la TLB, qui est déjà très importante.
La seconde solution est la '''prédiction de taille de page'''. L'idée est que le processeur tente de prédire quelle sera la taille de page, afin de tomber directement sur la bonne taille de page. Au lieu de tenter d'abord pour une page de 4 kibioctets, puis 2 mébioctets et enfin 1-2 Gibioctets, le processeur testera la taille de page la plus probable, puis la seconde plus probable, avant de tester la dernière taille de page possible. Le problème est alors de concevoir un circuit capable de réaliser cette prédiction.
* Une manière simple mais extrêmement inefficace de faire cela est de mémoriser la taille des pages récemment accédées dans un cache spécialisé, qui mémorise la correspondance entre le numéro de la page et sa taille. La taille de la page est mémorisée dans ce cache, après le premier accès à cette page. Mais le défaut que le temps d'accès à ce cache de prédiction s'ajoute au temps d'accès à la TLBL2, ce qui en ruine totalement l'intérêt. Aussi, il faut trouver une solution alternative.
* Une autre solution n'utilise pas le numéro de page, mais l'instruction responsable de l'accès à la TLB. Un accès à la TLB signifie un accès en mémoire RAM, qui est réalisé par une instruction. Instruction qui est identifiée par une adresse, elle-même située dans le ''program counter''. L'idée est que si une instruction est exécutée plusieurs fois, elle a tendance à accéder aux données d'une même page (localité spatiale). Ce n'est pas systématique, mais c'est une bonne supposition. On peut donc associer cette instruction à la taille de la page accédée récemment. Le cache de prédiction mémorise donc l'association entre adresse de cette instruction et taille de la page. Lors de l'accès mémoire, le ''program counter'' est récupéré et envoyé au cache de prédiction. En cas de succès d'accès, on obtient la taille de la page prédite. Cette méthode a le défaut que si plusieurs instructions accèdent à la même page, elles prendront chacune une ligne de cache dans le cache de prédiction et rien ne sera mutualisé.
* Une solution alternative est de récupérer lune adresse virtuelle proche de l'adresse lue/écrite avant que l'accès mémoire soit démarré. L'idée est que lors du décodage de l'instruction d'accès mémoire, on récupére les registres utilisés, et on y accéde pour lire en avance l'adresse virtuelle.
Une dernière méthode consiste à amortir le temps d'accès en cas de défaut dans la TLB L2. L'idée est de démarrer un accès à la table des pages en RAM en parallèle de l'accès à la TLB L2. La raison est que le temps d'accès à la TLB L2, avec ''hash-rehashing'', est très long. Il est sensiblement proche du quart du temps de défaut de TLB. Si en plus il fallait rajouter le temps d'accès à la table des pages en cas de défaut, le temps d'accès total serait énorme. Mais en lançant une lecture spéculative de la table des pages, le temps d'accès en cas de défaut est partiellement amorti. Le temps d'accès en cas de succès de TLB L2 reste cependant le même.
====Le ''skewing''====
La technique du ''skewing'' est assez simple à comprendre pour qui se rappelle ce qu'est un cache associatif par voie, et encore plus un cache ''skew-associative''. L'idée est d'utiliser un cache associatif par voie, mais où chaque voie est dédiée à une taille de page bien définie. Un cache associatif à trois voie pourra ainsi avoir une voie pour les pages de 4 kibioctets, une voie pour les pages de 2 mébioctets et une dernière voie pour les pages de plusieurs gibioctets. Cette méthode est conceptuellement équivalente au fait d'utiliser un cache pour chaque taille, à quelques différences près. Notamment, cela permet de mutualiser des circuits qui auraient été dupliqués en utilisant des caches séparés.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le préchargement
| prevTextLe préchargement
| next=Le pipeline
| nextText=Le pipeline
}}
</noinclude>
n5kac5x3a7arvqi8a81edc2v0nlwxlt
Mathc initiation/Fichiers h : c78eh
0
78681
682078
2022-07-20T20:44:28Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer ce fichier dans votre répertoire de travail.
{{Fichier|fh.h|largeur=70%|info=utilitaire|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as fh.h */
/* --------------------------------- */
double f1(
double x)
{
return( tanh(3.*x) );
}
char f1eq[] = "tanh(3x)";
/* --------------------------------- */
double f2(
double x)
{
return((3.*tanh(x) + tanh(x)*tanh(x)*tanh(x))
/
( 1. + 3.*tanh(x)*tanh(x)));
}
char f2eq[] = "(3*tanh(x)+tanh(x)**3)/(1+3tanh(x)**2)";
/* --------------------------------- */
/* --------------------------------- */
</syntaxhighlight>
{{AutoCat}}
m009gkgr3tfdtegmod9q9mfy3zs8qwz
Mathc initiation/Fichiers c : c78eh2
0
78682
682079
2022-07-20T20:47:56Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78dd| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01h.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1h.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "fh.h"
/* --------------------------------- */
int main(void)
{
double x = 1.8;
clrscrn();
printf(" x = %0.1f \n\n\n",x);
printf(" %s \t\t\t\t\t= %0.8f \n", f1eq, f1(x));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
x = 1.8
tanh(3x) = 0.99995920
(3*tanh(x)+tanh(x)**3)/(1+3tanh(x)**2) = 0.99995920
Press return to continue.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
Nous avons vu que :
tanh(y) + tanh(x)
tanh(x+y) = ------------------
1 + tanh(y) tanh(x)
posons y = 2x :
tanh(2x) + tanh(x)
tanh(x+2x) = ------------------
1 + tanh(2x) tanh(x)
tanh(2x) + tanh(x) (a)
tanh(3x) = --------------------
1 + tanh(2x) tanh(x) (b)
a) ------------------------------------------
2*tanh(x)
tanh(2x) + tanh(x) = ------------ + tanh(x)
1+tanh(x)**2
2*tanh(x) tanh(x) (1+tanh(x)**2)
tanh(2x) + tanh(x) = ------------ + ----------------------
1+tanh(x)**2 1+tanh(x)**2
2*tanh(x) tanh(x)+tanh(x)**3
tanh(2x) + tanh(x) = ------------ + ------------------
1+tanh(x)**2 1+tanh(x)**2
3*tanh(x) + tanh(x)**3
tanh(2x) + tanh(x) = ----------------------
1+tanh(x)**2
b) ------------------------------------------
2*tanh(x)
1 + tanh(2x) tanh(x) = 1 + ------------ tan(x)
1+tanh(x)**2
1+tanh(x)**2 2*tanh(x)**2
1 + tanh(2x) tanh(x) = ------------ + ------------
1+tanh(x)**2 1+tanh(x)**2
1 + 3 tanh(x)**2
1 + tanh(2x) tanh(x) = ----------------
1+tanh(x)**2
a/b) ------------------------------------------
3*tanh(x) + tanh(x)**3
----------------------
1+tanh(x)**2
tanh(3x) = ------------
1 + 3 tanh(x)**2
----------------
1+tanh(x)**2
donc
3*tanh(x) + tanh(x)**3
tanh(3x) = ------------------
1 + 3 tanh(x)**2
</syntaxhighlight>
{{AutoCat}}
5m0gi4pqpftlume6urnzit3u02fxg37
Mathc initiation/Fichiers h : c78ha
0
78683
682102
2022-07-21T10:57:08Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78hx| Sommaire]]
Installer ce fichier dans votre répertoire de travail.
{{Fichier|fa.h|largeur=70%|info=utilitaire|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as fa.h */
/* --------------------------------- */
double f1(
double x,
double y)
{
return(sinh(x) + sinh(y));
}
char f1eq[] = "sinh(x) + sinh(y)";
/* --------------------------------- */
/* --------------------------------- */
double f2(
double x,
double y)
{
return(2.* sinh((x+y)/2.) * cosh((x-y)/2.) );
}
char f2eq[] = "2 sinh((x+y)/2) cosh((x-y)/2)";
/* --------------------------------- */
/* --------------------------------- */
</syntaxhighlight>
{{AutoCat}}
d9umjbak9slciwscz9hfdmez8pybbgd
Mathc initiation/Fichiers c : c78ha2
0
78684
682103
2022-07-21T10:59:48Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78hx| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01a.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1a.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "fa.h"
/* --------------------------------- */
int main(void)
{
double x = 1.2;
double y = 1.5;
clrscrn();
printf(" (x,y) = (%0.1f,%0.1f) \n\n\n",x,y);
printf(" %s \t\t\t= %0.8f\n", f1eq, f1(x,y));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x,y));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
(x,y) = (1.2,1.5)
sinh(x) + sinh(y) = 3.63874081
2 sinh((x+y)/2) cosh((x-y)/2) = 3.63874081
Press return to continue.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
sinh(x) + sinh(y) = 2 sinh( (x+y)/2 ) cosh( (x-y)/2 )
sinh( X ) = [e**X - e**(-X) ] / 2
sinh((x+y)/2) = [e**((x+y)/2) - e**(-(x+y)/2)] / 2
cosh( X ) = [e**X + e**(-X) ] / 2
cosh((x-y)/2) = [e**((x-y)/2) + e**(-(x-y)/2)] / 2
sinh(x) + sinh(y) = 2 sinh( (x+y)/2 ) cosh( (x-y)/2 )
sinh(x) + sinh(y) = 2 [e**((x+y)/2)-e**(-(x+y)/2)]/2 [e**((x-y)/2)+e**(-(x-y)/2)]/2
2[sinh(x) + sinh(y)] = [e**((x+y)/2)-e**(-(x+y)/2)] [e**((x-y)/2)+e**(-(x-y)/2)]
(x+y)/2 + (x-y)/2 = x
(x+y)/2 + (-(x-y)/2)) = y
-(x+y)/2) + (x-y)/2 = -y
-(x+y)/2) + (-(x-y)/2) = -x
2[sinh(x) + sinh(y)] = e**x+e**y-e**(-y)-e**(-x)
2[sinh(x) + sinh(y)] = e**x-e**(-x) + e**y-e**(-y)
sinh(x) + sinh(y) = [e**x-e**(-x)]/2 + [e**y-e**(-y)]/2
sinh(x) + sinh(y) = sinh(x) + sinh(y)
</syntaxhighlight>
{{AutoCat}}
pu9e1qmxkrkajv113un5fnkabshrj1o
Mathc initiation/Fichiers h : c78hc
0
78685
682105
2022-07-21T11:06:14Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78hx| Sommaire]]
Installer ce fichier dans votre répertoire de travail.
{{Fichier|fc.h|largeur=70%|info=utilitaire|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as fc.h */
/* --------------------------------- */
double f1(
double x,
double y)
{
return(cosh(x) + cosh(y));
}
char f1eq[] = "cosh(x) + cosh(y)";
/* --------------------------------- */
/* --------------------------------- */
double f2(
double x,
double y)
{
return(2.* cosh((x+y)/2.) * cosh((x-y)/2.) );
}
char f2eq[] = "2 cosh((x+y)/2) cosh((x-y)/2) ";
/* --------------------------------- */
/* --------------------------------- */
</syntaxhighlight>
{{AutoCat}}
82vfo3123uf7wllvgsfeap749idlon9
Mathc initiation/Fichiers c : c78hc2
0
78686
682106
2022-07-21T11:07:22Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78hx| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01c.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1c.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "fc.h"
/* --------------------------------- */
int main(void)
{
double x = 1.2;
double y = 1.5;
clrscrn();
printf(" (x,y) = (%0.1f,%0.1f) \n\n\n",x,y);
printf(" %s \t\t\t= %0.8f\n", f1eq, f1(x,y));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x,y));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
(x,y) = (1.2,1.5)
cosh(x) + cosh(y) = 4.16306518
2 cosh((x+y)/2) cosh((x-y)/2) = 4.16306518
Press return to continue.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
cosh(x) + cosh(y) = 2 cosh( (x+y)/2 ) cosh( (x-y)/2 )
cosh( X ) = [e**X + e**(-X) ] / 2
cosh((x+y)/2) = [e**((x+y)/2) + e**(-(x+y)/2)] / 2
cosh((x-y)/2) = [e**((x-y)/2) + e**(-(x-y)/2)] / 2
cosh(x) + cosh(y) = 2 cosh( (x+y)/2 ) cosh( (x-y)/2 )
cosh(x) + cosh(y) = 2 [e**((x+y)/2)+e**(-(x+y)/2)] / 2 [e**((x-y)/2)+e**(-(x-y)/2)] / 2
2[cosh(x) + cosh(y)] = [e**((x+y)/2)+e**(-(x+y)/2)] [e**((x-y)/2)+e**(-(x-y)/2)]
(x+y)/2 + (x-y)/2 = x
(x+y)/2 + (-(x-y)/2)) = y
-(x+y)/2) + (x-y)/2 = -y
-(x+y)/2) + (-(x-y)/2) = -x
2[cosh(x) + cosh(y)] = e**x+e**y+e**(-y)+e**(-x)
2[cosh(x) + cosh(y)] = e**x+e**(-x) + e**y+e**(-y)
cosh(x) + cosh(y) = [e**x+e**(-x)]/2 + [e**y+e**(-y)]/2
cosh(x) + cosh(y) = cosh(x) + cosh(y)
</syntaxhighlight>
{{AutoCat}}
hlnwbw72pl1cux0de9hc1ol9rjp5px4
Mathc initiation/Fichiers h : c78hb
0
78687
682108
2022-07-21T11:12:38Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78hx| Sommaire]]
Installer ce fichier dans votre répertoire de travail.
{{Fichier|fb.h|largeur=70%|info=utilitaire|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as fb.h */
/* --------------------------------- */
double f1(
double x,
double y)
{
return(sinh(x) - sinh(y));
}
char f1eq[] = "sinh(x) - sinh(y)";
/* --------------------------------- */
/* --------------------------------- */
double f2(
double x,
double y)
{
return(2.* cosh((x+y)/2.)*sinh((x-y)/2.));
}
char f2eq[] = "2 cosh((x+y)/2) sinh((x-y)/2)";
/* --------------------------------- */
/* --------------------------------- */
</syntaxhighlight>
{{AutoCat}}
ij54axx0dltywt8ecvb5cv7gmwr1kug
Mathc initiation/Fichiers c : c78hb2
0
78688
682109
2022-07-21T11:14:17Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78hx| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01b.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1b.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "fb.h"
/* --------------------------------- */
int main(void)
{
double x = 1.2;
double y = 1.5;
clrscrn();
printf(" (x,y) = (%0.1f,%0.1f) \n\n\n",x,y);
printf(" %s \t\t\t= %0.8f\n", f1eq, f1(x,y));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x,y));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
(x,y) = (1.2,1.5)
sinh(x) - sinh(y) = -0.61981810
2 cosh((x+y)/2) sinh((x-y)/2) = -0.61981810
Press return to continue.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
sinh(x) - sinh(y) = 2 cosh( (x+y)/2 ) sinh( (x-y)/2 )
sinh( X ) = [e**X - e**(-X) ] / 2
sinh((x-y)/2) = [e**((x-y)/2) - e**(-(x-y)/2)] / 2
cosh( X ) = [e**X + e**(-X) ] / 2
cosh((x+y)/2) = [e**((x+y)/2) + e**(-(x+y)/2)] / 2
sinh(x) - sinh(y) = 2 cosh( (x+y)/2 ) sinh( (x-y)/2 )
sinh(x) - sinh(y) = 2 [e**((x+y)/2) + e**(-(x+y)/2)]/2 [e**((x-y)/2) - e**(-(x-y)/2)]/2
2[sinh(x) - sinh(y)] = [e**((x+y)/2) + e**(-(x+y)/2)] [e**((x-y)/2) - e**(-(x-y)/2)]
(x+y)/2 + (x-y)/2 = x
(x+y)/2 + (-(x-y)/2)) = y
-(x+y)/2) + (x-y)/2 = -y
-(x+y)/2) + (-(x-y)/2) = -x
2[sinh(x) - sinh(y)] = e**x-e**y+e**(-y)-e**(-x)
2[sinh(x) - sinh(y)] = e**x-e**(-x) - e**y+e**(-y)
2[sinh(x) - sinh(y)] = e**x-e**(-x) - [e**y-e**(-y)]
sinh(x) - sinh(y) = [e**x-e**(-x)]/2 - [e**y-e**(-y)]/2
sinh(x) - sinh(y) = sinh(x) - sinh(y)
</syntaxhighlight>
{{AutoCat}}
mqbwljorm598dx1danytyjwzzfoo7q2
Mathc initiation/Fichiers h : c78hd
0
78689
682111
2022-07-21T11:19:08Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78hx| Sommaire]]
Installer ce fichier dans votre répertoire de travail.
{{Fichier|fd.h|largeur=70%|info=utilitaire|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as fd.h */
/* --------------------------------- */
double f1(
double x,
double y)
{
return(cosh(x) - cosh(y));
}
char f1eq[] = "cosh(x) - cosh(y)";
/* --------------------------------- */
/* --------------------------------- */
double f2(
double x,
double y)
{
return(2.*sinh((x+y)/2.)*sinh((x-y)/2.));
}
char f2eq[] = "2 sinh( (x+y)/2 ) sinh( (x-y)/2 )";
/* --------------------------------- */
/* --------------------------------- */
</syntaxhighlight>
{{AutoCat}}
cqpxmj624odxevonofo59jcrc0k0way
Mathc initiation/Fichiers c : c78hd2
0
78690
682112
2022-07-21T11:19:51Z
Xhungab
23827
modification mineure
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/Fichiers h : c78hx| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c01d.c|largeur=70%|info=|icon=Crystal Clear mimetype source c.png}}
<syntaxhighlight lang="c">
/* --------------------------------- */
/* save as c1d.c */
/* --------------------------------- */
#include "x_hfile.h"
#include "fd.h"
/* --------------------------------- */
int main(void)
{
double x = 1.2;
double y = 1.5;
clrscrn();
printf(" (x,y) = (%0.1f,%0.1f) \n\n\n",x,y);
printf(" %s \t\t\t= %0.8f\n", f1eq, f1(x,y));
printf(" %s \t= %0.8f \n\n\n", f2eq, f2(x,y));
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Vérifions par le calcul :'''
<syntaxhighlight lang="dos">
(x,y) = (1.2,1.5)
cosh(x) - cosh(y) = -0.54175405
2 sinh( (x+y)/2 ) sinh( (x-y)/2 ) = -0.54175405
Press return to continue.
</syntaxhighlight>
'''Vérifions les égalités : '''
<syntaxhighlight lang="dos">
cosh(x) - cosh(y) = 2 sinh( (x+y)/2 ) sinh( (x-y)/2 )
sinh( X ) = [e**X - e**(-X) ] / 2
sinh((x+y)/2) = [e**((x+y)/2) - e**(-(x+y)/2)] / 2
sinh((x-y)/2) = [e**((x-y)/2) - e**(-(x-y)/2)] / 2
cosh(x) - cosh(y) = 2 sinh( (x+y)/2 ) sinh( (x-y)/2 )
cosh(x) - cosh(y) = 2 [e**((x+y)/2)-e**(-(x+y)/2)] / 2 [e**((x-y)/2)-e**(-(x-y)/2)] / 2
2[cosh(x) - cosh(y)] = [e**((x+y)/2)-e**(-(x+y)/2)] [e**((x-y)/2)-e**(-(x-y)/2)]
(x+y)/2 + (x-y)/2 = x
(x+y)/2 + (-(x-y)/2)) = y
-(x+y)/2) + (x-y)/2 = -y
-(x+y)/2) + (-(x-y)/2) = -x
2[cosh(x) - cosh(y)] = e**x-e**y-e**(-y)+e**(-x)
2[cosh(x) - cosh(y)] = e**x+e**(-x) - e**y-e**(-y)
2[cosh(x) - cosh(y)] = e**x+e**(-x) - (e**y+e**(-y))
cosh(x) - cosh(y) = (e**x+e**(-x))/2 - (e**y+e**(-y))/2
cosh(x) - cosh(y) = cosh(x) - cosh(y)
</syntaxhighlight>
{{AutoCat}}
k4k5jrsbgajybi34twqvlaoe7fsl6bh