Hirdetés

Új hozzászólás Aktív témák

  • P.H.

    senior tag

    válasz LordX #141 üzenetére

    Azért, hogy ne legyen ilyen töredezett a társalgás, legyen egyben az egész.

    8.1.1 bekezdés

    Az első a bekezdés igazából nem közöl érdemleges információt, csak a sorok között. És kizárólag a tárolási műveletekről szól (MOV mem,reg FST(P) mem FIST(P) mem MOVQ mem,mmreg). A 486-os esetén minden kiírt byte bitjei egyszerre jelennek meg a kiírva, továbbá minden 16 bites adaté, ha az páros memóriacímre lett kiírva és minden 32 bites adaté, ha az 4-gyel osztható címre; Pentium 1-nél továbbá minden 64 bites (=MMX, itt jelent meg) adaté, ha az 8-cal osztható címre.

    Ehhez képest a P6 family (Pentium Pro, Pentium 2 és Pentium III) esetén ez lecserélődik arra, hogy bármely 2-, 4- és 8-byte méretű adat bitjei egyszerre jelennek meg kiírva, ha az befér a - az akkor még 32 byte-os - befoglaló cache line-ba, azon belül bárhova (pl. 0x07 címre) írva. Ez a csere a Pentium Pro-val jelent meg, amelyet az Intel az első valóban több CPU-s rendszerekre szánt (aztán a Pentium II sorozatban megjelent a Xeon márkanév), ennyit arról, hogy olyasmivel kell kompatibilisnek lenni, ami még messze nem volt több CPU-s rendszer; technikailag több egymagos CPU és egy többmagos CPU kezelése nem sokban különbözik.
    - azaz ha egy másik mag/CPU ráolvas a változóra, vagy a régi, vagy az új értéket fogja látni, fals értéket nem; és ehhez nem kell külön szinkronizálás
    - magon belül is fontos ez, mert ez teszi lehetővé a store-to-load-forwarding-ot is a magon belül (azaz ha ugyanaz a mag/CPU olvassa vissza a változót), mert a Pentium Pro az Intel első OoO felépítése.

    Ez a szabály azt mondja ki, hogy ha pl. 32 bites single precision lebegőpontos változóban pl. 1.1x2^2 érték van jelenleg és kiírunk oda mondjuk 1.5x2^4 értéket, akkor nem lesz olyan órajel/pillanat, amelyben a változó köztes adatot tartalmazna olyan értelemben, hogy pár bit - monjuk a kitevő byte-ja - már megérkezett, a többi még nem, pl. 1.1x2^4 érték lenne a változóban.
    Persze ettől eltérően ki lehetne írni nem egyben is, csak az még lassabb is lenne. Ez amúgy nem jelentős kitétel, mert a fordítóprogramok már több évtizede gondoskodnak arról, hogy a változók jól legyenek align-olva akár stack-ben vannak, akár globális változók vagy foglalt (malloc) memóriában vannak: pl. egy byte méretű változónak pl. 4 byte-ot foglalnak le, ha utána közvetlenül uint van, vagy az említett extended precision 10 byte-os számnak 16 byte-ot, a rendszer által adott lefoglalt memória pedig NT 3.1 óta 4-byte aligned, Windows Vista óta pedig 32-es címre van igazítva.

    Továbbá megfigyelhető, hogy a szabály nem vonatkozik (a 10 byte-os extended precision mellett) a 128 bites (=SSE) adatokra, pedig az a Pentium III-ban jelent meg. Ennek két praktikus oka van, amit nem itt kell keresni:
    - egyrészt ekkoriban (a Pentium III-ban, a Pentium M-ben és a Pentium 4-ben is) 64 bites végrehajtókon hajtották végre a 128 bites utasításokat, így a tárolásokat is, így garantálni és garantáltatni sem lehetett, hogy a 128 bites adat teljes egésze egyszerre jelenik meg a külvilág számára
    - másrészt az SSE utasítások, legyenek azok load, load-op vagy store jellegűek (load-op-store nincs SSE-ben, sem MMX-ben, sem AVX-ben, legfejlebb non-temporal, de az más téma), per definition megkövetelik, hogy a megadott memóriacím 16-byte aligned legyen. Ellenkező esetben le se futnak, #invalid address (a.k.a. "runtime error 216" vagy "access violation error..." hibát dobnak; erről a programozónak kell gondoskodni (ez nem nehéz, mert a virtuális és a lefordított fizikai memóriacím 4 KB-os lapnál a 12. bittól felfele különbözik csak, tehát programíráskor látható az igazítás).

    Az SSE utasítások a Core 2-ben kaptak natív 128 bites végrehajtókat és hamarosan (vagy a Westmere-ben, vagy a Nehalem-ben; AMD-nél a K10-ben) eltörölték a 16 byte-aligned memóriacím követelményt, a 128 bites adatokra vonatkozó kitétel mégse jelent meg a szabályban ("elfogyott"). Ahogy a 256 bites AVX sem. Pedig kaphatott volna. Hogy bonyolultabb legyen a kép. Vagy elavultabb?

    A tárolás időigénye a fentiek miatt attól függ, hogy hol van az a cache line, amire tárolunk, mivel a tárolás maga csakis az L1-be történhet (hacsak nem non-temporal, akkor meg nem függ attól, hogy van), ezért az L1-be kell hozni az adott cache line-t. Ha legrosszabb esetben RAM-ban van és nincs meg egyetlen közelebbi cache szinten se, akkor sok-sok órajelbe kerülhet. Szerencsére Intel-nél Core2 óta, AMD-nél K10 óta out-of-order a tárolás is (gondolom a store queue, teljes nevén Store Queue for L1-miss Stores szólíthatjuk, mint CPU építőelem ismerős) és mérete szokás szerint nő generációról generációra, közben az utasításvégrehajtás kedvére folytatóthat. (persze ha betelik a retirement window, azaz a ROB közben, akkor van baj, dehát azért optimalizálunk, hogy az L1-hit 95% felett legyen, bár a nagy részben megoldják a hardware prefetch-erek.

    A második bekezdés már igenis hordoz lényegi információt. A fordítók készítői számára.

    Mégpedig azt, hogy a tárolás nem fér fele 1 cache line-ba, akkor a fenti nem igaz, ez az eset két db tárolási műveletként van implementálva, fele ide, fele oda, a rendszer semmilyen törekvést nem tesz arra, hogy kívülről egyszerre lehessen látni a teljes adatot. Emiatt a programozónak kell gondoskodnia arról, hogy ha az adott adatra ráolvas másik mag/CPU, egy - mostmár 64 byte-os - cache-egységen belül legyen, de mint említettem, a fordítóprogramok ezt elvégzik. Persze szorgos munkával meg lehet ezt kerülni, szorosan típusos nyelvekben mondjuk nem, C-ben hosszadalmas és jól látható pointer+typecasting-gel, vagy ASM-ben pl. megnöveled a stack pointer-t eggyel, de ugye ez mind proaktív dolog.

    Amúgy a Bulldozer esetében az AMD valószínűleg pont ezt tolta túl és lett 20+ órajel egy 256 bites store, mert a 256 bites AVX tárolásokra egy modulban egyetlen 128 bites FSTORE tároló végrehajtási egysége van a megosztott FPU-ban, viszont a write-through L1D cache miatt az L2-be kell átvinni az adatot és a Write Coleascing Cache-ben megpróbálták egyesíteni a két felet. Aztán a Kaveri-vel vissza is léptek ettől, mert a szabály ezt nem írja elő; más kérdés, hogy az Intel az AVX-et tudó processzorait eleve 256 bites tárolóporttal szerelte fel.

    8.1.2 bekezdés

    Tehát a fentiek a store illetve a másik mag/CPU által kiadott load vagy load-op kapcsolatát írják le. Van még egy eset: van egy változó, legyen VAR, ami kezdetben 0; a feladatot elosztottuk két szálra, amelyek növelik ennek értékét (pl. számolnak valamit). Ha mindkét szál lefuttatja az ADD [VAR],1 utasítást mint load-op-store műveletet, akkor nem garantált, hogy nem fog előfordulni, hogy - mivel az újabb CPU belül több mikroműveletre bontotta az utasítást, régi in-order CPU pedig a futószalag elején beolvas, közepén módosít, a végén ér), hogy beolvasva az érték 0, növelve 1, kiírva 1 lesz: azaz 2 helyett 1 lesz az eredmény.

    Ezen az utasítások elé lehet kitenni a LOCK prefix-et, azaz LOCK ADD [VAR],1 lesz írva mindkét szál kódjába.
    Csak ezen load-op-store utasítások elé, store vagy load-op elé nem lehet, különben jön az #UD, és csak a felsoroltak elé, tehát nincs shift vagy ROR forgatás.

    A rendszer garantálja, hogy ha azonos órajelben indul el mindkét utasítás, akkor is lesz egy sorrend, amelyben az egyik beolvassa-megnöveli-kiírja (az első snoopable szintre, L1 vagy Bulldozernél és Pentium 4-nél L2) az 1 értéket, majd ezt kapja meg a másik szál utasítása, növeli és kiírja a 2-t.
    Amíg le nem fut az ilyen LOCK prefix-szel megjelölt utasítás, addig az adott mag/CPU nem dekódol több utasítást; ez az első magnál lehet gond, mert lehet, hogy neki kell (ha kell) beolvasni RAM-ból az adatot (load-op-store, tehát kell a forrásadat is), eredményét az L1-ből vagy L2-ből közvetlenül megkapja a másik mag/CPU. De hogy ne kelljen RAM-olvasás, van L3; vagy ha Broadwell-t veszel, akkor méretes L4 is.

    Az egyetlen utasítás, ami elé nem kell kiírni a LOCK-ot, az XCHG mem,reg, mert - ahogy a fejezet is említi -, a CPU automatikusan odagondolja.

    Ezek által lehet kezelni a szinkronizációs változókat.

    Meglehetősen nehéz kimérni egy memóriaoperandusú utasítás végrehajtását (meg amúgy életszerűtlen), ha az L1-hozzáférés időigényét nem számolod hozzá... De ha sűrűn módosítod a változót, megtalálod az L2-ban vagy az L3-ban (adj a kiírt órajelhez ~15 vagy ~25 órajelet Intel-nél); ha meg nem, akkor 600 órajel se tétel.

    8.2.2 bekezdés

    Ennek felelnek meg teljes egészében a leírtak. Hogy konkretizáljam:

    - az LFENCE, MFENCE, SFENCE, MOVNTxx non-temporal utasítások, külön faj (a lényegük, hogy semmilyen konzisztenciát nem biztosítanak, a tárolt adat semelyik cache-ben nem jelenik meg, egy átmeneti pufferből azonnal a RAM-ba kerül, jelentősen gyorsabb így nagy mennyiségű adat kezelése, mintha cache-be tennék az eredményt, és a cache tartalma is megőrizhető. Az MOVNT-csoport ír, a FENCE-csoport puffert ürít).
    - a CLFLUSH a Cache Line Flush to RAM, pl. ha laptáblát módosítasz (pár bitenként egyszerre, mint hozzáférési jogok, címrész) és végül globálisan láthatóvá akarod tenni.
    - az I/O műveletek (IN és OUT egy megadott portra) ugyanúgy viselkednek, mint a LOCKED prefix-es utasítások, addig nem dekódol a CPU, amíg ez véget nem ért.

    "Reads may be reordered with older writes to different locations but not with older writes to the same location.[I/]"
    Ha ez nem így van bármely CPU-t, a teljes integritást veszélyezteti.

    "Writes are not reordered with older reads."
    Szintúgy.

    "The only enhancements in the Pentium 4, Intel Xeon, and P6 family processors are "
    "• Added support for speculative reads, while still adhering to the ordering principles above."
    Ez tisztán hardware-es. Nagyrészt kiüti "Reads are not reordered with other reads." kitételt, látszólagos megfelelőséget biztosít neki; továbbá az előző pontot is spekulatívvá teszi, nem kell tudni előre, hogy a korábbi tárolás milyen címre történik; ha kiderül később, hogy azonosra, újraindítja a pipeline-t, mint egy sima branch misprediction-nál. Memory Disambiguation a neve.

    "• Store-buffer forwarding, when a read passes a write to the same memory location."
    Ezt említettem fejlebb, a load vagy load-op utasítás a store queue-ből veszi a kiírt adatot, nem az L1-ből.

    A képen látható bal "Writes are in order with respect to individual processes." és a jobb "Writes from all processors are not guaranteed to occur in a particular order." pedig magáért beszél.

    [ Szerkesztve ]

    Arguing on the Internet is like running in the Special Olympics. Even if you win, you are still ... ˙˙˙ Real Eyes Realize Real Lies ˙˙˙

Új hozzászólás Aktív témák

Hirdetés