40 609
modifications
m (orthotypo) |
|||
==L'accès au cache==
Le cache est divisé en groupes de plusieurs bytes (de 64 à 256 octets chacun), qui portent le nom de '''lignes de cache'''. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins.
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM. Le nombre de succès de cache par nombre d'accès mémoire , appelé le '''taux de succès''' (''hit ratio''), est déterminant pour les performances : plus il est élevé, plus le cache est efficace.
Les défauts de cache peuvent avoir des origines diverses. Lorsqu'on exécute une instruction ou qu'on accède à une donnée pour
===Le tag d'une ligne de cache===
[[File:Cache hash table.png|vignette|Cache hash table]]
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Pour faire la correspondance entre une ligne de cache et l'adresse mémoire correspondante, la ligne de cache mémorise tout ou partie de l'adresse mémoire correspondante. Les bits de l'adresse mémoire qui sont mémorisés dans la ligne de cache forment dce qu'on appelle le '''tag'''. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et compare celui-ci avec les
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Sur ces caches, il n'a pas d'index ou quoique ce soit du genre. L'adresse mémoire est donc juste découpée en un tag, et de quoi identifier la position de la donnée dans la ligne de cache correspondante. Pour déterminer l’occurrence d'un défaut de cache, il suffit de comparer le tag de l'adresse avec
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
====LRU amélioré====
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
===Les caches ''Write-through''===
Sans
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. A chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non
===Les caches ''Write-back''===
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=2|Circuit d'arbitrage du cache.]]
Le cache L1 dédié aux instructions est souvent en « lecture seule » : on ne peut pas modifier son contenu, mais juste le lire ou charger des instructions dedans. Cela complique la gestion du code automodifiant, c'est-à-dire des
Sur certains processeurs, l'étape de décodage est assez complexe et lente. Pour accélérer cette étape, certains concepteurs de processeurs ont décidés d'utiliser la (ou les) mémoire cache dédiée aux instructions pour accélérer ce décodage. Lorsque ces instructions sont chargées depuis la RAM ou les niveaux de cache inférieurs, celles-ci sont partiellement décodées. On peut par exemple rajouter des informations qui permettent de délimiter les instructions ou déterminer leur taille, ce qui est utile pour décoder les instructions de taille variable. Bref, le cache d'instructions peut se charger d'une partie du décodage des instructions, grâce à un circuit séparé de l'unité de décodage d'instruction.
===Les caches à accès non-uniforme===
Sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, mais cela gâche les
La première version de ce genre de caches a une correspondance ligne de cache → bloc de mémoire statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais des versions plus optimisées en sont capables : la correspondance entre une ligne de cache et un bloc de mémoire cache peut varier à l’exécution. Ainsi, les lignes de cache les plus utilisées peuvent migrer dans un bloc de mémoire plus rapide : cela demande juste de copier les données entre blocs de mémoire et de mettre à jour la correspondance entre ligne de cache et bloc de mémoire.
Un '''cache bloquant''' est un cache auquel le processeur ne peut pas accéder pendant un défaut de cache. Il faut attendre que la lecture ou écriture en RAM soit terminée avant de pouvoir utiliser de nouveau le cache. Un '''cache non bloquant''' n'a pas ce problème : on peut l'utiliser pendant un défaut de cache. Tous les caches non bloquants permettent ainsi de démarrer une nouvelle lecture ou écriture alors qu'une autre est en cours. C, ce qui permet d'exécuter plusieurs lectures ou écritures en même temps. Mais le cache ne peut supporter qu'un nombre limité d'accès mémoires simultanés (pipelinés).
Su un cache bloquant, lors d'un défaut de cache, le cache et la mémoire RAM communiquent directement. Pendant un défaut de cache, le contrôleur mémoire charge la donnée depuis la mémoire et la copie plus ou moins directement dans le cache. Le cache étant utilisé, il ne peut pas être utilisé aussi bien
Les caches non bloquants sont de deux types, qui portent les noms
===Les caches non bloquants de type succès après défaut===
===Les caches non bloquants de type défaut après défaut===
Au-dessus, on a vu des caches incapables de gérer plusieurs défauts simultanés.Voyons maintenant les caches
====Les ''miss status handling registers''====
* un bit de validité qui indique si le MSHR est vide ou pas et qui est mis à 0 quand le défaut de cache est résolu.
Supposons qu'un défaut de cache
====Les accès simultanés à une même ligne de cache====
* un bit empty, qui dit si l'entrée est vide ou non.
La fusion de deux défauts de cache est ainsi assez simple. Si deux défauts de cache à la même adresse lisent des mots mémoire différents, il suffit de configurer les entrées convenablement. Le premier défaut configurera ses entrées, et le second aura les siennes. Avec cette organisation, le nombre de MSHR indique combien de lignes de cache peuvent être lues en même temps depuis la mémoire. Quant
Pour éviter de bloquer le cache lors d'accès à la même ligne de cache, certains chercheurs ont
* les index et décalages d'un accès mémoire dans la ligne de cache ;
* la destination de la lecture effectuée par le défaut de cache (généralement un registre) ;
* un bit empty, qui dit si l'entrée est vide ou non.
Avec cette organisation, le nombre de MSHR indique combien de lignes de cache peuvent être lues en même temps depuis la mémoire. Quant
Généralement, plus on veut supporter de défauts de cache, plus le nombre de MSHR et d'entrées augmente. Mais au-delà d'un certain nombre d'entrées et de MSHR, les MSHR adressés implicitement et explicitement ont tendance à bouffer un peu trop de circuits. Utiliser une organisation un peu moins gourmande en circuits est donc une nécessité. Cette organisation plus économe se base sur des ''MSHR inversés''. Ces MSHR ne contiennent qu'une seule entrée, en quelque sorte : au lieu d’utiliser n MSHR de m
Certains chercheurs ont remarqué que pendant qu'une ligne de cache est en train de subir un défaut de cache, celle-ci reste inutilisée, et son contenu est destiné à être perdu une fois le défaut de cache résolu. Ils se sont dit que, plutôt que d'utiliser des MSHR séparés, il vaudrait mieux utiliser la ligne de cache pour stocker les informations sur les
==Le ''cache bypassing'' : contourner le cache==
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache.
La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demande d'ajouter un cache un peu particulier au processeur. Ce cache s'appelle la '''table d’historique des défauts de lecture''' (''load miss history table''). Comme son nom l'indique, cette table mémorise, pour chaque instruction, un petit historique des défauts de cache qu'
==Les caches adressés par somme et hashés==
Pour rappel, la gestion de certains modes d'adressage demande que le processeur calcule l'adresse. Les calculs d'adresse en question sont généralement assez simples : ils prennent une adresse de base, à laquelle ils ajoutent une constante. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la
===Les caches hashés===
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
|