Mentegetőzések. Hatékony CPU kód. írásához hasznos architekturális háttér, hogy miért is lassú a kódom. Valasek Gábor

Hasonló dokumentumok
Hatékony CPU kód. írásához hasznos architekturális háttér. Valasek Gábor

Számítógépek felépítése

Mutatók és mutató-aritmetika C-ben március 19.

Programozás alapjai. 10. előadás

Memóriagazdálkodás. Kódgenerálás. Kódoptimalizálás

Készítette: Trosztel Mátyás Konzulens: Hajós Gergely

A verem (stack) A verem egy olyan struktúra, aminek a tetejéről kivehetünk egy (vagy sorban több) elemet. A verem felhasználása

SZÁMÍTÓGÉP ARCHITEKTÚRÁK

találhatók. A memória-szervezési modell mondja meg azt, hogy miként

Párhuzamos és Grid rendszerek

Magas szintű optimalizálás

Hatékony memóriakezelési technikák. Smidla József Operációkutatási Laboratórium január 16.

Az interrupt Benesóczky Zoltán 2004

Dr. Schuster György október 14.

Assembly. Iványi Péter

Pénzügyi algoritmusok

Szoftvertechnológia alapjai Java előadások

Mintavételes szabályozás mikrovezérlő segítségével

Operandus típusok Bevezetés: Az utasítás-feldolgozás menete

SzA19. Az elágazások vizsgálata

Pénzügyi algoritmusok

8. gyakorlat Pointerek, dinamikus memóriakezelés

Programozási nyelvek Java

Operációs rendszerek III.

Ismerkedjünk tovább a számítógéppel. Alaplap és a processzeor

Architektúra, cache. Mirıl lesz szó? Mi a probléma? Teljesítmény. Cache elve. Megoldás. Egy rövid idıintervallum alatt a memóriahivatkozások a teljes

elektronikus adattárolást memóriacím

5-6. ea Created by mrjrm & Pogácsa, frissítette: Félix

Számítógép felépítése

SZÁMÍTÓGÉP ARCHITEKTÚRÁK

1. Template (sablon) 1.1. Függvénysablon Függvénysablon példányosítás Osztálysablon

Függvények. Programozás I. Hatwágner F. Miklós november 16. Széchenyi István Egyetem, Gy r

LabView Academy. 4. óra párhuzamos programozás

SZÁMÍTÓGÉP ARCHITEKTÚRÁK

A Számítógépek felépítése, mőködési módjai

OOP #14 (referencia-elv)

A PROGAMOZÁS ALAPJAI 1. Függvény mint függvény paramétere. Függvény mint függvény paramétere. Függvény mint függvény paramétere

GPU Lab. 4. fejezet. Fordítók felépítése. Grafikus Processzorok Tudományos Célú Programozása. Berényi Dániel Nagy-Egri Máté Ferenc

Fordító részei. Fordító részei. Kód visszafejtés. Izsó Tamás szeptember 29. Izsó Tamás Fordító részei / 1

C++ programok fordítása

Programozas 1. Strukturak, mutatok

Informatika terméktervezőknek

Programozás C nyelven FELÜLNÉZETBŐL elhullatott MORZSÁK. Sapientia EMTE

A 32 bites x86-os architektúra regiszterei

A fordítóprogramok szerkezete. Kódoptimalizálás. A kódoptimalizálás célja. A szintézis menete valójában. Kódoptimalizálási lépések osztályozása

Adatelérés és memóriakezelés

1. Alapok. Programozás II

Programozási nyelvek Java

Számítógépek felépítése, alapfogalmak

A processzor hajtja végre a műveleteket. összeadás, szorzás, logikai műveletek (és, vagy, nem)

Architektúra, megszakítási rendszerek

Java II. I A Java programozási nyelv alapelemei

8. Fejezet Processzor (CPU) és memória: tervezés, implementáció, modern megoldások

Java II. I A Java programozási nyelv alapelemei

OpenCL - The open standard for parallel programming of heterogeneous systems

Számítógép architektúrák. Tartalom. A memória. A memória

Máté: Számítógép architektúrák

8. Fejezet Processzor (CPU) és memória: tervezés, implementáció, modern megoldások

egy szisztolikus példa

Programozási nyelvek a közoktatásban alapfogalmak II. előadás

Adatok ábrázolása, adattípusok

C memóriakezelés. Mutató típusú változót egy típus és a változó neve elé írt csillag karakterrel hozhatjuk létre.

Számítógép Architektúrák

3. Osztályok II. Programozás II

Programozás I gyakorlat

Programozás I. 3. gyakorlat. Szegedi Tudományegyetem Természettudományi és Informatikai Kar

Operációs rendszerek. Az NT memóriakezelése

Programozás II. 4. Dr. Iványi Péter

Szoftvergyártás: gyártásvezérlés kód-figyeléssel

ARM Cortex magú mikrovezérlők

Digitális rendszerek. Utasításarchitektúra szintje

Bevezetés a programozásba Előadás: Objektumszintű és osztályszintű elemek, hibakezelés

A programozás alapjai

A számítógép alapfelépítése

Fábián Zoltán Hálózatok elmélet

Bevezetés a programozásba I 10. gyakorlat. C++: alprogramok deklarációja és paraméterátadása

Készítette: Nagy Tibor István

Flynn féle osztályozás Single Isntruction Multiple Instruction Single Data SISD SIMD Multiple Data MISD MIMD

Grafikus csővezeték 1 / 44

Objektum orientált kiterjesztés A+ programozási nyelvhez

Programozás C++ -ban 2007/7

1. Bevezetés szeptember 9. BME Fizika Intézet. Szám. szim. labor ea. Tőke Csaba. Tudnivalók. feladat. Tematika. Moodle Házi feladatok

Programozás. (GKxB_INTM021) Dr. Hatwágner F. Miklós április 4. Széchenyi István Egyetem, Gy r

1. Az utasítás beolvasása a processzorba

Adatbázis Rendszerek II. 5. PLSQL Csomagok 16/1B IT MAN

Bepillantás a gépházba

Memóriák - tárak. Memória. Kapacitás Ár. Sebesség. Háttértár. (felejtő) (nem felejtő)

Programozási nyelvek JAVA EA+GY 1. gyakolat

Assembly programozás levelező tagozat

A C# programozási nyelv alapjai

Bevezetés. Többszálú, többmagos architektúrák és programozásuk Óbudai Egyetem, Neumann János Informatikai Kar

Számítógép architektúrák. A mai témák. A teljesítmény fokozás. A processzor teljesítmény növelése

Számítógép Architektúrák

A függvény kód szekvenciáját kapcsos zárójelek közt definiáljuk, a { } -ek közti részt a Bash héj kód blokknak (code block) nevezi.

Matlab alapok. Baran Ágnes. Baran Ágnes Matlab alapok Elágazások, függvények 1 / 15

Vizuális, eseményvezérelt programozás XI.

Adatbázis és szoftverfejlesztés elmélet

Programozási nyelvek (ADA)

C programozási nyelv Pointerek, tömbök, pointer aritmetika

PE/COFF fájl formátum

Átírás:

Mentegetőzések Hatékony CPU kód írásához hasznos architekturális háttér, hogy miért is lassú a kódom Valasek Gábor

Tartalom 1. 2. 3. 4. Pipeline-ing, szuperskalár architektúra A programunk a memóriában Cache-ek: I cache és D cache Néhány probléma (stalling, branch prediction, load-hit-store)

Tartalom 1. 2. 3. 4. Pipeline-ing, szuperskalár architektúra A programunk a memóriában Cache-ek: I cache és D cache Néhány probléma (stalling, branch prediction, load-hit-store)

Pipeline architektúrák

Pipeline architektúrák Két fontos statisztikájuk van: Latency: egy utasítás végrehajtásához szükséges teljes idő. Bandwidth vagy throughput: egységnyi idő alatt hány utasítást tud feldolgozni a rendszer. Ez a leglassabb komponenstől függ.

Szuperskalár architektúrák

Szuperskalár architektúrák A pipeline egyes fázisait megvalósító egységeknek több, redundáns példánya is van Ezáltal egyszerre több utasítás is végrehajtható párhuzamosan Például az Intel processzorok pipeline-os és szuperskalár architektúrák Érdeklődőknek további anyagok: http://www.lighterra.com/papers/modernmicroprocessors/

Hatékony kód régen és most Amíg az órajelek relatív alacsonyak voltak, megérte minimalizálni a számítások számát Ez ma már megfordult: a mantra az, hogy inkább végezz több munkát a CPU-val és csak akkor nyúlj a memóriához, ha feltétlenül szükséges Ennek oka az elérési idők módosulása (forrás: https://www.theregister.co.uk/2016/04/21/storage_approaches_memory_spee d_with_point_and_storageclass_memory/ )

Elérési idők

Elérési idők

Irodalom Egy nagyon hasznos bejegyzés erről a témáról: http://ithare.com/infographics-operation-costs-in-cpu-clock-cycles/

Tartalom 1. 2. 3. 4. Pipeline-ing, szuperskalár architektúra A programunk a memóriában Cache-ek: I cache és D cache Néhány probléma (stalling, branch prediction, load-hit-store)

Program a memóriában A végrehajtható fájl lehet egy.ee (Windows) vagy.elf (eecutable and linking format - Uni) A lefordított és összelinkelt végrehajtható fájl tartalmazza a program eecutable image-ét Ez egy részleges képe annak, ahogyan a program memóriában ki fog nézni a dinamikus memóriafoglalásokat értelemszerűen nem tartalmazza Az image 4 szegmensből áll

Eecution image Code/tet segment Data segment BSS segment Read-only data segment

Eecution image Végrehajtható gépikód A kódban inicializált globális illetve statikus változók Inicializálatlan globális és statikus változók Csak olvasható adatok Az értékük a specifikáció szerint adott (=0), de csak a program belépési pontjának meghívása előtt nullázódik ki. Egyes esetekben (ún. manifest constant-oknál) a kódba fordul bele a konstans értéke. Ilyen pl. az int konstans.

Eecution image A BSS megnevezés történelmi hagyaték: a block started by symbol rövidítése A manifest konstansok trükkösek: mivel a compiler az értéküket belehelyettesíti a kódba, ezért az ő tárolásuk valójában a kódszegmensben történik

Endian Az egy bájtnál nagyobb foglalású változók esetén jön képbe Kétféle verziója van: Little endian: a kisebb helyiértéket reprezentáló bájtok a memória elejéhez vannak közelebb Big endian: a legnagyobb helyiérték bájtja van a kisebb memóriacímen Fontos, hogy min fejlesztünk és mire fejlesztünk: Intel processzorok: little endian-ok Wii, Xbo 360, PlayStation 3 (vagyis PowerPC hajtotta konzolok): big endian-ok

Program végrehajtása A belépési pont elindításával kezdődik (pl. main() ) A futtatás megkezdésekor az OS lefoglal egy memóriaterületet az alkalmazásnak, amit program stack-nek hívnak Minden egyes függvényhíváskor erre a stack-re push-olnak egy összefüggő (=folytonos) memóriafoglalást, amit stack frame-nek hívnak

Stack frame Háromféle adatot tárolunk itt: A függvényünket hívó függvény memóriacímét, hogy a visszatérésünk után folytatódhasson a program futtatása A CPU regisztereinek értékeit a hívás pillanatában. Visszatéréskor ezeket az értékeket visszaírjuk a regiszterekbe. A függvény visszatérési értéke viszont általában egy speciális regiszterbe kerül, amit értelemszerűen nem állítunk vissza A függvény lokális változói is itt kerülnek foglalásra. (Meg néha regiszterekbe kerülnek, de ezt most nem részletezzük)

Példa void c() { U32 localc1;... } F32 b() { F32 localb1; I32 localb2;... c(); return localb1; } void a() { U32 alocalsa1[5];... F32 locala2 = b();... }

Változók helye a memóriában A globális és a statikus változók a futtatható fájlban vannak A lokális változók a stack-re kerülnek A dinamikus változók azonban a heap-re A probléma ezzel az, hogy az OS-től függ a foglalás Ezért váratlanul sokat állhat a programunk, amíg a new vissza nem tér (...legalábbis 4 new a 6-ból (<C++17)/8-ból(>=C++17)..)

Tartalom 1. 2. 3. 4. Pipeline-ing, szuperskalár architektúra A programunk a memóriában Cache-ek: I cache és D cache Néhány probléma (stalling, branch prediction, load-hit-store)

Cache A CPU által írható és olvasható memóriadarab, aminek kialakításánál a cél a minél kisebb késleltetés Ezt kétféleképpen érik el: A lehető leggyorsabb memóriatechnológiát használják hozzájuk A cache-ek fizikailag is közelebb vannak a CPU-hoz A cache lényegében a globális memóriában lévő változóknak egy (CPU szempontjából) lokális másolata Méghozzá azoké, amiket gyakran lekérdez a program Így ha épp cache-ben van, akkor nem kell elzarándokolni a RAM-ig (ez a cache hit) Ha nem volt a cache-ben (cache miss), elmegyünk a RAM-ig, visszahozzuk az adatot de egyúttal beírjuk a cache-be is (hátha legközelebb is kell)

Cache line Amikor egy új memóriádarabot érünk el cache miss miatt, nem csak az általunk kért adatok jönnek vissza, hanem egy cache line-nyi darab i7-es archiektúrákon a L1, L2, L3 cache line-ok 64 byte-osak Így ha a programunk következő utasítása az előzőleg lekért memória utáni memóriát olvasná, akkor garantált () cache hit-ünk van A cache-ek asszociatív memóriák: tudják, hogy melyik RAM-beli valódi memóriaterület van bennük (ehhez használják a translation lookaside buffer-t, azaz a TLB-t)

Cache

Cache - írási policy Amikor a programunk egy változó értékét módosítja, akkor azt vissza kell juttatni a memóriába is Ez a CPU architektúra write policy-jétől függ, hogy miképp történik Az írások bekerülnek a cache-be, aztán A write-through cache-ek az írásokat rögtön továbbítják a memóriába is A write-back policy-k pedig csak bizonyos esetekben (pl. cache miss) írnak vissza

Többszintű cache Probléma, hogy kisméretű cache A nagyméretű viszont Nagyon gyors De sok cache miss-t generál Fizikailag nem tud olyan közel kerülni a CPU-hoz De cserébe több cache hit lesz Ezért csinálnak többszintű cache-eket, egyre nagyobb méretben

Cache-ek többmagos környezetben

Cache-ek többmagos környezetben Többmagos környezetben előjön a cache konzisztencia problémája Vagyis hogy minden mag lokális cache-e a fizikai memóriában található valódi értéket tükrözze Két elterjedt protokoll erre a MESI (modified, eclusive, shared, invalid) és a MOESI (modified, owned, eclusive, shared, invalid): https://en.wikipedia.org/wiki/moesi_protocol

A kétféle cache Instruction cache: a programunk kódját cache-eli. Elágazásokkal ezt tudjuk tönkrevágni, ezért van szükség branch prediction-re stb. Data cache: a programunk adatait cache-eli A fenti kettő független egymástól

D-cache-re optimalizálás Az adatainkat a memóriában folytonosan tároljuk, a lehető legkisebb méretben és szekvenciálisan dolgozzuk fel őket

I-cache-re optimalizálás A teljesítmény-kritikus ciklusok a lehető legrövidebbek legyenek kódméret szempontjából Ne hívjunk belőlük függvényeket Ha mégis kell és kicsi a függvény, akkor inline-oljuk Ha nem kicsi és csak igazi függvényhívással érhető el, ekkor érjük el, hogy a memóriában a függvény gépi kódja a ciklushoz a lehető legközelebb legyen

I-cache-re optimalizálás Az utóbbi nagyon nehéz, mert a linker és a compiler dönti el, hogy fizikailag hová kerül az eecutable image-ben a függvény törzse Viszont van néhány rule-of-thumb amit általában be szoktak tartani: Egy függvény kódja a memóriában szinte mindig folytonos, azaz a linker nem szúr bele más kódot a függvénybe (kivéve ha az a más kód egy inline-olt függvény hívása) A függvények a fordítási egységük forráskódjában (.cpp) található sorrendjüknek megfelelően kerülnek a memóriába Vagyis egyetlen fordítási egység függvényei a memóriában folytonosan helyezkednek el (általában)

Tartalom 1. 2. 3. 4. Pipeline-ing, szuperskalár architektúra A programunk a memóriában Cache-ek: I cache és D cache Néhány probléma (stalling, branch prediction, load-hit-store)

Stalling Előfordulhat, hogy egy utasítás végrehajtása csak akkor kezdődhet meg, hogy ha egy előtte lévő utasítás teljesen végigment a pipeline-on Ekkor a függő utasítás beakasztja a pipeline-t - ez a stalling

Stalling - adatfüggőség miatt

Stalling A fenti példában az add utasításnak be kellett várni a mul befejeződését Az ilyenek elkerülése érdekében a compilerek megpróbálják automatikusan átrendezni az utasításaink sorrendjét, hogy a függés miatt várakozó utasítás helyett független utasítások futhassanak

Branch prediction Amikor a végrehajtás elágazáshoz ér, akkor a pipeline-nak döntenie kell, hogy az then vagy az else ág kódját kezdi végrehajtani Ha rosszul tippelt, akkor a pipeline-t flush-olni kell (az eddigi műveleteket érvényteleníteni) és újratölteni a helyes elágazási irány kódjával A legegyszerűbb (statikus) stratégia a CPU részéről hogy a backward branch-et tekinti valószínűbbnek (azaz azt az ágát az elágazásnak, aminek a gépi kód memóriacíme kisebb, mint az aktuális cím - ilyen például az az eset, amikor a ciklusunk még folytatja a futását) Következmény: if-nél, többfelé ágazó if-nél, switch-case-nél először a leggyakoribb eseteket rakjátok

Stalling + branching megoldások + O(e) => meltdown

Load-Hit-Store A probléma lényege, hogy egy változóba történő írás után közvetlenül próbáljuk újra olvasni a változó értékét A klasszikus példa: az oris-en stallol: stfs fr3,0(r3) ;Store the float lwz r9,0(r3) ;Read it back into an integer register oris r9,r9,08000 ;Force to negative

Load-Hit-Store int CauseLHS( int ptra ) { Nem lehetett ptra értékét regiszterben/cache-ben tartani, mert nem tudja a compiler, hogy nem módosította-e a ptra területét egy másik pointer a függvényben, ami ugyanoda mutat! int a,b; } int ptrb = ptra; // B and A point to the same direction ptra = 5; // Write data to address prta b = ptrb; // Read that data back again (won't be available for 40/80 cycles) a = b + 10;// Stall! The data b isn't available yet

Load-Hit-Store int slow( int a, int b) { a = 5; b = 7; return a + b;// Stall! The compiler doesn't know whether // a==b, so it has to reload both // before the add }

Irodalom és javasolt néznivalók Jason Gregory: Game Engine Architecture (az ábrák forrása is ez) Kitekintésnek C++ HPC workshop: https://www.youtube.com/watch?v=7wvlfzrksk&list=pl1tk5lgm7zvqh6rk uropdmdomhb6lzcwl

Polinomok kiértékelése Forrás: http://lolengine.net/blog/2011/9/17/playing-with-the-cpu-pipeline A probléma: hogyan lehet kiértékelni egy polinomot hatékonyan? Most: a sin() hatványsoros közelítését, elvágva az ^15-edikenes tagnál

Polinomok kiértékelése static static static static static static static static double double double double double double double double a0 a1 a2 a3 a4 a5 a6 a7 = = = = = = = = +1.0; -1.666666666666580809419428987894207e-1; +8.333333333262716094425037738346873e-3; -1.984126982005911439283646346964929e-4; +2.755731607338689220657382272783309e-6; -2.505185130214293595900283001271652e-8; +1.604729591825977403374012010065495e-10; -7.364589573262279913270651228486670e-13; double sin1(double ) { return a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 } 64 szorzás + 7 összeadás ;

Polinomok kiértékelése double sin2(double { double ret, y =, 2 = ret = a0 y; ret += a1 y; ret += a2 y; ret += a3 y; ret += a4 y; ret += a5 y; ret += a6 y; ret += a7 y; return ret; } ) y y y y y y y ; = = = = = = = 2; 2; 2; 2; 2; 2; 2; 16 szorzás + 7 összeadás

Polinomok kiértékelése double sin3(double ) // Horner { double 2 = ; return (a0 + 2 (a1 + 2 (a2 + 2 (a3 + 2 (a4 + 2 (a5 + 2 (a6 + 2 a7))))))); } 9 szorzás + 7 összeadás

Mérések Intel Core i7-2620m CPU at 2.70GHz. The functions were compiled using -O3 -ffast-math: function nanoseconds per call sin 22.518 sin1 16.406 sin2 16.658 sin3 25.276

Polinomok kiértékelése double sin3(double ) // Horner { double 2 = ; return (a0 + 2 (a1 + 2 (a2 + 2 (a3 + 2 (a4 + 2 (a5 + 2 (a6 + 2 a7))))))); }

Polinomok kiértékelése double sin2(double { double ret, y =, 2 = ret = a0 y; ret += a1 y; ret += a2 y; ret += a3 y; ret += a4 y; ret += a5 y; ret += a6 y; ret += a7 y; return ret; } ) y y y y y y y ; = = = = = = = 2; 2; 2; 2; 2; 2; 2;

Polinomok kiértékelése Kézzel optimalizálni: function sin sin1 sin2 sin3 sin4 sin5 sin6 sin7 nanoseconds per call 22.518 16.406 16.658 25.276 18.666 18.582 16.366 17.470 Persze nem árt a compiler flag-ekkel is kicsit foglalkozni (csak -O3): function nanoseconds per call sin 22.497 sin1 30.250 sin2 19.865 sin3 25.279 sin4 18.587 De akár használhatsz irodalmat is: Estrin séma sin5 18.958 sin6 16.362 sin7 15.891