- A gyorsítótár koherencia biztosítja, hogy ugyanazon adatok összes másolata a különböző gyorsítótárakban és a RAM-ban konzisztens maradjon a többmagos rendszereken.
- A megosztott utolsó szinttel rendelkező gyorsítótár-hierarchia leegyszerűsíti a konzisztencia-szabályozást és csökkenti a főmemóriához való közvetlen hozzáféréseket.
- A koherenciaprotokollok másolat-érvénytelenítési vagy frissítési stratégiákat használnak, amelyeket állapotok és vezérlőbitek támogatnak gyorsítótár-soronként.
- A fordító és az operációs rendszer kiegészítheti a hardver konzisztenciáját utasítások beillesztésével és a memória kritikus időszakokra történő konfigurálásával.

Ha egy modern többmagos processzor diagramját nézzük, mindig ugyanaz a minta jelenik meg: több mag, mindegyik saját közeli gyorsítótárral, és egy megosztott utolsó szintű gyorsítótár, amely közös pontként szolgál a RAM elérése előtt. Ez az elrendezés nem véletlen vagy a tervezők szeszélye, hanem közvetlen válasz a párhuzamos rendszerek egy kritikus problémájára: a gyorsítótár koherenciájára.
Egy robusztus konzisztencia-mechanizmus nélkül minden mag a memóriában tárolt adatok egy eltérő és elavult verziójával dolgozhat , ami egy valós programban finom hibákhoz, kiszámíthatatlan meghibásodásokhoz és akár rendszerösszeomlásokhoz is vezethet. Ezért annak megértése, hogyan tartható fenn ez a konzisztencia – mind hardveres, mind szoftveres szinten –, kulcsfontosságú a modern többmagos CPU-k teljesítményének és stabilitásának megértéséhez.
Mi a gyorsítótár koherencia: a terminál metaforája
Képzeljen el több embert, akik különböző terminálok előtt ülnek, és mindannyian ugyanazt a dokumentumot szerkesztik, amely egy központi szerveren tárolódik . Minden képernyőn a fájl egy másolata látható, és az egyik személy által végrehajtott módosítások várhatóan azonnal megjelennek mindenki más képernyőjén is.
Ahhoz, hogy ez működjön, szükség van egy szinkronizációs mechanizmusra, amely a dokumentummódosításokat minden terminálra továbbítja, hogy mindenki mindig ugyanazt a verziót lássa. Amíg ez a rendszer működik, minden rendben van: aki módosítja a szöveget, tudja, hogy mindenki más szinte azonnal látni fogja az új verziót.
Most képzeljük el, hogy a szinkronizációs rendszer hirtelen meghibásodik. Mindenki folytatja a szerkesztést, meggyőződve arról, hogy a megosztott dokumentumon dolgozik, de a valóságban minden terminálnak a saját, leválasztott helyi másolata marad . Ettől a pillanattól kezdve az egyik személy által végrehajtott módosítások nem érik el a többieket, és a dokumentum kontrollálhatatlanul elkezd eltérni.
A számítástechnika birodalmában pontosan ez történne, ha a CPU-nak nem lenne megbízható konzisztencia-protokollja: az egyik mag módosítja a memóriában lévő adatokat, de a többi mag továbbra is egy régebbi verziót olvas a saját gyorsítótárából . Ez termékeny talajt teremt a súlyos logikai hibáknak, a sérült adatoknak és a hibajavítási viselkedésnek.
A gyorsítótár koherencia tehát azon mechanizmusok összessége, amelyek biztosítják, hogy egy többmagos rendszerben ugyanazon adatok összes, a különböző gyorsítótárakba és RAM-ba elosztott másolata konzisztens állapotot tartson fenn . Még ha több másolat is létezik, a rendszernek úgy kell viselkednie, „mintha” csak egy létezne.

Gyorsítótárak és memóriahierarchia egy többmagos CPU-ban
A CPU gyorsítótárai kicsi, nagyon gyors memóriák, amelyek a gyakran használt RAM-darabok másolatait tárolják . Amikor a processzor kódot futtat, a (viszonylag lassú) RAM folyamatos elérése helyett megpróbál olvasni a gyorsítótárból és írni oda, drasztikusan csökkentve a késleltetést.
A trükk természetesen az, hogy a gyorsítótárak nem az adatok „hivatalos verzióját” tárolják, hanem csak egy ideiglenes másolatot . A terminál metaforáját követve a RAM a szerveren lévő dokumentum lenne, míg a gyorsítótárak a helyi képernyők, amelyek a fájl bizonyos részeinek másolatait jelenítik meg.
Egy többmagos CPU-ban a kialakítás bonyolultabbá válik, mivel minden magnak jellemzően saját 1. szintű (L1) és akár 2. szintű (L2) gyorsítótára van . Ezek fölé (például) egy megosztott 3. szintű gyorsítótár kerül hozzáadásra, amely a magok és a RAM-hoz való hozzáférést biztosító memóriavezérlő között található.
Ezt a megosztott gyorsítótárat azért vezették be, mert ha az összes mag közvetlenül és intenzíven hozzáférne a RAM-hoz, az hozzáférési konfliktusokat, versenyhelyzetet a memóriabuszon és jelentős teljesítménycsökkenést okozna . Az utolsó szintű gyorsítótár egy közös „pufferként” működik, amely csökkenti a RAM-hozzáféréseket és központosítja az adatforgalom nagy részét.
Továbbá számos architektúra inkluzív módon szervezi a gyorsítótárakat: a processzorhoz közeli szinteken tárolt sorok a hierarchia magasabb szintjein is megtalálhatók . Vagyis egy sor, amely az L1-ben jelenik meg, az L2-ben is megtalálható, és így az L3-ban is. Ennek nagyon hasznos következménye van a konzisztencia szempontjából: a legalacsonyabb szintű gyorsítótár helyes frissítése elegendő a többi szint állapotának vezérléséhez anélkül, hogy folyamatosan hozzáférnénk a RAM-hoz.
Miért kulcsfontosságú a legutolsó szintű megosztott gyorsítótár a konzisztencia szempontjából?
E globális, utolsó szintű gyorsítótár nélkül minden egyes magnak közvetlenül a főmemóriával kellene ellenőriznie a konzisztenciát . Minden alkalommal, amikor egy privát gyorsítótárban lévő memóriasor módosul, ellenőrizni kellene, hogy más magok megtartják-e ugyanazon sor másolatát, és ha igen, akkor mindenhol frissíteni vagy érvényteleníteni kellene azt.
Egy sokmagos rendszerben ez az ellenőrzési munkaterhelés hatalmas számú RAM-mal végzett tranzakciót eredményezne , ami semmissé tenné a gyors gyorsítótárak előnyeinek nagy részét. A magok és a memória közötti megosztott gyorsítótár elhelyezésével a CPU egyetlen köztes helyre koncentrálhatja a koherencia-vezérlést.
Sok implementációban a magasabb szinteken (a processzortól távolabb) lévő gyorsítótárak a maghoz közelebbi szinteken található sorok másolatait tartalmazzák . Ezzel a szervezéssel a koherencia protokollnak csak azt kell biztosítania, hogy az utolsó szint szinkronizálva legyen a főmemóriával, és hogy az egyes magok privát szintjei szinkronizálva legyenek a közvetlenül felette lévő szinttel.
Ez egyfajta orosz fészekbabaként képzelhető el: a harmadik szintű gyorsítótár tartalmazza a második és az első szint tartalmát , a második szint a saját és az első szint tartalmát, az első szint pedig csak a saját sorait ismeri. Így a "nagy baba" (az utolsó szint) vezérlésével a rendszer hatékonyabban tudja koordinálni a többit.
Az eredmény az, hogy a konzisztencia fenntartása gazdaságosabbá válik a tervezés és a memóriaforgalom szempontjából . Ahelyett, hogy minden magot állandóan a RAM-mal kellene foglalkozni, a protokoll a megosztott gyorsítótáron működik, és onnan kezeli, hogy mely sorokat kell frissíteni vagy érvényteleníteni a privát gyorsítótárakban.
Frissítési módszerek: másolatok érvénytelenítése és frissítése
Kritikus probléma merül fel, amikor két vagy több mag szinte egyszerre kíván hozzáférni ugyanahhoz az adatsorhoz, amely több gyorsítótárban replikálódik . Ebben az összefüggésben a konzisztencia-rendszerek jellemzően két alapvető stratégiát alkalmaznak az írások kezelésekor.
Az első módszer az érvénytelenítésen alapul. Amikor egy kernelnek egy adott gyorsítótár-sorba kell írnia, a protokoll érvényteleníti az adott sor minden olyan másolatát, amely más gyorsítótárakban létezhet . Csak az írni szándékozó kernel tartja a sort olvasási és írási állapotban; a többi kernelnek, ha újra használni akarják az adatokat, újra kell tölteniük a sort a magasabb szintről (vagy a memóriából) a frissített verzióval.
A második stratégia a frissítést foglalja magában. Ebben az esetben, amikor egy kernel módosít egy sort, a rendszer megpróbálja automatikusan továbbítani az új tartalmat a többi gyorsítótárban lévő meglévő másolatokba . Így az összes gyorsítótár, amely az adott sort tárolta, megkapja a frissített verziót anélkül, hogy később érvényteleníteni és újra kellene tölteni azt.
Mindegyik megközelítésnek megvannak a maga előnyei és hátrányai. Az érvénytelenítés általában hatékonyabb, ha gyakoriak az írások, mivel így elkerülhető a memóriarendszer telítettsége olyan frissítésekkel, amelyekre más magoknak esetleg nincs azonnal szükségük. Ezzel szemben a frissítés előnyös lehet, ha sok mag gyakran olvassa ugyanazokat az adatokat, amelyeket viszonylag ritkán módosítanak , mivel csökkenti a késleltetést azáltal, hogy nem kell minden érvénytelenítés után újraindítani a sort.
Mindkét esetben mindkét módszer további állapotokat és vezérlőbiteket használ a gyorsítótár soraiban. Minden sor jellemzően tartalmaz információt arról, hogy a tartalma megegyezik-e a RAM-ban lévővel , valamint hogy megosztott, módosított, kizárólagos, foglalt stb.-e, az adott protokolltól (MESI, MOESI, MSI stb.) függően. Ez lehetővé teszi a hardver számára, hogy gyors döntéseket hozzon arról, hogy mit tegyen, amikor olvasási vagy írási művelet történik egy már replikált soron.
A gyorsítótárak és a memória közötti konzisztencia ellenőrzése
A CPU vagy GPU összes gyorsítótár-szintje és a fő memória közötti konzisztencia közvetlen ellenőrzése hatalmas feladat lenne, mind a tervezési komplexitás, mind a teljesítményköltségek szempontjából. Ezért a modern rendszerek hierarchikusan szervezik ezt az ellenőrzést.
A processzorhoz legközelebb eső gyorsítótárak (L1, L2) általában nem közvetlenül a RAM-hoz, hanem a gyorsítótár következő szintjéhez csatlakoznak. Ez azt jelenti, hogy a konzisztenciát nem az egyes szinteken lévő főmemóriához, hanem a közvetlenül felette lévő szinthez viszonyítva ellenőrizzük . Ez csökkenti a RAM-hozzáférések számát és leegyszerűsíti az alacsonyabb szinteken szükséges logikát.
Végső soron a gyorsítótár tartalmának és a RAM tartalmának összehasonlítása az utolsó szintű gyorsítótár és a fő memória között történik . Ha ez az utolsó szint helyes és konzisztens állapotot tart fenn, és minden alsóbb szint megőrzi konzisztenciáját a felette lévővel, akkor a teljes hierarchia konzisztens marad anélkül, hogy minden sort ismételten ellenőrizni kellene a RAM-mal szemben.
Amikor egy kernel a gyorsítótár egy sorába ír és megváltoztatja az adatait, az adott sor állapota megjelölésre kerül, jelezve, hogy az már nem egyezik meg pontosan a memóriában tárolt példánnyal . Innentől kezdve a protokoll koordinálja a frissítést: a más gyorsítótárakban található megfelelő másolatokat foglaltként vagy érvénytelenként jelöli meg, és amikor szükséges, az új tartalmat a hozzá tartozó főmemória sorába írja.
Ez a kaszkádszerű szervezés lehetővé teszi, hogy a változások fokozatosan terjedjenek a kernelből, amely frissíti az adatokat, a fő memóriába, szabályozott módon haladva át minden gyorsítótár-szinten. Ily módon a konzisztencia fenntartása nem válik leküzdhetetlen szűk keresztmetszetté a processzor számára.
Hardver koherencia kontra szoftver koherencia
Eddig olyan konzisztencia-mechanizmusokat tárgyaltunk, amelyek főként hardveresen valósulnak meg: protokollok, állapotbitek, megosztott gyorsítótárak stb. Létezik azonban egy másik megközelítés is, amely e komplexitás egy részét a szoftverekre , konkrétan a fordítóra és az operációs rendszerre kívánja áthelyezni .
A szoftveralapú konzisztencia-sémák a kód elemzésével és fordítási idejű döntések meghozatalával próbálják csökkenteni a további, chipre integrált logika szükségességét . Az ötlet az, hogy ha a fordítóprogram kikövetkeztetni tudja, hogy mikor és hogyan férnek hozzá bizonyos megosztott adatokhoz, akkor sok esetben megakadályozhatja az adatok gyorsítótárazását, vagy explicit módon kezelheti azok láthatóságát.
Ennek a megközelítésnek egyértelmű előnye van: a munkaterhelés egy része a futásidejűről a fordítási idejű megoldásra helyeződik át . Ahelyett, hogy a hardver menet közben észlelné és kezelné az összes konfliktust, a fordító megpróbálja előre látni azokat, és olyan kódot generálni, amely elkerüli a veszélyes helyzeteket.
A hátránya, hogy a statikus kód elemzése korlátozott, ezért a fordítóprogramok általában konzervatívak . Ez azt jelenti, hogy a konzisztencia megsértésének elkerülése érdekében gyakran olyan döntéseket hoznak, amelyek csökkentik a gyorsítótárak hatékonyságát. Ha gyanítják, hogy egyes adatok problémásak lehetnek, gyakran megakadályozzák azok gyorsítótárazását, vagy a feltétlenül szükségesnél gyakrabban kényszerítik ki a szinkronizálást.
Ezért, bár ezek a szoftveres sémák elméletileg vonzóak, különösen a hardvertervezés egyszerűsítése szempontjából, a gyakorlatban nem helyettesítik a magába a CPU-ba integrált koherencia-támogatást , hanem bizonyos konkrét esetekben kiegészítik azt.
A fordítóprogram szerepe a gyorsítótár konzisztenciájában
A szoftveralapú konzisztencia-megközelítések egyik kulcsfontosságú eleme a fordítóprogram szerepe. A fordítóprogram mélyreható kódelemzést végezhet, és meghatározhatja, hogy mely megosztott adatstruktúrák lehetnek nem biztonságosak a gyorsítótárazáshoz . Ennek alapján speciális módon megjelöli ezeket az elemeket, vagy módosítja a kódgenerálást.
A legegyszerűbb, és egyben a legkonzervatívabb megközelítés az, hogy megakadályozzuk a megosztott adatváltozók gyorsítótárazását . Vagyis minden egyes hozzáférés ezekhez a változókhoz a főmemóriához vagy egy nem gyorsítótárazható területhez kényszerít ki hozzáférést. Ez garantálja a konzisztenciát, de számos teljesítménybeli lehetőséget elszalaszt, mivel egy megosztott struktúra valójában bizonyos időszakokban privát módon, máskor pedig csak olvasható módon használható.
A valóságban a konzisztenciaprobléma csak azokban az időközönként jelentkezik, amikor legalább egy folyamat írni tud a változóba, egy másik pedig olvasni tudja azt . Ezen kritikus időszakokon kívül a változó kezelhető úgy, mintha egyetlen szál kizárólagos használatára szolgálna, vagy akár egy ideig effektív konstansként is használható, így problémamentesen gyorsítótárazható.
A legfejlettebb fordítási stratégiák megpróbálják azonosítani azokat a „biztonságos” időszakokat, amelyek alatt a megosztott változó nem ütközőnek tekinthető . Ehhez a fordító elemzi a végrehajtási útvonalakat, a lehetséges egyidejű hozzáféréseket és a szinkronizációs mintákat (zárolások, kritikus szakaszok stb.). Ezen elemzés alapján a változó élettartamát fázisokra osztja: némelyik alkalmas gyorsítótárazásra, mások speciális kezelést igényelnek.
Kritikus időszakokban, amikor írással egyidejű hozzáférést észlel, a fordító további utasításokat illeszt be a generált kódba a gyorsítótár konzisztenciájának kikényszerítése érdekében . Ezek az utasítások kikényszeríthetik a gyorsítótár ürítését, a memória újratöltését, a memóriakorlátozásokat, vagy a nem gyorsítótárazhatóként megjelölt régiókhoz való hozzáférést, a programozási modelltől és az alapul szolgáló architektúrától függően.
A fordítóprogram, az operációs rendszer és a hardver közötti kapcsolat
Az a kifejezés, hogy „a fordítóprogram utasításokat illeszt be a generált kódba a gyorsítótár konzisztenciájának kikényszerítése érdekében”, azt a benyomást keltheti, hogy az operációs rendszer ezeket az utasításokat magas szintű tippekként olvassa , és ezek alapján dönti el, hogyan hajtsa végre a programot. A valóságban a mechanizmus némileg eltér.
Amikor a fordító ilyen típusú utasításokat ad hozzá a bináris fájlhoz, a bináris fájlba az architektúra vagy a futási környezet által támogatott specifikus műveleteket illeszt be . Például beszúrhat gyorsítótár-ürítő utasításokat, memóriakorlátokat, speciális utasításokat a régiók nem gyorsítótárazhatóként való megjelölésére, vagy hívásokat az operációs rendszer szolgáltatásaihoz, amelyek memóriaattribútumokat konfigurálnak.
Az operációs rendszer nem értelmezi ezeket az utasításokat a fordítóprogram által írt magas szintű „kommentekként” vagy „tippekként”; egyszerűen csak végrehajtja a gépi kódot, mint bármely más . Azonban ezek közül néhány utasítás úgy van kialakítva, hogy kölcsönhatásba lépjen a memória alrendszerrel és a gyorsítótár-kezeléssel, ezáltal megváltoztatva, hogyan fér hozzá a CPU bizonyos adatokhoz.
Más szóval, a fordítóprogram előzetes elemzést végez, és kódot generál, amely végrehajtásakor a kívánt gyorsítótár-viselkedést hozza létre . Az operációs rendszer memória-attribútumok (gyorsítótárazható vagy nem gyorsítótárazható területek, írási szabályzatok stb.) létrehozásával és szinkronizációs primitívek biztosításával működik együtt, de nem "olvassa" a speciális utasításokat abban az értelemben, hogy szemantikailag értelmezi azokat, ahogy egy fordítóprogram tenné.
Az is előfordulhat, hogy a hardver bizonyos utasítások láttán specifikus koherencia- vagy szinkronizációs mechanizmusokat aktivál . Például a kerítés vagy a záró utasítások garantálják a memória-hozzáférések sorrendjét, és bizonyos láthatósági hatásokat érvényesítenek a gyorsítótár hierarchiájában. Ebben az esetben háromirányú együttműködésről van szó: a fordítóprogram dönti el, hová helyezze ezeket az utasításokat, az operációs rendszer konfigurálja a végrehajtási környezetet, a hardver pedig megvalósítja a tényleges viselkedést a gyorsítótár és a memóriabusz szintjén.
Ezek az elemek együttesen biztosítják, hogy még akkor is, ha ugyanazon adatok több példánya van elosztva különböző gyorsítótárak és a fő memória között, a párhuzamos programok konzisztens memóriamodellel fussanak . A gyorsítótár koherencia, amely korántsem egy egyszerű belső CPU-részlet, központi elemévé válik a többmagos rendszerek megbízható és hatékony működéséhez.
A gyorsítótár-hierarchia, a hardveres koherencia protokollok és a szoftveres támogatási technikák kombinációjának megértése világosabbá teszi, hogy a modern CPU-tervek miért osztoznak ilyen hasonló struktúrán, és miért okozhat bármelyik mechanizmus apró hibája kaotikus viselkedést az olyan egyidejű alkalmazásokban , amelyek teljes mértékben attól függenek, hogy minden mag ugyanazokat az adatokat lássa a megfelelő időben.