« Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs » : différence entre les versions
Contenu supprimé Contenu ajouté
mAucun résumé des modifications |
|||
Ligne 1 :
Rien ne vaut l'utilisation de plusieurs processeurs pour réellement tirer
==Types de multicœurs==
Ligne 7 :
===Multicœurs asymétrique===
Le processeur CELL est un des exemples les plus
[[File:Cell Arch.png|centre|650px|Architecture du processeur CELL.]]
Ligne 13 :
===Cluster multithreading===
Sur certains processeurs
==Interruptions inter-processeurs==
===L'exemple du X86===
Les anciens PC incorporaient sur leur carté mère un contrôleur d'interruption crée par Intel, le 8259A, qui ne gérait pas les interruptions inter-processeurs. Pour gérer cette situation, les carte mères multiprocesseurs devaient incorporer un contrôleur spécial en complément. De nos jours, chaque cœur x86 possède son propre contrôleur d’interruption, le local APIC, qui s'occupe de gérer les interruptions en provenance ou arrivant vers ce processeur. On trouve aussi un IO-APIC, qui s'occupe de gérer les interruptions en provenance des périphériques et de les redistribuer vers les
[[File:Contrôleurs d'interrptions sur systèmes x86 multicoeurs.png|centre|Contrôleurs d’interruptions sur systèmes x86 multicœurs.]]
Ligne 29 :
==Partage des caches==
Quand on conçoit un processeur
{|
Ligne 38 :
===Caches dédiés et partagés===
Avant toute chose, il faut savoir que la quantité de mémoire cache que l'on peut placer sur une surface donnée est limitée : on ne peut pas mettre autant de cache que l'on veut dans un processeur. Et le cache prend une très grande place dans notre processeur : environ la moitié, voire 60% des circuits de notre processeur servent à intégrer la mémoire cache ! Autant vous dire que le cache est une ressource précieuse. Et cela pose un problème pour les architectures qui utilisent des caches séparés pour chaque cœur : ceux-ci seront individuellement assez petits. Le principal défaut des architectures à cache dédiés vient de là. Si on exécute un programme prévu pour n'utiliser qu'un seul cœur, celui-ci n'aura accès qu'à un seul cache : celui dédié au cœur sur lequel il va s’exécuter. Alors qu'est-ce qui est le mieux :
[[File:Cache partagé contre cache dédié.png|centre|Cache partagé contre cache dédié]]
Ligne 46 :
===Architecture hybride===
Dans la réalité, il faut nuancer un tout petit peu les choses : un processeur
On peut décider de partager un cache entre tous les cœurs, voire limiter ce partage à quelques cœurs particuliers pour des raisons de performances. Ainsi, rien n’empêche pour un processeur quad-core d'avoir deux caches L2, chacun partagés avec deux cœurs, et le cache L3 partagé entre tous les cœurs.
Ligne 52 :
{|
|[[File:Dual Core Generic.svg|centre|vignette|upright=1.25|Partage des caches sur un double cœurs.]]
|[[File:Partage des caches sur un processeur multicoeurs.png|centre|vignette|upright=2.0|Partage des caches sur un processeur
|}
Ligne 61 :
[[File:Cohérence des caches.png|centre|Cohérence des caches]]
Les caches partagés ne posent aucun problème de cohérence. Avec eux, une donnée n'est pas dupliquée en plusieurs exemplaires, mais n'est présente qu'une seule fois dans tout le cache. Ainsi, si on écrit celle-ci, on peut être
[[File:Shared cache coherency.png|centre|Cohérence et caches partagés]]
Ligne 81 :
Tout protocole de cohérence des caches doit répondre ce problème : comment les autres caches sont-ils au courant qu'ils ont une donnée périmée ? Pour cela, il existe deux solutions : l'espionnage du bus et l'usage de répertoires. Certains protocoles utilisent un '''répertoire''', une table de correspondance qui mémorise, pour chaque ligne de cache, toutes les informations nécessaires pour maintenir sa cohérence. Ces protocoles sont surtout utilisés sur les architectures distribuées : ils sont en effet difficiles à implémenter, tandis que leurs concurrents sont plus simples à implanter sur les machines à mémoire partagée.
Avec l’'''espionnage du bus''', les caches interceptent les écritures sur le bus (qu'elles proviennent ou non des autres processeurs), afin de lettre à jour la ligne de cache ou de la périmer. Avec ces protocoles, chaque ligne de cache contient des bits qui indique si la donnée contenue est à jour ou périmée. Quand on veut lire une donnée, les circuits chargés de lire dans le cache vont vérifier ces bits. Si ces bits indiquent que la ligne de cache est périmée, le processeur va lire les données depuis la RAM ou les autres caches et mettre à jour la ligne de cache. La mise à jour de ces bits de validité a lieu à chaque écriture (y compris les caches des autres
===Protocole sans nouveaux états===
Ligne 106 :
===Protocoles à état Exclusif===
Le protocole MSI n'est pas parfait : si un seul cache possède une donnée, on aura prévenu les autres caches pour rien en cas d'écriture. Ces communications sur le bus ne sont pas gratuites et en faire trop
{|
|[[File:Diagrama MESI.GIF|Diagramme du protocole MESI. Les abréviations PrRd et PrWr correspondent à des accès mémoire initiés par le processeur associé au cache, respectivement aux lectures et écritures. Les abréviations BusRd et BusRdx et Flush correspondent aux lectures, lectures exclusives ou écritures initiées par d'autres processeurs sur la ligne de cache.]]
|[[File:MESI State Transaction Diagram.svg|Autre description du
|}
Ligne 123 :
[[File:MOESI State Transaction Diagram.svg|centre|MOESI State Transaction Diagram]]
Cependant, celui-ci n'est pas le seul. On peut notamment citer le '''protocole MOSI''', une variante du MESI où l'état
{|
Ligne 130 :
|}
Lors d'une lecture, le cache va vérifier si la lecture envoyée sur le bus correspond à une de ses
* pour savoir si un cache contient une copie de la donnée demandée, chaque cache devra répondre en fournissant un bit ;
Ligne 146 :
Pour avoir le bon résultat il y a une seule et unique solution : le processeur qui accède à la donnée doit avoir un accès exclusif à la donnée partagée. Sans cela, l'autre processeur ira lire une version de la donnée qui n'aura pas encore été modifiée par l'autre processeur. Dans notre exemple, un seul thread doit pouvoir manipuler notre compteur à la fois. Et bien sûr, cette réponse peut, et doit se généraliser à presque toutes les autres situations impliquant une donnée partagée. Chaque thread doit donc avoir un accès exclusif à notre donnée partagée, sans qu'aucun autre thread ne puisse manipuler notre donnée. On doit donc définir ce qu'on appelle une '''section critique''' : un morceau de temps durant lequel un thread aura un accès exclusif à une donnée partagée : notre thread est certain qu'aucun autre thread n'ira modifier la donnée qu'il manipule durant ce temps. Autant prévenir tout de suite : créer de telles sections critiques se base sur des mécanismes mêlant le matériel et le logiciel. Il existe deux grandes solutions, qui peuvent être soit codées sous la forme de programmes, soit implantées directement dans le silicium de nos processeurs.
Voyons la première de ces solutions : l''''exclusion mutuelle'''. Avec celle-ci, on fait en sorte qu'un thread puisse réserver la donnée partagée. Un thread qui veut manipuler cette donnée va donc attendre qu'elle soit libre pour la réserver afin de l'utiliser, et la libérera une fois qu'il en a fini avec elle. Si la donnée est occupée par un thread, tous les autres threads devront attendre leur tour. Pour mettre en œuvre cette réservation/dé-réservation, on va devoir ajouter un compteur à la donnée partagée, qui indique si la donnée partagée est libre ou déjà réservée. Dans le cas le plus simple, ce compteur vaudra 0 si la donnée est réservée, et 1 si elle est libre. Ainsi, un thread qui voudra réserver la donnée va d'abord devoir vérifier si ce nombre est à 1 avant de pouvoir réserver sa donnée. Si c'est le cas, il réservera la donnée en passant ce nombre à 0. Si la donnée est réservée par un autre thread, il devra tout simplement attendre son tour. On a alors
[[File:Mutex.png|centre|Mutex]]
Ligne 172 :
|}
Comme je l'ai dit plus haut, ces instructions empêchent tout autre processeur d'aller lire ou modifier la donnée qu'elles sont en train de modifier. Ainsi, lors de l’exécution de l'instruction atomique, aucun processeur ne peut aller manipuler la mémoire : notre instruction atomique va alors bloquer la mémoire et en réserver l'accès au bus mémoire rien que pour elle. Cela peut se faire en envoyant un signal sur le bus mémoire, ou pas d'autres mécanismes de synchronisation entre processeurs.
===Instructions LL/SC===
Ligne 190 :
===Speculative Lock Elision===
Les instructions atomiques sont lentes, sans compter qu'elles sont utilisées de façon pessimistes : l'atomicité est garantie même si aucun autre thread n'accède à la donnée lue/écrite. Aussi, pour accélérer l'exécution des instructions atomiques, des chercheurs se sont penchés sur ce problème de réservations inutiles et ont trouvé une solution assez sympathique, basée sur la mémoire transactionnelle. Au lieu de devoir mettre un
Ainsi, on n’exécute pas l'instruction atomique permettant d'effectuer une réservation, et on la transforme en une simple instruction de lecture de la donnée partagée. Une fois cela fait, on commence à exécuter la suite du programme en faisant en sorte que les autres processeurs ne voient pas les modifications effectuées sur nos données partagées. Puis, vient le moment de libérer la donnée partagée via une autre instruction atomique. A ce moment, si aucun autre thread n'a écrit dans notre donnée partagée, tout ira pour le mieux : on pourra rendre permanents les changements effectués. Par contre, si jamais un autre thread se permet d'aller écrire dans notre donnée partagée, on doit annuler les changements faits. A la suite de l'échec de cette exécution optimiste, cette transaction cachée, le processeur reprendre son exécution au début de notre fausse transaction, et va exécuter son programme normalement : le processeur effectuera alors de vraies instructions atomiques, au lieu de les interpréter comme des débuts e transactions.
Ligne 198 :
===L'exemple avec le x86===
Autre exemple, on peut citer les processeurs Intel récents. Je pense notamment aux processeurs basés sur l’architecture Haswell. Au
Le mode TSX correspond simplement à quelques instructions supplémentaires permettant de gérer la mémoire transactionnelle matérielle. On trouve ainsi trois nouvelles instructions : XBEGIN, XEND et XABORT. XBEGIN sert en quelque sorte de top départ : elle sert à démarrer une transaction. Toutes les instructions placées après elles dans l'ordre du programme seront ainsi dans une transaction. Au cas où la transaction échoue, il est intéressant de laisser le programmeur quoi faire. Pour cela, l'instruction XBEGIN permet au programmeur de spécifier une adresse. Cette adresse permet de pointer sur un morceau de code permettant de gérer l'échec de la transaction. En cas d'échec de la transaction, notre processeur va reprendre automatiquement son exécution à cette adresse. Évidemment, si on a de quoi marquer le début d'une transaction, il faut aussi indiquer sa fin. Pour cela, on utilise l'instruction XEND. XABORT
HLE
Les processeurs Haswall supportent aussi le Lock Elision. Les instructions atomiques peuvent ainsi supporter l’exécution en tant que transaction à une condition : qu'on leur rajoute un préfixe. Le préfixe, pour les instructions x86, correspond simplement à un octet optionnel, placé au début de notre instruction dans la mémoire. Cet octet servira à donner des indications au processeur, qui permettront de modifier le comportement de notre instruction. Par exemple, les processeurs x86 supportent pas mal d'octets de préfixe : LOCK, qui permet de rendre certaines instructions atomiques, ou REPNZE, qui permet de répéter certaines
==Consistance mémoire==
Ligne 224 :
|-
!No Store Ordering
|Toutes les réorganisations possibles entre lectures et écritures sont autorisées, tant qu'
|}
Ligne 237 :
===L'exemple du modèle mémoire x86===
Après avoir vu la théorie, passons maintenant à la pratique. Dans cette partie, on va voir les modèles de consistances utilisés sur les processeurs x86, ceux qu'on retrouve dans nos PC actuels. Le modèle de consistance des processeurs x86 a varié au cours de l'existence de l'architecture : un vulgaire 486DX n'a pas le même modèle de consistance qu'un Core 2 duo, par exemple.
* ces écritures doivent se faire dans la mémoire cache ;
Ligne 253 :
* des lectures peuvent être déplacées avant des écritures, si ces écritures et la lecture ne se font pas au même endroit, à la même adresse.
Dans cette liste, j'ai mentionné le fait que les écritures en mémoire peuvent changer dans certains cas exceptionnels. Ces cas exceptionnels sont les écritures effectuées par les instructions de gestion de tableaux et de chaines de caractères, ainsi que certaines instructions SSE. Ces instructions SSE sont les instructions qui permettent d'écrire des données sans passer par le cache, mentionnées il y a quelques chapitres. Ce sont donc les instructions MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPS, MOVNTPD. Mais ce ne sont pas les seules : le x86 possède quelques instructions permettant de travailler directement sur des chaines de caractères ou des tableaux : ce sont les
<noinclude>
|