1.13k likes | 1.26k Views
Vývoj vysoce výkonného software. O čem to bude. O čem to bude. Maximální využití výkonu procesoru a paměti Moderní programování vs. výkon Relevantní vlastnosti moderních procesorů ILP, SIMD Nástroje pro ladění výkon u Co dokáže a co nedokáže překladač Paměťová hierarchie
E N D
Vývoj vysoce výkonného software NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
O čem to bude NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
O čem to bude NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek • Maximální využití výkonu procesoru a paměti • Moderní programování vs. výkon • Relevantní vlastnosti moderních procesorů • ILP, SIMD • Nástroje pro ladění výkonu • Co dokáže a co nedokáže překladač • Paměťová hierarchie • Cache-Aware a Cache-Oblivious algoritmy
O čem to nebude NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
O čem to nebude NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek • Programování v asembleru • Překladače jsou většinou lepší než lidé • Ale naučíme se využívat SIMD instrukce z C++ • Paralelní programování • Viz NPRG042 Programování v paralelním prostředí • Poučení z minulých let: Pečlivá implementace jednovláknové verze přináší stejné zrychlení jako paralelizace • Optimalizace programů překladačem • Viz NSWI109 Konstrukce překladačů • Ale dozvíte se, co můžete od překladače očekávat
Proč? NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Loňské DÚ 1 (letošní DÚ 2 bez optimalizace) – výsledky studentů NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Loňské DÚ2 (letošní DÚ2 včetně optimalizace) - výsledky studentů NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Motivační příklad NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Motivační příklad - DÚ 0+1 NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek • Hromada jablek a pomerančů, spočítejte zrníčka • class Apple ... • { ... • ... int seeds() { return ...; } • }; • class Orange ... • { ... • ... int seeds() { return ...; } • }; • vector< Apple + Orange> data;
Motivační příklad • class Fruit { • ... • virtual int seeds() = 0; • }; • class Apple: public class Fruit { • virtual int seeds() { return ...; } • ... • }; • class Orange : public class Fruit { • virtual int seeds() { return ...; } • ... • }; • vector< Fruit *> data; • Klasické objektové řešení • Abstraktní třída s virtuální funkcí • Konkrétní třídy • Různá data • Různé implementace virtuální funkce • Kontejner • Obsahuje ukazatele na abstraktní třídu • Toto řešení existuje ve všech objektových jazycích • V jazyzích s referenční semantikou jsou ukazatele skryté • List< Fruit> data; NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Motivační příklad • class Fruit { • virtual int seeds() = 0; • }; • class Apple: public class Fruit { • virtual int seeds() { return d2; } • int d1, d2, d3; • }; • class Orange : public class Fruit { • virtual int seeds() { return d3; } • int d1, d2, d3, d4, d5; • }; • vector< Fruit *> data; • int s = 0; • for_each( data.begin(), data.end(), • [&]( Fruit * p) { s += p->seeds(); }); • Jak je to rychlé? • Testovací data • 5 konkrétních tříd lišících se počtem datových položek • Implementace virtuální funkce čtou některou z položek • Kontejner naplněn náhodnou směsí konkrétních tříd NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Motivační příklad • void test() • { • int s = 0; • for_each( data.begin(), data.end(), • [&]( Fruit * p) { s += p->seeds(); }); • } • generate_data(); • test(); // cold run • time_before = now(); • for ( inti = 0; i < N; ++ i) • test(); • time_after = now(); • cout << (time_after - time_before) / N; • Testovací prostředí • Test je opakován N-krát • N je voleno tak, aby celkový čas byl přiměřený • V našem testu sekundy • Program si měří čas sám • Vestandardu C++ vhodná funkce now() není • Plaformy nabízejí různé možnosti • Ty lepší jsou pouze pro privilegované • V našem testu měříme "wall clock" • Je to to, co uživatele zajímá? • Zatíženo paralelně běžícími procesy • Granularita 1-10 ms NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Výsledky • Výsledky testu • 1 000 000 objektů • Celkem 10,4 ms • 10,4 ns na objekt • 1 000 objektů • Celkem 9,0 µs • 9,0 ns na objekt • Spolehlivost výsledků? • Při opakování se výsledky lišío5-10% • Pro orientacidostatečné • Statisticky správné měření je věda • NSWI131 Vyhodnocování výkonnosti počítačových systémů • Hardware • Intel Core2Quad Q6700 • 2,66 GHz • 4 GB RAM • Operační systém • Windows 7 64-bit • 64-bitový kód • Překladače • MS Visual C++ 2010 • MS Visual C++ 2012 • Intel Composer XE 2013 • Rozdíly menší než 5% NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Výsledky - porovnání architektur na témže HW • 64-bitový kód (Intel-64) • 1 000 000 objektů • Celkem 10,4 ms • 10,4 ns na objekt • 1 000 objektů • Celkem 9,0 µs • 9,0 ns na objekt • Ukazatele jsou delší • Procesor vykonává více práce • Přesto běží rychleji! • Architektura ma víc registrů • Procesor je optimalizován pro tento mód • Nebo je to jinak... • 32-bitový kód (IA-32) • 1 000 000 objektů • Celkem 10,7 ms • 10,7 ns na objekt • 1 000 objektů • Celkem 10,0 µs • 10,0 ns na objekt NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Výsledky - závislost na distribuci dat • 64-bitový kód (Intel-64) • 1 000 000 objektů náhodně • 10,4 ns na objekt • 1 000 objektů náhodně • 9,0 ns na objekt • 1 000 000 objektů round-robin • 8,0 ns na objekt • 1 000 objektů round-robin • 2,6 ns na objekt • 1 000 000 objektů skupinově • 7,6 ns na objekt • 1 000 objektů skupinově • 2,8 ns na objekt • 32-bitový kód (IA-32) • 1 000 000 objektů náhodně • 10,7 ns na objekt • 1 000 objektů náhodně • 10,0 ns na objekt • 1 000 000 objektů round-robin • 5,9 ns na objekt • 1 000 objektů round-robin • 2,2 ns na objekt • 1 000 000 objektů skupinově • 5,3 ns na objekt • 1 000 objektů skupinově • 2,6 ns na objekt NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Příčiny NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek • Predikce skoků • Volání virtuální funkce je nepřímý skok • Dokud není znám cíl skoku, dekódování instrukcí nemůže pokračovat • Procesory předvídají cíle • Z předchozích průchodů tímtéž kódem • Asociativní paměť adresa skokové instrukce - adresa cíle • Heuristické metody predikce • Call-return páry • Když predikce nevyjde • Dekódované a částečně provedené instrukce se zahazují • Čeká se na dekódování těch správných • Zdržení v řádu jednotek až desítek cyklů procesoru
Příčiny NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek • Predikce skoků • Volání virtuální funkce je nepřímý skok • Dokud není znám cíl skoku, dekódování instrukcí nemůže pokračovat • Procesory předvídají cíle • Z předchozích průchodů tímtéž kódem • Asociativní paměť adresa skokové instrukce - adresa cíle • Heuristické metody predikce • Call-return páry • Když predikce nevyjde • Dekódované a částečně provedené instrukce se zahazují • Čeká se na dekódování těch správných • Zdržení v řádu jednotek až desítek cyklů procesoru
Typické mikroarchitektury procesorů (2000-2012) NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Intel Netburst Microarchitecture [2000] NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Intel NetBurst Microarchitecture [2000] NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Intel Core Microarchitecture Pipeline [2006] NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Intel Core Microarchitecture NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek • Procesor (teoreticky) zvládá v jediném cyklu najednou: • Fetch: 16 B (cca. 4 instrukce) z instrukční cache • Decode: 1 až 5 instrukcí • ALU: 3 jednodušší operace (add/mul) • Memory load: 1 čtení (až 128 bitů) z L1 cache • Memory store: 1 zápis (až 128 bitů) z L1 cache • Doba trvání operací (latence) • integer add, mul: 1 • FP add: 3, FP mul: 4-5 • div: podle dat • integer load: 3, FP load: 4 (L1 cache) • store address: 3 • store data: 2 (retirement, in-order)
Intel Core Microarchitecture NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek • Branch prediction • podmínky, nepřímé skoky, call/return páry • spekulativní provádění • Dekodér instrukcí • loop cache (jednoduché cykly do 18 instrukcí) • převod na mikrooperace 1:1, 1:N, 2:1 • simulátor ukazatele zásobníku • Renamer • 16 logických registrů mapováno do 144 fyzických (podobně FP registry) • Out-of-order execution • 32 rozpracovaných mikrooperací (RS) z okna délky 96 (ROB) • retirement: zápisy do paměti/registrů běží in-order opožděně na pozadí • store forwarding: čekající čtení dostávají hodnotu přímo ze zápisu • spekulativní čtení: nečeká se na předchozí store
Intel Nehalem Pipeline [2008] NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Intel Sandy Bridge Pipeline [2011] NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Intel vs. AMD architectures (realworldtech.com) NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Intel Haswell Microarchitecture NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Pohled překladače na procesor NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek • Scheduling - volba uspořádání instrukcí • Nejpodstatnější fáze překladače z hlediska výkonu kódu • Platilo do nedávna, narušeno out-of-order execution v CPU • Hledá se takové pořadí které je • Ekvivalentní z hlediska efektu/pravidel jazyka • Vyhovuje dependencím • Nejrychlejší • Model časování procesoru
Závislosti Závislost (dependence) Povinnost provést jednu operaci/instrukci po jiné Částečné uspořádání operací/instrukcí v jednom BB Datová závislost (dependence) Závislost producent-konzument v toku dat Antidependence Read-Write: Čtení se musí stihnout před zápisem Write-Write: Pořadí zápisů se nesmí změnit Jiné důvody, obvykle nízkoúrovňového původu
Scheduling Scheduling pouze odhaduje skutečné časování Skutečné časování je ovlivněno nepředvídatelnými jevy Zbytky rozpracovaných instrukcí z předchozího kódu Řešení: Trace-scheduling, řízení profilem Paměťová hierarchie Doba přístupu k paměti závisí na přítomnosti v cache Obvykle se předpokládá nejlepší možný průběh Speciální problém: Multithreaded aplikace na multiprocesorech Fetch bandwidth Instrukce nemusí být načteny a dekódovány včas Zdržují skoky a soupeření o přístup do paměti Přesné simulování fetch jednotky by neúměrně komplikovalo scheduler Scheduler nezná skutečné závislosti přístupů do paměti Musí postupovat opatrně a zohledňuje i nejisté závislosti Procesor zná skutečné adresy přístupů a detekuje pouze skutečné závislosti Agresivně optimalizující procesor může zvolit zcela jiné pořadí instrukcí
Scheduling Model procesoru Latence – časování závislých dvojic instrukcí Počet cyklů procesoru, který musí proběhnout mezi referenčními body závislých instrukcí U antidependencí a ve speciálních případech může být nulová U procesorů se sekvenčními body může být záporná
Scheduling Model procesoru Rezervační tabulky – schopnosti paralelního zpracování Procesor je rozdělen na funkční jednotky různých druhů Je určen počet jednotek každého druhu Pro každou instrukci definována rezervační tabulka Počet jednotek daného druhu, který instrukce potřebuje v daném čase (měřeno od referenčního bodu) Rezervační tabulky jsou nutné i pro procesory, které nejsou super-skalární
Scheduling - pokročilé techniky Loop unrolling V cyklech s malým počtem instrukcí někdy není co paralelizovat Současné zpracování dvou nebo více iterací pomůže Modulo scheduling, software pipelining
Příklad – Intel compiler – x64 /*...*/ k = i >> 1; j = 0; do { r8 = *p; r9 = *(p+1); s ^= r8; s ^= r9; p += 2; j += 1; } while ( j < k ); /* ... */ charchksum( char* p, inti) { char s = 0; while ( i > 0 ) { s ^= *p++; --i; } return s; } ..B1.4: movsbq (%rdi), %r8 movsbq 1(%rdi), %r9 xorl %r8d, %eax xorl %r9d, %eax addq $2, %rdi addl $1, %ecx cmpl %edx, %ecx jb ..B1.4
Průchod polymorfní datové struktury - pohled procesoru NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Kritický kód - Intel C++ 64-bit • Zdrojový kód • std::for_each( b, e, [&](Fruit * p){ s += p->seeds(); }); • Smyčka po inline expanzi • for(; b != e; ++ b) s += (*b)->seeds(); • Generovaný strojový kód • .lp: • mov rcx, QWORD PTR [r14] • mov rax, QWORD PTR [rcx] • call QWORD PTR [8+rax] • add r14, 8 • add DWORD PTR [88+rbp], eax • cmp r14, r13 • jne .lp ; Prob 82% • Tělo virtuální funkce seeds • mov eax, DWORD PTR [8+rcx] • ret • Registry • r14 = b • r13 = e • rcx = * b • rax = VTable • eax = hodnota f() • rbp = stackframe • [88+rbp] = s • Skoky • dobrá predikce • ret • jne • špatná predikce • call NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Kritický kód - Intel C++ 64-bit • Zdrojový kód • std::for_each( b, e, [&](Fruit * p){ s += p->seeds(); }); • Smyčka po inline expanzi • for(; b != e; ++ b) s += (*b)->seeds(); • Generovaný strojový kód • .lp: • mov rcx, QWORD PTR [r14] • mov rax, QWORD PTR [rcx] • call QWORD PTR [8+rax] • add r14, 8 • add DWORD PTR [88+rbp], eax • cmp r14, r13 • jne .lp ; Prob 82% • Tělo virtuální funkce seeds • mov eax, DWORD PTR [8+rcx] • ret • Závislosti • write-read • read-write • test-access • Restart pipeline • Špatně predikovaný skok do virtuální funkce NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Kritický kód - Intel C++ 64-bit • Tělo virtuální funkce seeds • mov eax, DWORD PTR [8+rcx] • ret • Konec smyčky • add r14, 8 • add DWORD PTR [88+rbp], eax • cmp r14, r13 • jne .lp ; Prob 82% • Začátek smyčky • .lp: • mov rcx, QWORD PTR [r14] • mov rax, QWORD PTR [rcx] • call QWORD PTR [8+rax] • Špatně predikovaný cíl volání • mov eax, DWORD PTR [4+rcx] • ret • Mikrooperace (odhad) • load eax,[8+rcx] • load t1,[rsp++] • jmp t1 • add r14,8 • load t2,[88+rbp] • add t2,eax • store [88+rbp],t2 • cmp r14,r13,eflags • jne .lp,eflags • load rcx,[r14] • load rax,[rcx] • load t3,[8+rax] • store [--rsp],rip • jmp t3 • load eax’,[4+rcx] • load t4,[rsp++] • jmp t4 NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Odhad provedení kritického kódu s neúspěšnou predikcí 1: load eax,[8+rcx] 2: load t1,[rsp++] 3: jmp t1 4: add r14,8 5: load t2,[88+rbp] 6: add t2,eax 7: store [88+rbp],t2 8: cmp r14,r13,eflags 9: jne .lp,eflags 10: load rcx,[r14] 11: load rax,[rcx] 12: load t3,[8+rax] 13: store [--rsp],rip 14: jmp t3 1': load eax’,[4+rcx] 2': load t4,[rsp++] 3': jmp t4 NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Odhad provedení kritického kódu s úspěšnou predikcí 1: load eax,[8+rcx] 2: load t1,[rsp++] 3: jmp t1 4: add r14,8 5: load t2,[88+rbp] 6: add t2,eax 7: store [88+rbp],t2 8: cmp r14,r13,eflags 9: jne .lp,eflags 10: load rcx,[r14] 11: load rax,[rcx] 12: load t3,[8+rax] 13: store [--rsp],rip 14: jmp t3 NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Výsledky (Intel Core Microarchitecture, 2.66 GHz) NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Hodnocení výsledků NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek • Náhodně promíchané objekty • Neúspěšná predikce skoku • Zpoždění načítání instrukcí + zátěž spekulativně provedenými instrukcemi • Odhad 20 cyklů = 7.5 ns, měření 9-13 ns (cache misses) • Objekty uspořádané do skupin podle druhu • Zlepšuje predikci skoků • U malých dat neúčinné – procesor se nestíhá naučit • Zůstává režie nepřímého volání (ověření predikce) - zatěžuje procesor • Odhad 8 cyklů = 3 ns, měření 3-8 ns (cache misses)
Jiný přístup • class Apple{ • int seeds() { return d2; } • int d1, d2, d3; • }; • class Orange { • int seeds() { return d3; } • int d1, d2, d3, d4, d5; • }; • vector< Apple> dataA; • vector< Orange> dataO; • int s = 0; • for_each( dataA.begin(), dataA.end(), • [&]( Apple & x) { s += x.seeds(); }); • for_each( dataO.begin(), dataO.end(), • [&]( Orange& x) { s += x.seeds(); }); • Bez dědičnosti • Volání nebudou nepřímá • Překladač volané funkce integruje • Odpadá režie volání • Dovoluje další optimalizace • Bez ukazatelů • Ušetříme 1 přístup do paměti • Data jsou těsně vedle sebe • Nulová režie • Hardware prefetch • Použitelné pro operace nezávislé na pořadí • Pro udržení pořadí je zapotřebí další struktura • Pro operace závislé na pořadí neušetříme NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Výsledky (Intel Core Microarchitecture, 2.66 GHz) NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Sloupcově orientovaný přístup • vector< int> Ad1, Ad2, Ad3; • vector< int> Od1, Od2, Od3, Od4, Od5; • int s = 0; • for_each( Ad2.begin(), Ad2.end(), • [&]( int x) { s += x; }); • for_each( Od3.begin(), Od3.end(), • [&]( int x) { s += x; }); • Uložení po sloupcích • Umožňuje použití SIMD instrukcí • Čtená data jsou těsně vedle sebe • Lepší využití cache • Nevýhody • Ignoruje výhody objektového programování (enkapsulace) • Data jednoho objektu jsou rozptýlena • Horší využití cache při náhodném přístupu • Moderní databázové stroje používají sloupcový přístup NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Sloupcově orientovaný přístup • vector< int> Ad1, Ad2, Ad3; • vector< int> Od1, Od2, Od3, Od4, Od5; • int s = 0; • for_each( Ad2.begin(), Ad2.end(), • [&]( int x) { s += x; }); • for_each( Od3.begin(), Od3.end(), • [&]( int x) { s += x; }); • SIMD instrukce • Intel/AMD: MMX/SSE/AVX/... • Některé překladače je dokážou vygenerovat samy • Jinde existují knihovní funkce skrývající SIMD instrukce • Použití vyžaduje znalost těchto instrukcí • Problém: zarovnání • Data pro SIMD instrukci se načítají obvykle atomicky • Vyžadují zarovnání větší než obvykle (SSE: 16 B) • Problém: nedělitelné zbytky na koncích polí • Zvýšená režie cyklu • SIMD se nevyplatí pro malá pole NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek
Sloupcově orientovaný přístup – SSE3 • #include <emmintrin.h> • vector< __m128i> Ad1, Ad2, Ad3; • std::size_t reserve; • __m128i ss = 0; • for_each( Ad2.begin(), Ad2.end() - 1, • [&]( __m128i & x) { • ss = _mm_add_epi32( ss, x); • }); • int s = ss.m128i_i32[ 0] • + ss.m128i_i32[ 1] • + ss.m128i_i32[ 2] • + ss.m128i_i32[ 3]; • for_each( Ad2.back().m128i_i32, • Ad2.back().m128i_i32 + 4 - reserve, • [&]( int x) { s += x; }); • Microsoft VC++/Intel C++ • Knihovní funkce skrývající SIMD instrukce • Baleny jednotlivě • Překladač je zná a integruje • Překladač sám přiděluje registry • Použití vyžaduje znalost těchto instrukcí • jednodušší než programování v assembleru • __m128i je 128-bitový typ odpovídající SSE registru • Překladač (někdy) zajistí zarovnání • _mm_add_epi32 odpovídá SIMD instrukci PADDQ • 4-krát součet 32-bitových čísel NPRG054 Vývoj vysoce výkonného software - 2013/2014 David Bednárek