Cohérence de cache et protocoles
Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar
Ce que la séance construit
Chaque cœur a ses propres caches : une même donnée peut exister en plusieurs copies. La séance montre comment le matériel garde ces copies cohérentes (MSI, MESI), ce que cela coûte, comment un programme en souffre sans partager de données (false sharing), et dans quel ordre les cœurs voient les écritures (cohérence séquentielle, TSO).
Plan
Prérequis : Séance 1 (hiérarchie mémoire, lignes de \(64\) octets, succès et défauts de cache).
Lab associé : Lab 3 (la mémoire : caches, copies et partage).
Ensuite : Séance 3, programmation parallèle avancée (synchronisation).
D’où l’on vient, où l’on va
Amont. Séance 1 (hiérarchie mémoire, lignes de \(64\) octets, succès et défauts de cache).
Cette séance. Le problème de cohérence de cache, opérations du processeur et transactions sur le bus, protocoles MSI et MESI, false sharing, cohérence séquentielle, modèles de cohérence relâchée, métriques et optimisations.
Aval. Séance 3 : programmation parallèle avancée (synchronisation). Lab associé : Lab 3.
Dans un système multicœur, chaque cœur possède ses propres caches (L1, L2) pour aller plus vite. Mais cela crée un problème fondamental :
Le problème de cohérence de cache
Il survient quand plusieurs caches peuvent contenir des copies d’une même donnée, et qu’une modification dans un cache doit être visible par tous les autres de manière cohérente.
Exemple concret : x = 0 en mémoire.
x : copie dans son cache, x = 0.x : copie dans son cache, x = 0.x = 5 dans son cache.x : \(0\) ou \(5\) ?Les défis de la cohérence
Quand une donnée change :
Les protocoles de cohérence sont les règles qui régissent ces décisions ; MSI et MESI invalident les autres copies.
Lecture. Le contrôleur du cache, s’il lui faut la ligne ou l’exclusivité, place une transaction sur le bus, que tous les caches observent. Les latences du cours sont arrondies : une vraie lecture en mémoire coûte plutôt \(180\) à \(300\) cycles (Séance 1).
Write-through et write-back
Write-through : chaque écriture est propagée sans attendre à la mémoire principale. Mémoire toujours à jour, mais beaucoup de trafic sur le bus.
Write-back : les écritures sont faites dans le cache seulement ; la mémoire est mise à jour à l’éviction de la ligne. Moins de trafic, mais un contrôleur plus élaboré (une marque « modifiée » par ligne) et une mémoire pas toujours à jour.
Write-allocate et write-no-allocate
Write-allocate : sur un défaut en écriture, la ligne est d’abord chargée dans le cache, puis modifiée. Exploite la localité temporelle ; coûte un chargement. Va typiquement avec write-back.
Write-no-allocate : sur un défaut en écriture, l’écriture va directement en mémoire, sans charger la ligne. Moins de pollution du cache, pas d’exploitation de la localité. Va typiquement avec write-through.
Énoncé
Un programme effectue la séquence suivante sur une variable non présente en cache :
Comparer le nombre de transferts mémoire avec :
Correction
(a) Write-through + write-no-allocate
(b) Write-back + write-allocate
Conclusion : write-back + write-allocate est beaucoup plus efficace pour les accès répétés.
Lecture. Write \(x=10\), ligne absente. À gauche, écriture directe en mémoire (\(1\) transfert), le cache reste vide. À droite, la ligne est chargée (\(1\) transfert), puis écrite dans le cache seulement (\(\cdot\cdot\) : valeur inconnue).
Lecture. Write \(x=20\). À gauche, encore un défaut : nouvelle écriture directe en mémoire. À droite, succès : écriture dans le cache, aucun transfert.
Lecture. Read \(x\). À gauche, défaut : la ligne est lue en mémoire et mise en cache (\(1\) transfert). À droite, succès : lecture dans le cache.
Lecture. Write \(x=30\). À gauche, succès, mais write-through écrit aussi en mémoire (total \(4\)). À droite, succès, écriture dans le cache seulement : la mémoire n’a toujours pas la valeur.
Lecture. Éviction de la ligne. À gauche, ligne propre : rien à écrire, total \(4\). À droite, la ligne modifiée part en mémoire (write-back) : total \(2\) pour quatre accès.
MSI est le protocole d’invalidation de base : MESI et les protocoles suivants l’enrichissent.
Le protocole MSI
Trois états possibles pour chaque ligne de cache :
Intuition
Pensez aux états MSI comme à des permissions sur un fichier :
Espionnage du bus {bus snooping}
Chaque cache surveille toutes les transactions sur le bus et réagit selon son état actuel et le type de transaction observée.
Notation des transitions : A / B
A, avant la barre : l’événement observé (PrRd, PrWr, BusRd, BusRdX).
B, après la barre : l’action entreprise (BusRd, BusRdX, BusWB, ou – pour aucune action).
Les trois règles de MSI
Lecture. Une ligne en M est la seule copie à jour : si un autre cache la demande, son détenteur l’écrit en mémoire (BusWB) avant de la céder.
Lecture. Notation A / B : événement observé A, action déclenchée B (– pour aucune). Au-dessus, les demandes du processeur : lire depuis I émet un BusRd, écrire depuis I ou S un BusRdX. Au-dessous, les réactions au bus, depuis M toujours après un BusWB.
S vers M
En S, la copie locale est déjà valide : le BusRdX sert à invalider les autres copies. Certains processeurs utilisent ici une transaction sans données (BusUpgr) ; le cours garde BusRdX, avec la même latence de \(100\) cycles.
Lecture. P1 lit \(x\) : C1 est en I, P1 émet un BusRd ; aucune autre copie valide, la mémoire fournit. C1 passe de I à S.
Lecture. P2 lit \(x\) : BusRd, la mémoire fournit encore ; C1 a une copie valide, il examine le bus (cadre orange) et reste en S. Deux copies en lecture seule sont permises.
Lecture. P1 écrit \(x=5\) : il faut être en M. P1 émet un BusRdX ; C2 s’invalide (S\(\to\)I), C1 passe de S à M. C1 avait déjà la donnée : le BusRdX sert à invalider. Question (a) de la fiche : l’invalidation a lieu ici.
Lecture. P3 lit \(x\) : BusRd. C1, en M, intercepte la requête, écrit la ligne en mémoire (BusWB, flèche violette) et la fournit ; C1 passe de M à S, C3 de I à S. Question (b) : la donnée vient de C1, pas de la mémoire.
Lecture. P2 écrit \(x=10\) : C2 est en I, P2 émet un BusRdX ; la mémoire, à jour depuis le BusWB, fournit la ligne. C1 et C3 s’invalident, C2 passe en M.
Énoncé
Trois processeurs, protocole MSI, caches initialement invalides. Tracer les états et les transactions pour :
Correction
| Action | Bus | Fournie par | C1 | C2 | C3 |
|---|---|---|---|---|---|
| Initial | – | – | I | I | I |
| P1 : PrRd x | BusRd | Mémoire | I\(\to\)S | I | I |
| P3 : PrRd x | BusRd | Mémoire | S | I | I\(\to\)S |
| P3 : PrWr x=20 | BusRdX | – | S\(\to\)I | I | S\(\to\)M |
| P2 : PrRd x | BusRd | C3 (BusWB) | I | I\(\to\)S | M\(\to\)S |
| P1 : PrRd x | BusRd | Mémoire | I\(\to\)S | S | S |
Pour améliorer les performances de MSI, MESI ajoute un quatrième état.
Le protocole MESI
E (Exclusive) : la ligne est présente uniquement dans ce cache et identique à la mémoire (propre). Le cache peut la modifier sans transaction sur le bus.
Les quatre états : M exclusive et modifiée ({dirty}) ; E exclusive et propre ({clean}) ; S partagée et propre ; I invalide.
Le choix entre E et S se fait à la lecture : les autres caches signalent sur le bus s’ils ont la ligne (signal de partage).
Analogie
L’état Exclusive, c’est emprunter un livre à la bibliothèque :
L’avantage : passer d’Exclusive à Modified ne demande aucune communication.
Lecture. Lire depuis I donne E si personne n’a la ligne, S sinon ; E\(\to\)M (en vert) se fait sans transaction ; une ligne en E, propre, quitte E sans BusWB.
Le gain principal
La transition E\(\to\)M est LE gain de MESI : une écriture sur une donnée exclusive ne génère AUCUNE transaction bus, donc une latence minimale (\(1\) cycle).
Énoncé
Système MESI : succès \(1\) cycle ; défaut \(100\) ; BusWB \(100\) ; BusRdX \(100\). Latence totale de :
Correction
| Action | P0 | P1 | Transaction | Latence |
|---|---|---|---|---|
| Initial | I | I | – | – |
| P0 : Read x | I\(\to\)E | I | BusRd, sans partage | \(100\) |
| P0 : Write x=10 | E\(\to\)M | I | aucune (locale) | \(1\) |
| P1 : Read x | M\(\to\)S | I\(\to\)S | BusRd + BusWB de P0 | \(200\) |
| P1 : Write x=20 | S\(\to\)I | S\(\to\)M | BusRdX | \(100\) |
| Latence totale | \(\mathbf{401}\) cycles |
Comparaison avec MSI : l’étape 2 aurait demandé un BusRdX (\(100\) cycles au lieu de \(1\)) ; MSI : \(100+100+200+100=500\) cycles. Gain MESI : \(\mathbf{99}\) cycles dans ce scénario.
Lecture. P0 lit \(x\), personne ne l’a : BusRd, aucun cache ne signale de copie, P0 reçoit la ligne en E (\(100\) cycles). En MSI, elle serait en S.
Lecture. P0 écrit \(x=10\) : E\(\to\)M sans transaction, \(1\) cycle. MSI aurait émis un BusRdX (\(100\) cycles) : c’est le gain de MESI.
Lecture. P1 lit \(x\) : BusRd ; P0, en M, fait un BusWB puis passe en S ; P1 arrive en S. \(100+100=200\) cycles, dans les deux protocoles.
Lecture. P1 écrit \(x=20\) : BusRdX pour invalider la copie de P0 (\(100\) cycles). Total : \(401\) cycles en MESI contre \(500\) en MSI, soit \(99\) cycles de gagnés, tous à l’étape 2.
False sharing
Le false sharing (faux partage) se produit quand deux processeurs modifient des variables différentes qui se trouvent dans la même ligne de cache (typiquement \(64\) octets). Cela provoque des invalidations mutuelles constantes.
Lecture. c1, c2 : counter1, counter2 ; une case \(=4\) octets. Relevé avec gcc : counter2 au décalage \(4\), puis \(4+60=64\) : toujours deux lignes.
Lecture. Le fil 1 (sur P1) incrémente counter1 : c’est une écriture, la ligne doit être en M ; BusRdX, la ligne entière (les deux compteurs) entre dans C1 en M.
Lecture. Le fil 2 (sur P2) incrémente counter2, une autre variable, mais dans la même ligne : BusRdX, C1 fait un BusWB et s’invalide, la ligne passe dans C2 en M. Première invalidation.
Lecture. Le fil 1 incrémente encore : la ligne est repartie, C1 est en I. Nouveau BusRdX, C2 fait un BusWB et s’invalide. Deuxième invalidation.
Lecture. Et ainsi de suite : chaque incrémentation peut faire voyager toute la ligne d’un cache à l’autre (ping-pong), alors que les fils ne partagent aucune donnée.
Lecture. Avec le rembourrage (padding[60]), counter2 est à \(64\) octets de counter1 : chaque compteur a sa ligne, en M dans son cache. Après le premier BusRdX de chaque fil, plus aucune transaction.
Énoncé
\(4\) fils incrémentent chacun leur compteur un million de fois :
Ligne de \(64\) octets, int de \(4\) octets.
Correction
int value; char pad[60];) ; champ aligné (alignas(64) int value; dans une structure) ; variable locale et une seule écriture finale.Piège : alignas(64) int counters[4]; n’aligne que le début du tableau. Avec gcc -O2, la boucle devient une seule addition : l’effet se mesure à -O0 ou avec volatile.
Lecture. Devant le tableau, alignas(64) aligne seulement son début. Sur le champ d’une structure, il fixe taille et alignement de chaque élément à \(64\) : un compteur par ligne. Le rembourrage donne le même écart, sans aligner le tableau.
Cohérence séquentielle
Elle garantit que :
Tout se passe comme si une seule mémoire servait les processeurs un accès à la fois, en piochant chaque fois dans l’un des programmes, sans jamais réordonner un même programme.
Lecture. Une exécution est donc un entrelacement des programmes : pour savoir si une sortie est possible, il faut trouver un ordre global qui la produise.
Énoncé
A et B valent \(0\) au départ.
| Thread 1 | Thread 2 |
|---|---|
(1) A = 1 |
(3) B = 1 |
(2) print(B) |
(4) print(A) |
Correction
Exécutions possibles
Sortie impossible : les deux threads affichent \(0\).
Explication : si Thread 1 affiche \(0\), (2) précède (3) ; si Thread 2 affiche \(0\), (4) précède (1). Avec l’ordre des programmes, cela crée un cycle : (2)\(\to\)(3)\(\to\)(4)\(\to\)(1)\(\to\)(2), impossible en cohérence séquentielle.
Lecture. Deux fils de deux opérations donnent \(\binom{4}{2}=6\) ordres globaux, et trois sorties : \((0,1)\), \((1,0)\), \((1,1)\). La sortie \((0,0)\) n’apparaît dans aucun : la cohérence séquentielle l’interdit.
La cohérence séquentielle est intuitive mais limite les optimisations. Les modèles relâchés permettent certains réordonnancements pour améliorer les performances.
Modèles de cohérence relâchée
Ils assouplissent les contraintes d’ordonnancement. Le plus répandu sur les PC et les serveurs est TSO ({Total Store Order}), celui des processeurs x86 ; ARM et RISC-V autorisent davantage de réordonnancements.
Le modèle TSO (x86)
Il permet uniquement le réordonnancement W\(\to\)R :
Lecture. Le fil 1 écrit \(A=1\) : sous TSO, l’écriture entre dans le tampon d’écriture de P1 (cadre rouge) ; la mémoire garde \(A=0\) et le fil continue sans attendre.
Lecture. Le fil 2 écrit \(B=1\) : même chose, dans le tampon de P2. Aucun des deux cœurs ne voit l’écriture de l’autre.
Lecture. Le fil 1 lit \(B\) : la lecture dépasse l’écriture qui attend dans son tampon (réordonnancement W\(\to\)R) ; elle va en mémoire, où \(B\) vaut encore \(0\) : \(r_1=0\).
Lecture. Le fil 2 lit \(A\) : même dépassement, \(r_2=0\). Les deux lectures ont vu \(0\).
Lecture. Les tampons se vident dans l’ordre : \(A=1\) et \(B=1\) en mémoire. Résultat \(r_1=r_2=0\), impossible en cohérence séquentielle, possible sous TSO. Une barrière StoreLoad dans chaque fil l’interdit.
Barrières mémoire
Elles forcent l’ordre des opérations :
Sous TSO, seule la barrière StoreLoad est utile : les trois autres ordres sont déjà garantis.
| Thread 1 | Thread 2 |
|---|---|
A = 1; |
B = 1; |
[barrière StoreLoad]{.c-2073ba} |
[barrière StoreLoad]{.c-2073ba} |
r1 = B; |
r2 = A; |
Lecture. Sur x86, l’instruction mfence joue ce rôle : chaque fil vide son tampon d’écriture avant de lire. La sortie \(r_1=r_2=0\) devient impossible ; il reste \((0,1)\), \((1,0)\) et \((1,1)\).
Métriques clés pour la cohérence
| Transition | Transaction | Cycles | MSI | MESI |
|---|---|---|---|---|
| Read miss (excl.) | BusRd | \(100\) | I\(\to\)S | I\(\to\)E |
| Write après read excl. | BusRdX / aucune | \(100\) / \(1\) | S\(\to\)M | E\(\to\)M |
| Write miss | BusRdX | \(100\) | I\(\to\)M | I\(\to\)M |
| Invalidation autre | BusRdX | – | S/M\(\to\)I | S/M\(\to\)I |
| Write-back | BusWB | \(100\) | M\(\to\)S/I | M\(\to\)S/I |
Lecture. Une seule ligne sépare les deux protocoles : écrire une donnée qu’on est seul à avoir lue coûte \(100\) cycles en MSI, \(1\) en MESI.
Techniques d’optimisation
alignas(64).| Caractéristique | MSI | MESI | Gain MESI |
|---|---|---|---|
| Nombre d’états | \(3\) | \(4\) | – |
| État Exclusive | non | oui | E\(\to\)M local |
| Write sur donnée excl. | BusRdX | aucun bus | \(99\) cycles (\(100\to1\)) |
| Signal de partage | non | oui | choix de E ou S |
| Trafic bus | plus élevé | réduit | selon le programme |
Lecture. Le gain de trafic de MESI porte sur les données lues puis écrites par un seul cœur ; il disparaît quand les données sont réellement partagées.
À retenir (1/2)
À retenir (2/2)
Vrai ou faux ?
Correction
Lab 3 : la mémoire
Prochaine séance
Séance 3 : programmation parallèle avancée. Synchronisation des fils : sections critiques, verrous, sémaphores, barrières. Lab 4 : synchroniser.
Références : Hennessy et Patterson, Computer Architecture: A Quantitative Approach, chap. 5 ; Papamarcos et Patel, ISCA 1984 (MESI, protocole Illinois) ; Lamport, IEEE Trans. Computers, 1979 (cohérence séquentielle).
Architecture et Programmation Parallèles — Séance 2 Dr. El Hadji Bassirou TOURÉ • DMI/FST/UCAD
Lecture. P1 lit \(x\) : défaut, la ligne vient de la mémoire (flèche verte) et une copie \(x=0\) entre dans C1 (cadre rouge).
Lecture. P2 lit \(x\) : une seconde copie \(x=0\) entre dans C2. Deux caches détiennent maintenant la même donnée.
Lecture. P1 écrit \(x=5\) dans sa copie (cache write-back) : la mémoire garde \(0\) et la copie de C2 est périmée. Personne ne prévient C2.
Lecture. P2 relit \(x\) : la ligne est dans C2, c’est un succès, et P2 lit \(0\) alors que la dernière valeur écrite est \(5\). Sans mécanisme de cohérence, P2 lirait toujours \(0\).