Les microprocesseurs généralistes sont apparus commercialement en 1971 avec l’Intel 4004. Il était alors possible d’intégrer tous les composants d’un processeur dans un seul boîtier. Il est désormais possible d’intégrer des dizaines et très bientôt des centaines de coeurs dans un seul boîtier. La complexité de telles architectures n’est pas, ou n’est plus, dans les unités de calcul. La complexité se trouve dans le système mémoire. En effet, au début, le système mémoire devait être capable de fournir à l’unité de calcul une nouvelle instruction tous les 5 à 10 cycles d’horloge et lire ou écrire des données avec un débit plus faible encore. Dans les architectures modernes, et pour celles à venir, le système mémoire doit fournir des centaines d’instructions à chaque cycle et permettre des dizaines d’accès simultanées en lecture ou en écriture de données. Pour parvenir à atteindre ces débits, le système mémoire est devenu une hiérarchie de caches de mémoire. Les caches permettent de rapprocher des unités de calcul, les instructions et les données fréquemment utilisées en les recopiant dans des petites mémoires locales. Toutefois, ces caches doivent tous être cohérents. La gestion de la cohérence de caches est un problème d’autant plus délicat que les solutions doivent être extensibles (passer à l’échelle), et donc rester performantes, quelque soit le nombre de caches à maintenir cohérent. En outre, les solutions doivent être économes en énergie, c’est-à-dire ne pas imposer de nombreux échanges d’information entre les caches.
Architectures manycores
Les architectures multicœurs [1] sont apparues commercialement au début des années 2000, mais elles se sont imposées à partir de 2005 [2]. Avant cette date, l’augmentation du nombre d’instructions exécutées par seconde était obtenue en complexifiant l’architecture interne des cœurs et en augmentant leur fréquence de fonctionnement. Ce faisant, les processeurs dissipaient toujours plus de puissance électrique. Le problème est qu’il n’est pas simple d’évacuer la chaleur produite par un circuit avec une simple ventilation au-delà de 100W/cm2. Alors, pour continuer à augmenter le nombre d’instructions exécutées par seconde, les constructeurs ont choisi de remplacer un cœur complexe par deux plus simples sans augmenter le budget énergétique. La réduction de puissance électrique consommée par cœur a été obtenue en simplifiant leur architecture interne, par exemple en réduisant les mécanismes d’exécution spéculative. Ils ont également plafonné la fréquence de fonctionnement ou encore l’ont rendu modulable, tout comme la tension d’alimentation, en fonction de la charge de calcul. Dès lors, le nombre de cœurs n’a cessé d’augmenter, pour atteindre plusieurs dizaines et bientôt plusieurs centaines par circuit. Les architectures contenant quelques cœurs sont nommées multicœurs. Au-delà de cent cœurs, elles sont massivement multicœurs, mais le terme consacré est manycore.
Architectures à mémoire partagée
Il existe plusieurs types d’architecture manycore. Notre travail porte sur un type en particulier : les architectures manycores à mémoire partagée et caches cohérents [3]. Dans ces architectures : (i) tous les cœurs partagent le même espace d’adressage physique, c’est-à-dire qu’ils partagent la même mémoire ; (ii) le système mémoire est constitué d’une hiérarchie des caches dont les mouvements de données sont gérés par des automates matériels ; (iii) la cohérence des caches est garantie par des automates matériels ; (iv) chaque application dispose d’un espace d’adressage propre dit espace virtuel, et chaque cœur contient une unité de traduction d’adresses virtuelles vers adresse physique. Les architectures (i) sans caches ou (ii) avec des espaces d’adressages distincts pour chaque cœur (ou groupe de cœurs) imposant au logiciel la gestion du mouvement des données entre les bancs de mémoire ou encore ; (iii) sans cohérence des caches ou enfin ; (iv) sans mémoire virtuelle ne rentrent pas dans le cadre de notre étude. En effet, seules les architectures à mémoire partagées et à caches cohérents sont capables d’exécuter simplement des systèmes d’exploitation de type UNIX, comme Linux ou BSD. Il existe deux grands types d’architectures à espace d’adressage partagé [4] : les architectures SMP (Symmetric Multi Processor) et les architectures NUMA (Non Uniform Memory Access).
Mémoires caches
Plus il y a de cœurs, plus le nombre d’instructions exécutées par cycle est important, plus la quantité de données traitées augmente, plus le débit des échanges entre les cœurs et la mémoire augmente. La hiérarchie des caches présente entre les cœurs et la mémoire principale permet d’augmenter ce débit, en recopiant localement, au plus près des cœurs, les instructions et les données fréquemment utilisées. La capacité des caches est bien inférieure à la capacité de stockage de la mémoire principale. Les caches copient temporairement une partie du contenu de la mémoire. Les caches de premier niveau sont les plus rapides, mais ils sont aussi les plus petits. Plus on s’éloigne des cœurs, plus les caches sont partagés, plus ils sont grands, mais plus ils sont lents. Les caches stockent des blocs de données contiguës en mémoire. Ces blocs sont de taille fixe et sont appelés lignes de cache (cache line en anglais). L’adresse d’un octet à l’intérieur d’une ligne de cache est nommée offset. L’adresse en mémoire de chaque ligne présentée dans un cache est rangée dans un répertoire. Lors d’un accès mémoire par un cœur, le répertoire permet de savoir si une donnée est présente dans le cache en y recherchant son numéro de ligne. Afin de réduire le temps d’accès, la position de la ligne dans le cache est définie par une partie de l’adresse de la ligne appelée set. Le reste de l’adresse de ligne est, nommé tag, est stocké dans le répertoire.
Protocoles de cohérence
Dans une architecture manycore disposant des caches, chaque cœur contient son ou ses caches privés (caches de premier niveau), lesquels accèdent à des caches de niveau supérieur et le dernier niveau de cache (LLC comme Last Level Cache) est partagé par tous les cœurs. Dans un programme (une application) multitâches coopératives, une ligne de cache écrite par un coeur peut être lue par plusieurs autres coeurs. Par conséquent, les caches privés des différents lecteurs stockent la même ligne de cache partagée. Lorsque le cœur écrivain modifie une donnée dans la ligne partagée, il faut que tous les cœurs lecteur voient cette modification. Il faut donc que la ligne partagée soit mise à jour ou invalidée dans chaque cache privé de lecteur. Il n’est pas envisageable de faire traiter cette mise à jour par le programme applicatif lui-même. C’est pourquoi un protocole de cohérence des caches doit être implémenté en matériel, ou dans le système d’exploitation [5] :
• Pour les protocoles de cohérence logiciels : le système d’exploitation (ou les bibliothèques systèmes) contrôle toutes les pages du système mémoire. Le noyau du système d’exploitation connaît le placement des pages partagées, il est donc capable, en principe, de gérer la cohérence entre les différentes copies.
• Pour les protocoles de cohérence matériels : le protocole de cohérence est implémenté dans les contrôleurs des caches qui gèrent la hiérarchie mémoire et la modification d’une ligne de cache provoque des échanges de message entre les caches de différents niveaux.
Les protocoles de cohérence logiciels imposent des contraintes fortes. En particulier, la granularité de partage de données est la page de mémoire virtuelle, alors que pour les protocoles de cohérence des caches matériels, la granularité de partage est la ligne de cache. Dans cette thèse, nous nous intéressons aux protocoles de cohérence matérielle. Il en existe deux grandes classes : Snoop et Directory [6] :
Protocole Snoop
Le protocole Snoop est largement utilisé par les architectures SMP. L’idée principale est que chaque cache observe (Snoop en anglais) les requêtes de cohérence émises par les autres caches afin de mettre à jour l’état de ses propres lignes. Le protocole Snoop a une latence de mise à jour très basse. Il est, par ailleurs, simple à l’implémenter. Toutefois il fait l’hypothèse de l’existence d’un bus partagé. C’est là le principal désavantage. En effet, sur un bus la bande passante est bornée et doit être partagée entre tous les cœurs. Le nombre de coeurs connectés sur un bus ne dépasse généralement pas une dizaine de coeurs.
Protocole Global Directory
Le protocole Global Directory est implémenté dans l’architecture NUMA pour résoudre le problème du passage à l’échelle du protocole Snoop. Le protocole Global Directory est conceptuellement implanté du côté du contrôleur mémoire, qui gère un répertoire global de l’état des toutes les lignes de cache. Ce répertoire global contient précisément le nombre de copies de chaque ligne, voire la localisation de chaque copie. Chaque contrôleur de cache informe directement le répertoire global des modifications qu’il fait sur ses propres copies. Le répertoire global envoie alors les requêtes de cohérence vers les autres contrôleurs de cache concernés. Avec le protocole Global Directory, c’est le répertoire global, par lequel passent toutes les demandes des contrôleurs de cache qui va garantir l’ordre du traitement des requêtes de cohérence. Ce protocole est donc bien adapté aux architectures manycore NUMA. Actuellement, la plupart des architectures multiprocesseurs industrielles possédant plus d’une dizaine de coeurs implémentent le protocole Global Directory.
|
Table des matières
Introduction
1 Problématique
1.1 Architectures manycores
1.1.1 Architectures à mémoire partagée
1.1.2 Mémoires caches
1.1.3 Protocoles de cohérence
1.1.4 Stratégies d’écriture
1.1.5 Mémoire virtuelle
1.1.6 Cohérence des TLBs
1.2 L’architecture TSAR
1.2.1 Hiérarchie des caches
1.2.2 Réseaux d’interconnexion
1.3 Protocole DHCCP dans l’architecture TSAR
1.3.1 États d’une ligne dans le cache L2
1.3.2 Les 4 types de transactions de cohérence
1.3.3 États d’une ligne dans le cache L1
1.4 Cohérence des TLBs dans l’architecture TSAR
1.4.1 MMU générique de TSAR
1.4.2 Cohérence des TLBs dans TSAR
1.5 Points faibles du protocole DHCCP actuel
1.5.1 Trafic d’écriture
1.5.2 Inclusivité des TLBs dans les caches L1
1.5.3 Consommation du cache instruction
1.6 conclusion
2 État de l’art
2.1 Protocoles à base de Snoop
2.2 Protocoles à écriture immédiate
2.3 Protocoles MESI et MOESI
2.3.1 Protocole MOESI chez AMD
2.3.2 Protocole GOLS pour le coprocesseur Intel Xeon Phi
2.3.3 Protocole ACKwise du MIT
2.4 Conclusion
3 Protocole Released Write Through
3.1 Principe du protocole RWT
3.2 Conséquences pour le cache L1
3.2.1 Évincement d’une ligne de cache
3.2.2 États d’une case dans le cache L1
3.3 Conséquences pour le cache L2
3.3.1 Évolution de l’état d’une ligne de cache
3.3.2 États d’une case dans le cache L2
3.3.3 Mécanisme d’évincement dans le cache L2
3.4 Surcoût matériel du protocole RWT
3.5 Conclusion
4 Protocole HMESI
4.1 Principe du protocole HMESI
4.1.1 Requête GetM
4.1.2 Transactions de cohérence
4.2 Implémentation du protocole HMESI
4.2.1 Table d’Invalidations
4.2.2 État LOCKED
4.2.3 Diagramme de transition du cache L1
4.2.4 Requête Multi ack miss
4.3 Protocole MOESI dans TSAR ?
4.4 Conclusion
Conclusion
Télécharger le rapport complet