View
258
Download
2
Category
Preview:
Citation preview
Paralelni programski modeli
pthreadsMPI/OpenMPCilkIPP, TBB
1
IPPIPP = Integrated Performance Primitives Visoko optimizovane primitive za paralelno
programiranje Mogu se bezbedno pozivati iz TBB niti i Cilk linija
Širok skup primitiva specifičnih za razne domene Obrada signala Obrada slike i videa Male matrice i realistično crtanje Kriptografija
2
Primeri IPP Obrada signala
Filtri, kodovanje govora i audia, kompresija podataka, itd.
Obrada slike i videa Aritmetika i izmena podataka, konverzija boja, prag i
poređenje, morfološke operacije, filtriranje, linearne transformacije, statističke funkcije, geometrija , wavelet transformacije, računarska vizija, kompresija, kodovanje, itd.
Male matrice i realistično crtanje Vektorska i matrična algebra, linearni sistemi, metoda
najmanjih kvadrata, realistično crtanje i obrada 3D podataka, matematičke funkcije za 2D, 3D i 4D vektore, itd.
Kriptografija Simetrične primitve, autentifikacija, javni ključevi, itd.
3
TBB (Threading Building Blocks)Teme Paralelizacija jednostavnih petlji Paralelizacija složenih petlje (petlje i protočne obrade) Izuzeća: try-catch Kontejneri Međusobno isključivanje niti Atomske operacije Vreme: metoda tick_count::now() Dodela memorije Raspoređivač zadataka
4
Jenostavne petlje: primer (1/2)Neka je bezbedno paralelno primeniti funkciju Foo na sve elemente niza ADa bi paralelizovali sekvencijalno rešenje Formiramo objekat tela
To je klasa čija metoda operator() radi nad delom iteracionog prostora
blocked_range<T> opisuje jednodimenzioni prostor blocked_range2d opisuje dvodimenzioni prostor
5
void SerialApplyFoo( float a[], size_t n ) {for( size_t i=0; i != n; ++i )
Foo(a[i]);}
Jenostavne petlje: primer (2/2)#include "tbb/tbb.h"using namespace tbb;class ApplyFoo {
float *const my_a;public:
void operator()( const blocked_range<size_t>& r ) const {float *a = my_a;for( size_t i=r.begin(); i!=r.end(); ++i )
Foo(a[i]);}ApplyFoo( float a[] ) : my_a(a) {}
};_______________________________________________#include "tbb/tbb.h"void ParallelApplyFoo( float a[], size_t n ) {
parallel_for(blocked_range<size_t>(0,n), ApplyFoo(a));}
6
Jednostavne petljeLambda izrazi (označeni sa [=] ili [&])
Lambda izraz zamenjuje deklaraciju i konstrukciju funkcijskog objekta ApplyFoo
7
#include "tbb/tbb.h"using namespace tbb;#pragma warning( disable: 588) // warning: use of local type …
void ParallelApplyFoo( float* a, size_t n ) {parallel_for( blocked_range<size_t>(0,n),
[=](const blocked_range<size_t>& r) {for(size_t i=r.begin(); i!=r.end(); ++i)
Foo(a[i]);}
);}//Note: [=] capture by value, [&] capture by reference
Jednostavne petljeparallel_for: skraćeni zapis od forfor(auto i=first; i<last; i+=step) f(i) Skraćen zapis: parallel_for(first,last,step,f)
Ako se f(i) mogu pozivati paralelno; step je opcion element
8
#include "tbb/tbb.h"using namespace tbb;#pragma warning(disable: 588)
void ParallelApplyFoo(float a[], size_t n) {parallel_for(size_t(0), n, [=](size_t i) {Foo(a[i]);});
}
Jednostavne petljeKontrolisana podela iteracionog prostora
Kontrola podele pomoću partitioner i grainsize Zadati simple_partitioner() unutar parallel_for blocked_range<T>(begin,end,grainsize)
Podrazumevani grainsize (G) je 1 simple_partitioner garantuje G/2 ≤ chunksize ≤ G Postoje još auto_partitioner i affinity_partitioner
9
#include "tbb/tbb.h"
void ParallelApplyFoo( float a[], size_t n ) {parallel_for(blocked_range<size_t>(0,n,G), ApplyFoo(a),
simple_partitioner());}
Jednostavne petljePodešavanje kvanta podele (grainsize)
Pravilo: grainsize iteracija traje 100.000 ciklusa Eksperiment: od 100.000 polovljenjem do optimuma Suviše mali grainsize može redukovati paralelizam Bolje malo veći, nego malo manji grainsize
Primer: a[i]=b[i]*c preko milion indeksa
10
Jednostavne petljeŠirina propusnog opsega i bliskost pri baferovanju
Malo ubrzanje zbog nedovoljnog propusnog opsega između procesora i memorije Prestruktuirati radi boljeg iskorišćenja bafera (cache) Ili, iskoristiti affinity_partitioner, kada
Obradu čine par operacija po pristupu podatku Podaci nad kojima petlja radi staju u bafer (cache) Petlja ili slična petlja se izvodi nad istim podacima Postoji više od 2 jezgra (hardware threads) u CPU
11
Jednostavne petljePrimer korišćenja affinity_partitioner (ap)
ap živi preko iteracija petlje To se postiže deklaracijom da je static Ap dodeljuje iteraciju niti koja ju je ranije izvršavala
12
#include "tbb/tbb.h"
void ParallelApplyFoo( float a[], size_t n ) {static affinity_partitioner ap;parallel_for(blocked_range<size_t>(0,n), ApplyFoo(a), ap);
}
void TimeStepFoo( float a[], size_t n, int steps ) {for( int t=0; t<steps; ++t )
ParallelApplyFoo( a, n );}
Jednostavne petljeKorist od bliskosti pri baferovanju
Zavisi od relativnog odnosa veličine skupa podataka i bafera Primer: A[i]+=B[i] u opsegu [0,N)
Za malo N dominira paralelno raspoređivanje, što rezultuje u malom ubrzanju
Za preveliko N skup podatak je prevelik da bi mogao biti prenešen u baferu preko iteracija petlje
13
20
10
010^4 10^5 10^6 10^7 10^8
N
Saffinity_partitionerauto_partitioner
Jednostavne petljeparallel_reduce (1/2)
Ako su iteracije nezavisne, gornja petlja se može paralelizovati sa parallel_reduce
14
float SerialSumFoo( float a[], size_t n ) {float sum = 0;for( size_t i=0; i!=n; ++i )
sum += Foo(a[i]);return sum;
}
float ParallelSumFoo( const float a[], size_t n ) {SumFoo sf(a);parallel_reduce( blocked_range<size_t>(0,n), sf );return sf.my_sum;
}
Jednostavne petljeparallel_reduce (2/2)
15
class SumFoo {float* my_a;
public:float my_sum;void operator()( const blocked_range<size_t>& r ) {
float *a = my_a;float sum = my_sum;size_t end = r.end();for( size_t i=r.begin(); i!=end; ++i )
sum += Foo(a[i]);my_sum = sum;
}SumFoo( SumFoo& x, split ) : my_a(x.my_a), my_sum(0) {}void join( const SumFoo& y ) {my_sum+=y.my_sum;}SumFoo(float a[] ) : my_a(a), my_sum(0) {}
};
Jednostavne petljeIzvršenje parallel_reduce-a
Kada je nit radnik (worker) raspoloživa Poziva se konstruktor razdvajanja
Pravi se podzadatak za nit radnika Nakon završetka podzadatka poziva se join radi akumuliranja
njegovog rezultata
16
Složene petljeparallel_do (1/2)
parallel_do se koristi ako iteracioni prostor nije poznat unapred Primer: povezana lista
U principu je bolje koristiti dinamičke nizove; liste su serijske Ima smisla ako obrada jednog elementa liste uključuje
nekoliko hiljada instrukcija Recimo da hoćemo da paralelizujmo ovaj sekvencijalan kod
17
void SerialApplyFooToList( const std::list<Item>& list ) {for( std::list<Item>::const_iterator i=list.begin() i!=list.end();
++i )Foo(*i);
}
Složene petljeparallel_do (2/2)
Nikada dve niti ne konkurišu za isti iterator Dva načina da se skalabilno dođe do više posla
Korišćenjem iteratora sa slučajnim pristupom Dodavanjem posla sa feeder.add(item)
feeder je drugi argument od funkcije operator() tipa parallel_do_feeder<Item>& 18
class ApplyFoo {public:
void operator()( Item& item ) const {Foo(item);
}};
void ParallelApplyFooToList( const std::list<Item>& list ) {parallel_do( list.begin(), list.end(), ApplyFoo() );
}
Složene petljeprotočna obrada (1/3)
TBB realizuje mustru protočne obrade Apstrakcije: pipeline i filter Primer: kvadriranje brojeva u datoteci
Stepeni protočne obrade: čitaj iz dat., kvadriraj, piši u dat. Blok podataka (4000 znakova) predstavljen klasom TextSlice
19
class TextSlice {char* logical_end, physical_end;static TextSlice* allocate( size_t max_size ) {
// +1 leaves room for a terminating null character.TextSlice* t = (TextSlice*)tbb::tbb_allocator<char>().allocate(
sizeof(TextSlice)+max_size+1 );t->logical_end = t->begin();t->physical_end = t->begin()+max_size;return t;
} ...
Složene petljeprotočna obrada (2/3)
Kod za pravljenje i izvršenje protočne obrade TextSlice objekti se prosleđuju putem pokazivača
20
void RunPipeline( int ntoken, FILE* input_file, FILE* output_file ) {tbb::parallel_pipeline(
ntoken,tbb::make_filter<void,TextSlice*>(
tbb::filter::serial_in_order, MyInputFunc(input_file) )&
tbb::make_filter<TextSlice*,TextSlice*>(tbb::filter::parallel, MyTransformFunc() )
&tbb::make_filter<TextSlice*,void>(
tbb::filter::serial_in_order, MyOutputFunc(output_file) ) );}
Složene petljeprotočna obrada (3/3)
Parametar ntoken kotroliše nivo paralelizma serial_in_order filter obrađuje znakove serijski parallel filter može obrađivati više znakova u paraleli Potencijalan problem: gde čuvati znake u srednjem stepenu
Ne dolazi do izražaja jer je broj znakova ograničen
ntoken specificira max br. znakova koji su u protočnoj obradi Drugi parametar specificira niz filtara make_filter<inputType,outputType>(mode,functor) Filtri se spajaju pomoću operatora & serial_out_of_order filter ne održava originalni redosled Prvi filtar ima spec param tbb::flow_control& fc
Zaustavljanje protočne obrade fc.stop();
21
Složene petljekružni baferi
Koriste se između filtara protočne obrade Dva serial_in_order filtra se mogu povezati kružnim
baferom veličine ntoken Kada se dođe do kraja, početak bafera je slobodan
Propusnost protočne obrade ograničena Brojem znakova N koji se istovremeno obrađuju
Malo N, mali paralelizam; preveliki N, previše resursa
Najsporiji filtar je usko grlo protočne obrade U TextSlice primeru U-I radnje ograničavaju ubrzanje
Veličina prozora može ograničiti propusnost Prevelik prozor – podaci ne mogu da stanu u bafer (cache)
22
Složene petljenelinearne protočne obrade
Mogu se transformisati u linearne Propusnost ostaje ista, jer je usko grlo ostalo isto Kašnjenje se povećava
Npr.: kašnjenje 3 filtra se povećava na kašnjenje 5 filtara Dobar kompromis između jednostavnosti i performanse
23
A
B
C
D
E
A
B
C
D
E
KontejneriOmogućavaju istovremen pristup za više niti Zaštita STL kontejnera semaforima ograničava
paralelizam, odnosno ubrzanje TBB koristi dve metode
Fino zaključavanje (fine-grained locking) samo onih delova kontejnera koji se moraju zaključati
Tehnike bez zaključavanja – niti uračunavaju i popravljaju efekte drugih niti koje ih ometaju
Primitive kontejnera za paralelni pristup su duže nego kod klasičnih STL kontejnera Treba ih koristiti samo ako se isplati dodatni paralelizam koji
one omogućavaju
24
Kontejnericoncurrent_hash_map (1/3)
concurrent_hash_map<Key, T, HashCompare > HashCompare je tip koji određuje kako se računa ključ i kako
se dva ključa porede
25
// Structure that defines hashing and comparison operationsstruct MyHashCompare {
static size_t hash( const string& x ) {size_t h = 0;for( const char* s = x.c_str(); *s; ++s )
h = (h*17)^*s;return h;
}// ! True if strings are equalstatic bool equal( const string& x, const string& y ) {
return x==y;}
};
Kontejnericoncurrent_hash_map (2/3)
Mapa StringTable preslikava string na ceo broj Funkcijski objekat Tally broji pojave stringa
26
// A concurrent hash table that maps strings to ints.typedef concurrent_hash_map<string,int,MyHashCompare>
StringTable;// Function object for counting occurrences of strings.struct Tally {
StringTable& table;Tally( StringTable& table_ ) : table(table_) {}void operator()( const blocked_range<string*> range ) const {
for( string* p=range.begin(); p!=range.end(); ++p ) {StringTable::accessor a;table.insert( a, *p );a->second += 1;
}}
};
Kontejnericoncurrent_hash_map (3/3)
Pokazivač za pristup elementu mape accessor – radi upisa (zaključava element) const_accessor – radi čitanja (dopušteno više u paraleli) accessor i const_accessor su argumenti za metode find i
insert određuju da li je pristup radi ažuriranja ili radi čitanja
Po povratku (const_)accessor traje do njegovog uništenja Kako ograničava paralelizam, poželjno je što pre ga uništiti
Ovo se može uraditi sa: a.release();
27
Kontejnericoncurrent_vector
concurrent_vector<T>, gde je T dinamički niz Metode: push_back, grow_by, grow_to_at_least Nužno je sinhronizovati konstruisanje i pristup elementima Operacije nad concurrent_vector su duže nego nad vector
Koristiti ga samo ako dobijeni paralelizam daje ubrzanje
Važno: metoda clear nije bezbedna za paralelno korišćenje
Čekanje elementa v[i], koji dodaje druga nit Čekaj dok je i<v.size() Čekaj da v[i] bude konstruisan
Koristiti concurrent_vector sa zero_allocator-om koji dodeljene postavlja na 0
Konstruktor elementa postavlja zastavicu tipa atomic u elementu, kao zadnju akciju pri konstruisanju
28
Kontejnericoncurrent_queue
concurrent_queue<T,Alloc >, red vrednosti tipa T Osnove operacije: push i try_pop (ispitaj i izvadi, atomski) U programu sa jednom niti red je FIFO struktura Za više niti, red odraža redosled po redosledu ubacivanja Red je neograničen i neblokirajući (nema wait metodu)
concurrent_bounded_queue<T,Alloc > Ograničen red: varijanta sa blokiranjem Operacije: push, pop, try_push, size Metoda size vraća broje nepreuzetih elemenata reda
Može biti manja od nule
Veličina se postavlja metodom set_capacity (inicijalno je red neograničen) Kod ograničenog push je blokirajući; sporiji od neograničenog
29
KontejneriNedostaci redova
Pre upotrebe redova razmotriti parallel_do i protočnu obradu, koji su obično efikasniji Red je usko grlo, zbog održavanja FIFO redosleda Nit koja treba da preuzme element može čekati
besposlena na redu Red je pasivna struktura; ubačen element se može
“ohladiti” u baferu Još gore, ako element preuzima nit u drugom jezgru, on i sve
što referencira mora se prebaciti u bafer tog jezgra
Nasuprot tome, parallel_do optimizuje upotrebu niti radnika da rade nešto drugo dok čekaju element i da održavaju elemente vrućima u baferu (cache)
30
Međusobno isključivanje nitiRealizuje se pomoću mutex i locks mutex je objekat, koji nit može zaključati, prethodno
dobijenim ključem Samo jedna nit može imati ključ, ostale moraju čekati Najjednostavniji mutex je spin_mutex
Adekvatan ako se ključ drži samo nekoliko instrukcija Primer: FreeListMutex štiti deljenu promenljivu FreeList
Grupisanje iskaza u blok može izgledati neobično – uloga je da se skrati vreme života ključa
31
Međusobno isključivanje nitiPrimer upotrebe spin_mutex-aNode* FreeList;typedef spin_mutex FreeListMutexType;FreeListMutexType FreeListMutex;
Node* AllocateNode() {Node* n;{
// constructor locks the listFreeListMutexType::scoped_lock lock(FreeListMutex);n = FreeList;if( n )
FreeList = n->next;} // descructor unlocks the listif( !n )
n = new Node();return n;
}
32
Međusobno isključivanje nitiAlternativan primer upotrebe spin_mutex-a
Metode acquire i release OO sprega – otključavanje u slučaju izuzeća Mutex tipa M ima ključ tipa M::scoped_lock
33
Node* AllocateNode() {Node* n;FreeListMutexType::scoped_lock lock;lock.acquire(FreeListMutex);n = FreeList;if( n )
FreeList = n->next;lock.release();if( !n )
n = new Node();return n;
}
Međusobno isključivanje nitiAtributi i vrste mutex-a
Atributi mutex-a Scalable/Non-scalable, Fair/Unfair, Recursive/Non-recursive,
Yield/Block (yield – upošljeno čekanje)
Vrste mutex-a spin_mutex
Non-scalable, unfair, non-recursive, yield (“vrti se”, spins)
queuing_mutex Scalable, fair, non-recursive, yield (spins)
spin_rw_mutex i queuing_rw_mutex Kao prethodni + podrška za ključeve čitača (readers)
mutex i recursive_mutex su omotači oko OS primitiva CRITICAL_SECTION na OS Windows
null_mutex and null_rw_mutex – ne rade ništa
34
Međusobno isključivanje nitiMutex-i čitača-pisača
Mutex-i su potrebni ako bar jedna nit piše u objekat Više čitača mogu dobiti ulaz u zaštićeni deo koda Mutex-i sa _rw_ u imenu razlikuju ključeve
Ključevi za čitanje i upis Razlikuju se po dodatnom parametru (tipa bool) scoped_lock
konstruktora: false – čitanje, true – upis (podrazumevano) Ključ za čitanje se može podići na nivo za pisanje
Metoda upgrade_to_writer U sl. primeru vektor se mora ponovo pretražiti, pošto
upgrade_to_writer može privremeno osloboditi ključ inače bi moglo doći do međusobnog blokiranja (deadlock) upgrade_to_writer vraća true, ako je oslobodila ključ
35
Međusobno isključivanje nitiPrimer mutex-i čitača-pisača
Pretpostavka: ključevi se dodaju na kraj i ne brišu se Dovoljno pretražiti elemente nakon onih originalno pretraženih
36
std::vector<string> MyVector;typedef spin_rw_mutex MyVectorMutexType;MyVectorMutexType MyVectorMutex;
void AddKeyIfMissing( const string& key ) {// Obtain a reader lock on MyVectorMutexMyVectorMutexType::scoped_lock
lock(MyVectorMutex,/*is_writer=*/false);size_t n = MyVector.size();for( size_t i=0; i<n; ++i )
if( MyVector[i]==key ) return;if( !MyVectorMutex.upgrade_to_writer() )
// Check if key was added while lock was temporarily releasedfor( int i=n; i<MyVector.size(); ++i )
if(MyVector[i]==key ) return;vector.push_back(key);
}
Međusobno isključivanje nitiPatologije ključeva
Međusobno blokiranje (deadlock); nastaje ako Postoji krug niti Svaka nit drži bar jedan ključ i čeka na mutex, koji je
zaključala druga nit Ni jedna nit ne želi da oslobodi svoje ključeve
Pravljenje konvoja (convoying) OS prekine nit koja drži ključ Druge niti moraju da sačekaju da prekinuta oslobodi ključ Kod fair mutex-a, još gore, čekaju nastavak prekinute
Izbegavanje konvoja Min vreme držanja ključa Koristiti atomske operacije umesto ključeva
37
Atomske operacijeDefinicija (1/2)
Druge niti vide atomske operacije kao trenutne Brže su od operacija sa ključevima Prednost: nema međusobnog zaključavanja niti konvoja Nedostatak: rade ograničen skup operacija Klasa atomic<T> implementira atomske operacije
Primer: referentno brojanje: akcija ako x==0 Sekvencijalno: --x; if(x==0) action() U sl. više niti, x postaje deljena promenljiva Smanjenje x i provera da li je 0 mora se uraditi neprekidivo x deklarisati kao atomic<int> i napisati if(--x==0) action()
Metod atomic<int>::operator-- dejstvuje atomatski
38
Atomske operacijeDefinicija (2/2)
atomic<T>, T int, enum ili pokazivač Pet osnovnih operacija nad x tipa atomic<T>
= x, pročitaj x x =, upiši vredost u x i vrati tu vrednost x.fetch_and_store(y), uradi x=y i vrati staru vrednost od x x.fetch_and_add(y), uradi x+=y i vrati staru vrednost od x x.compare_and_swap(y,z), ako je x==z, uradi x=y. Uvek
vrati staru vrednost od x
39
atomic<unsigned> counter;unsigned GetUniqueInteger() {
// return different int, until counter wraps aroundreturn counter.fetch_and_add(1);
}
Atomske operacijecompare_and_swap
compare_and_swap je osnova operacija za mnoge neblokirajuće algoritme Koristan idiom: ažuriranje globalx, zavisno od stare vrednosti Upozorenje: neke niti iteriraju, dok im druge ne zasmetaju
40
atomic<int> globalx;int UpdateX() { // Update x and return old value of x.
do {// Read globalXoldx = globalx;// Compute new valuenewx = ...expression involving oldx....// Store new value if another thread has not changed globalX.
} while( globalx.compare_and_swap(newx,oldx)!=oldx );return oldx;
}
Atomske operacijeABA problem
Prethodni idiom ne radi ispravno ako: Nit pročita vrednost A iz globalx Druga nit izmeni globalx od A na B na A Prva nit ne može da detektuje ovu promenu
Problem slabe memorijske konzistentnosi HW može promeniti redosleda operacija nad mem.
lokacijama radi povećanja efikasnosti Rešenje: arg. acquire/release za čitanje/upis objekata Primer: refcount.fetch_and_add<release>(-1);
Upis u deljeni objekat se mora završiti pre nego dođe do smanjenja refcount-a
41
Dodela memorije scalable_allocator<T> rešava problem skalabilnosti
Problem skalabilnosti se pojavljuje kada više niti dele zajednički memorijski bazen
Konvencionalan allocator: ekskluzivan pristup mem. bazenu scalable_allocator<T>: više niti istovremeno dobija objekte
tipa T
cache_aligned_allocator<T> rešava lažno deljenje linije bafera (cache) Nastaje kad dve niti pristupaju različitim rečima u liniji bafera Kad jedno jezgro promeni liniju, pa je promeni drugo jezgro,
linija se iz prvog prebacuje u drugo – nekoliko stotina taktova
Rešenje: cache_aligned_allocator<T> - garantuje da objekti koje napravi neće deliti istu liniju bafera
42
Raspoređivač zadatakaRealizuje paralelne aptrakcije visokog niova Nudi API, koji se može direktno koristiti Moguće je relizovati svoje apstrakcije visokog nivoa Treba koristiti logičke zadatke; prednosti:
Poklapanje paralelizma sa resursima, brže dizanje i spuštanje sistema, efikasniji redosled evaluacije, bolje uravnoteženje opterećenja, i razmišljanje na višem nivou
Programske niti se preslikavaju na fizička jezgra Krajnjosti pretplate: nedovoljna i prevelika Prevelika pretplata: dovodi do pada performanse zbog
deljenja vremena (time slicing) Raspoređivač pokušava da ima jednu nit na jednom jezgru
43
Raspoređivač zadatakaTBB zadaci
Zadaci su puno “lakši” od niti (brže se pokreću) Na OS Linux 18 puta, a na Windows više od 100 puta Zato što nit ima svoje registre, stek, ID Zadatak (task) u TBB je mala rutina
TTB zadaci jedan drugog ne istiskuju (preemption)
TBB raspoređivač je efikasan zato što nije pravedan On poseduje info o zadacima, tako da može da žrtvuje
pravednost za efikasnost Zadatak se pokreće tek kad može da učini koristan napredak
Raspoređivač uravnotežuje opterećenje (load balancing) Dovoljno je napraviti puno malih zadataka
Pri programiranju na nivou niti, programer sam rešava problem uravnoteženja opterećenja, koji nije trivijalan
44
Raspoređivač zadatakaPrimer: n-ti Fibonačijev niz (1/3)
Dat je serijski kod i paralelna verzija sa zadacima Veliki br. zadataka se najčešće prave mustrom rekurzivnog
zadataka
45
long SerialFib( long n ) {if( n<2 ) return n;else return SerialFib(n-1)+SerialFib(n-2);
}
long ParallelFib( long n ) {long sum;FibTask& a = *new(task::allocate_root()) FibTask(n,&sum);task::spawn_root_and_wait(a);return sum;
}
Raspoređivač zadatakaPrimer: n-ti Fibonačijev niz (2/3)
Funkcija ParallelFib 1. Dodeljuje prostor za zadatak, koristeći overloaded new
operator i metodu task::allocate_root (nema predaka) 2. Konstruiše zadatak konstruktorom FibTask(n,&sum) 3. Pokreće ga i čeka sa task::spawn_root_and_wait
Stvarni posao obavlja klasa FibTask
46
class FibTask: public task {public:
const long n;long* const sum;FibTask( long n_, long* sum_ ) :
n(n_), sum(sum_){}
//nastavak na drugom slajdu
Raspoređivač zadatakaPrimer: n-ti Fibonačijev niz (3/3)task* execute() { // Overrides virtual function task::execute
if( n<CutOff ) {*sum = SerialFib(n);
} else {long x, y;FibTask& a = *new( allocate_child() ) FibTask(n-1,&x);FibTask& b = *new( allocate_child() ) FibTask(n-2,&y);// Set ref_count to "two children plus one for the wait".set_ref_count(3);// Start b running.spawn( b );// Start a running and wait for all children (a and b).spawn_and_wait_for_all(a);// Do the sum*sum = x+y;
}return NULL; // pointer to next task to run
}47
Raspoređivač zadatakaGraf zadataka
Raspoređivač obrađuje graf zadataka Usmeren graf, svaki čvor je zadatak Svaki zadatak pokazuje na naslednika, koji ga čeka
Adresa naslednika se dobija sa task::parent()
Svaki zadatak ima refcount br. zadataka čiji je on naslednik dodaje se 1 za eksplicitno čekanje (wait)
Pimer grafa zadataka iz primera Fibonačijev niz A, B i C su izmrestili svoje potomke i čekaju ih D se izvršava, još nije izmrestio potomke E, F i G čekaju na izvršenje
48
Raspoređivač zadatakaPrimer grafa zadataka (Fibonačijev niz)
49
refcount=2
refcount=3 refcount=?
refcount=3 refcount=?
refcount=? refcount=?
zadatak A
zadatak B
zadatak C
zadatak D
zadatak G
zadatak F
zadatak E
Raspoređivač zadatakaAlgoritam raspoređivača (1/3)
Minimizira memorijske zahteve i komunikaciju niti Balansira izvršenje u dubinu i u širinu
(depth-first vs breadth-first)
Izvršenje u dubinu je najbolje za serijsko izvršenje Radi dok je bafer vruć. D je najnoviji, za njega je bafer
sigurno vruć; zatim nastavlja C, koji je noviji od A i B. Minimizira prostor. Kod izvršenja u širinu, koegzistira eksp.
br. zadataka. Kod izvršenja u dubinu, linearan, jer se ostali smeštaju na stek (E, F i G iz primera).
Iako izvršenje u širinu ima problem sa potrošnjom memorije, ono maksimizira paralelizam Koristiti dovoljno paralelizma, da jezgra budu upošljena
50
Raspoređivač zadatakaAlgoritam raspoređivača (2/3)
Hibrid izvršenja u dubinu i u širinu Svako jezgro ima svoj red sa dva kraja (deque)
Unutra su zadaci koji čekaju izvršenje
Kad zadatak izmresti naslednika, gurne ga na dno reda U primeru G je najstariji, a E najmlađi
Jezgro bira sledeći zadatak za izvršenje Onaj koji vrati metoda execute, ako nije NULL Onaj sa dna svog reda, ako nije prazan Krade zadatak sa vrha reda drugog slučajno izabranog jezgra
51
Raspoređivač zadatakaAlgoritam raspoređivača (3/3)
Tri uslova da jezgro gurne zadatak na dno reda Eksplicitno izmrešćen zadatak Zadatak označen za ponovno izvršenje
Metoda task::recycle_to_reexecute
Nakon izvršenja zadnjeg prethodnika refcount naslednika postaje 0 i jezgro gura naslednika na dno svog reda
Strategija: “Radi u dubinu, preuzimaj u širinu” “breadth-first theft and depth-first work” Izvršenje u širinu ide samo do potrebnog paralelizma Izvršenje u dubinu održava operativnu efikasnost, jednom
kada ima dovoljno posla
52
Raspoređivač zadatakaKorisne tehnike: pregled
Tehnike Rekurzivan lanac reakcija (Recursive Chain Reaction) Prosleđivanje nastavka (Continuation Passing) Zaobilaženje raspoređivača (Scheduler Bypass) Ponovna upotreba (Recycling) Prazni zadaci (Empty tasks) Opšti aciklični grafovi zadataka
53
Raspoređivač zadatakaKorisne tehnike (1/7)
Rekurzivan lanac reakcija Najbolja performansa za grafove u obliku stabala zadataka
Direktno stvaranje N zadataka O(N), kroz stablo O(lg(N))
Iteracioni prostor petlje se sa parallel_for može preslikati na binarno stablo zadataka
Prosleđivanje nastavka Predak uobičajeno čeka na svoje potomke
metoda spawn_and_wait_for_all
Ali, predak može da preda svoje potomke zadatku nastavka (continuation task) Besposleno jezgro može da preuzme (ukrade) zadatak
nastavka
54
Raspoređivač zadatakaKorisne tehnike (2/7)
allocate_continuation sličan allocate_child Usmerava successor od this na c, i postavlja successor od this
na NULL
55
} else {FibContinuation& c =
*new( allocate_continuation() ) FibContinuation(sum);FibTask& a = *new( c.allocate_child() ) FibTask(n-2,&c.x);FibTask& b = *new( c.allocate_child() ) FibTask(n-1,&c.y);// Set ref_count to "two children plus one for the wait".c.set_ref_count(2); // 2 instead of 3!!!spawn( b );spawn( a );return NULL;
}
Raspoređivač zadatakaKorisne tehnike (3/7)
Zaobilaženje raspoređivača Direktno se specificira sledeći zadatak za izvršenje Prosleđivanje nastavka obično otvara prostor za zaobilaženje
raspoređivača Npr. Rešenje ispod izbegava nepotrebno manipulisanje
stekom, kao i neželjeno preuzimanje zadatka od drugog jezgra
56
...FibTask& a = *new( c.allocate_child() ) FibTask(n-2,&c.x);FibTask& b = *new( c.allocate_child() ) FibTask(n-1,&c.y);// Set ref_count to "two children".c.set_ref_count(2);spawn( b );spawn( a );return &a; // instead of the return NULL;...
Raspoređivač zadatakaKorisne tehnike (4/7)
Ponovna upotreba (zadatka) Izbegava oslobađanje i (ponovnu) dodelu zadatka Ako se u prethodnom rešenju zadatak “a” zameni predkom
Umesto potomka “a” koristi se ponovno upotrebljen this Brišu se const atributi i return &a zameni sa retun this Dodaje se poziv recycle_as_child_of(c);
Označava da this ne treba obrisati po završetku execute Postavlja naslednika od this na c (continuation task)
57
Raspoređivač zadatakaKorisne tehnike (5/7)
Prazni zadaci, empty_task Neki put je samo potreban zadatak koji čeka potomke Primer u parallel_for.h, metoda start_for::execute
Pravi dva potomka i koristi prazan zadatak koji ih čeka, gornji parallel_for čeka na prazan zadatak
Opšti aciklični grafovi zadataka Do sada razmatrana stabla gde svaki zadatak ima jednog
naslednika task::parent() Moguće su složenije strukture, npr. svaki zadatak zavisi od
zadatka od gore i s leva
58
Raspoređivač zadatakaKorisne tehnike (6/7)const int M=3, N=4;class DagTask: public tbb::task {public:
const int i, j;double input[2]; // input[0] = sum from above, input[1] = sum from leftdouble sum;// successor[0] = successor below, successor[1] = successor to rightDagTask* successor[2];DagTask( int i_, int j_ ) : i(i_), j(j_) { input[0] = input[1] = 0; }task* execute() {
__TBB_ASSERT( ref_count()==0, NULL );sum = i==0 && j==0 ? 1 : input[0]+input[1];for( int k=0; k<2; ++k )
if( DagTask* t = successor[k] ) {t->input[k] = sum;if( t->decrement_ref_count()==0 )
spawn( *t );}
return NULL;}
}; 59
Raspoređivač zadatakaKorisne tehnike (7/7)double BuildAndEvaluateDAG() {
DagTask* x[M][N];for( int i=M; --i>=0; )
for( int j=N; --j>=0; ) {x[i][j] = new( tbb::task::allocate_root() ) DagTask(i,j);x[i][j]->successor[0] = i+1<M ? x[i+1][j] : NULL;x[i][j]->successor[1] = j+1<N ? x[i][j+1] : NULL;x[i][j]->set_ref_count((i>0)+(j>0));
}// Add extra reference to last taskx[M-1][N-1]->increment_ref_count();// Wait for all but last task to complete.x[M-1][N-1]->spawn_and_wait_for_all(*x[0][0]);// Last task is not executed implicitly, so execute it explicitly.x[M-1][N-1]->execute();double result = x[M-1][N-1]->sum;// Destroy last task.task::destroy(*x[M-1][N-1]);return result;
}60
Recommended