This shows you the differences between two versions of the page.
|
asc:laboratoare:03 [2021/02/16 20:40] 127.0.0.1 external edit |
asc:laboratoare:03 [2026/02/23 18:46] (current) giorgiana.vlasceanu |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Laboratorul 03 - Programare concurentă în Python (continuare) ====== | + | ====== Laboratorul 03 - Tehnici de Optimizare de Cod – Inmultirea Matricelor ====== |
| ===== Obiective ===== | ===== Obiective ===== | ||
| - | Vom continua în cadrul acestui laborator prezentarea elementelor de sincronizare oferite de Python. | + | In acest laborator vom exemplifica o serie de optimizari de cod pe una dintre cele mai simple, si in acelasi timp utilizate probleme, si anume, inmultirea matricelor. |
| - | ===== Obiecte de sincronizare ===== | + | ==== De ce inmultirea matricelor? ==== |
| - | ==== Event ==== | + | Este o operatie fundamentala si elementara in algebra liniara ce serveste la rezolvarea unui numar extrem de mare de probleme, cum ar fi: rezolvarea sistemelor liniare de ecuatii in majoritatea domeniilor stiintifice si economice (operatiile cu matrice sunt practic prezente pretudindeni); calcule si operatii cu grafuri; inversari de matrice. Problema inmultirii matricelor este in mod cert cea mai bine studiata problema in HPC (High Performance Computing), ea beneficiind de o multitudine de algoritmi inteligenti si implementari performante pe toate arhitecturile existente astazi. Pentru a simplifica lucrurile, in acest laborator ne vom ocupa doar de inmultirea matricelor patratice. |
| - | //[[http://docs.python.org/3/library/threading.html#event-objects | Event]]//-urile sunt obiecte simple de sincronizare care permit mai multor thread-uri blocarea voluntară până la apariția unui eveniment semnalat de un alt thread (ex: o condiție a devenit adevărată). Intern, un obiect //Event// conține un flag setat inițial la valoarea //false//. El oferă următoarele operații: | + | ===== Cel mai simplu algoritm ===== |
| - | * //[[http://docs.python.org/3/library/threading.html#threading.Event.set | set()]]// - care setează valoarea flag-ului pe //true// | + | |
| - | * //[[http://docs.python.org/3/library/threading.html#threading.Event.wait | wait()]]// - care blochează execuția thread-urilor apelante până când flag-ul devine //true//. Dacă flag-ul este deja //true// în momentul apelării lui //wait()// aceasta nu va bloca execuția. În momentul setării flag-ului pe //true// toate thread-urile blocate în //wait()// vor fi deblocate și își vor continua execuția. | + | |
| - | * //[[http://docs.python.org/3/library/threading.html#threading.Event.clear | clear()]]// - setează flag-ul intern la valoarea //false// (resetează evenimentul) | + | |
| - | * //[[http://docs.python.org/3/library/threading.html#threading.Event.is_set | is_set()]]// - care oferă posibilitatea de interogare a valorii curente a flag-ului. | + | |
| - | <note important> | + | Intuitiv, cel mai simplu algoritm, urmeaza formularea matematica: |
| - | Testarea valorii flag-ului cu //is_set()// într-o buclă, fără a executa calcule utile în acea buclă sau fără a apela metode blocante, reprezintă o formă de //busy-waiting//. Această utilizare trebuie evitată, deoarece, ca orice //busy-waiting//, irosește timp de procesor care ar putea fi folosit de celelate thread-uri. | + | {{:asc:lab5:cij.jpg|}} |
| - | </note> | + | |
| + | Matricele A = [aij], i,j=1,...,N si B = [bij], i,j=1,...,N sunt salvate ca vectori bidimensionali de marime N x N. Matricea rezultat C = A x B = [cij], i,j=1,...,N, avand fireste aceeasi dimensiune. | ||
| - | <note important> | + | {{:asc:lab5:axb_c.jpg|}} |
| - | Refolosirea obiectelor //Event// pentru a semnaliza un eveniment (cu metoda //clear()//) trebuie făcută cu grijă. A doua setare a flag-ului pe //true// poate fi ștearsă de un //clear()// întârziat, înainte ca thread-ul care dorește să aștepte evenimentul să facă //wait()// pentru a doua oară. Acest lucru va duce cel mai probabil la deadlock. | + | |
| - | </note> | + | |
| - | <note important> | + | Cum este si de asteptat, similar cu majoritatea operatiilor din algebra liniara, formula de mai sus se transforma in urmatorul program extrem de simplu: |
| - | O altă problemă care poate apărea în cazul refolosirii unui obiect //Event// este că un thread care dorește o a doua așteptare și-ar putea continua execuția, fără ca evenimentul să fie semnalizat a doua oară. Această situație apare atunci când resetarea flag-ului cu metoda //clear()// este întârziată mai mult decât al doilea //wait()//. Evenimentul va rămâne astfel la valoarea //true//, iar al doilea //wait()// nu va bloca execuția în acestă situație precum se dorește. | + | |
| - | </note> | + | |
| - | Capabilitatea obiectului //Event// de a bloca execuția thread-urilor și de a le debloca pe toate în același timp poate fi folosită în locul semaforului, ca **mecanism de blocare/deblocare**, la implementarea unei bariere ne-reentrante. Avantajul acestei soluții față de cea cu semafor este claritatea. //Event//-ul se ocupă de blocarea/deblocarea thread-urilor, iar contorul și //lock//-ul țin evidența thread-urilor intrate în barieră. Soluția cu semafor conține însă două contoare (unul dat de semaforul însuși), care trebuie să rămână corelate, iar **mecanismul de blocare/deblocare** este combinat cu **contorul**, complicând astfel analiza implementării. | + | <code cpp> |
| + | int i,j,k; | ||
| + | double a[N][N], b[N][N], c[N][N]; | ||
| + | // initializarea matricelor a si b | ||
| + | for (i=0;i<N;i++){ | ||
| + | for (j=0;j<N;j++){ | ||
| + | c[i][j] = 0.0; | ||
| + | for (k=0;k<N;k++){ | ||
| + | c[i][j] += a[i][k] * b[k][j]; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| - | <code python simple_barrier_event.py> | + | <note important> |
| - | from threading import * | + | **Cat de bun este acest algoritm?** |
| - | class SimpleBarrier(): | + | Algoritmul este bun pentru ca: |
| - | def __init__(self, num_threads): | + | * Se poate specifica in doar cateva linii; |
| - | self.num_threads = num_threads | + | * Este o mapare directa a formulei de calcul pentru Cij (din algebra liniara); este usor de inteles si de urmarit de catre oricine poseda cunostinte minime de matematica; |
| - | self.count_threads = self.num_threads # contorizeaza numarul de thread-uri ramase | + | * In sfarsit, in mod sigur nu contine bug-uri datorita simplitatii extreme pe care o manifesta algoritmul! |
| - | self.count_lock = Lock() # protejeaza accesarea/modificarea contorului | + | |
| - | self.threads_event = Event() # blocheaza thread-urile ajunse | + | |
| - | def wait(self): | + | Algoritmul este prost pentru ca: |
| - | with self.count_lock: | + | * Are performante extrem de reduse! |
| - | self.count_threads -= 1 | + | |
| - | if self.count_threads == 0: # a ajuns la bariera si ultimul thread | + | |
| - | self.threads_event.set() # deblocheaza toate thread-urile | + | |
| - | self.threads_event.wait() # num_threads-1 threaduri se blocheaza aici | + | |
| - | # ultimul thread nu se va bloca deoarece event-ul a fost setat | + | |
| - | class MyThread(Thread): | + | De aceea ne vom ocupa in acest laborator de optimizarea acestei operatii din punctul de vedere al performantei. |
| - | def __init__(self, tid, barrier): | + | |
| - | Thread.__init__(self) | + | |
| - | self.tid = tid | + | |
| - | self.barrier = barrier | + | |
| - | def run(self): | + | </note> |
| - | print ("I'm Thread " + str(self.tid) + " before\n") | + | |
| - | self.barrier.wait() | + | |
| - | print ("I'm Thread " + str(self.tid) + " after barrier\n") | + | |
| + | ===== Optimizarea algoritmului de inmultire a doua matrice ===== | ||
| + | |||
| + | ==== Detectarea constantelor din bucle ==== | ||
| + | |||
| + | Prima optimizare, consta in a observa ca c[i][j] este o constanta in cadrul ciclului interior k. Totusi, pentru un compilator acest fapt nu este neaparat evident deoarece c[i][j] este o referinta in cadrul unui vector. Astfel, o prima optimizare va arata asa: | ||
| + | |||
| + | <code cpp> | ||
| + | for (i=0;i<N;i++){ | ||
| + | for (j=0;j<N;j++){ | ||
| + | register double suma = 0.0; | ||
| + | for (k=0;k<N;k++) { | ||
| + | suma += a[i][k] * b[k][j]; | ||
| + | } | ||
| + | c[i][j] = suma; | ||
| + | } | ||
| + | } | ||
| </code> | </code> | ||
| - | Bariera obținută cu un singur obiect //Event// este însă ne-reentrantă. Încercări de transformare a acestei implementări într-o barieră reentrantă, prin resetarea evenimentului cu metoda //clear()//, vor duce fie la deadlock, fie la o barieră reentrantă care nu funcționează corect în momentul întârzierii apelului //clear()// (problemele care pot apărea la reutilizarea a unui obiect //Event// sunt exemplificate mai sus). O barieră reentrantă poate fi însă ușor implementată cu două obiecte //Event//, asemănător cu folosirea a două semafoare. | + | In acest mod, compilatorul va putea avea grija ca variabila suma sa fie tinut intr-un registru, permitand astfel o utilizare optima a acestei resurse. Astfel utilizarea keyword-ului "register" este util de folosit ca hint pentru compilator, atunci cand socotiti ca acest lucru este util. |
| - | ==== Condition ==== | + | ==== Accesul la vectori ==== |
| - | //[[http://docs.python.org/3/library/threading.html#condition-objects | Condition]]// (sau variabilă condiție) este un obiect de sincronizare care permite mai multor thread-uri blocarea voluntară până la apariția unei condiții semnalate de un alt thread, asemenător //Event-urilor//. Spre deosebire de acestea însă, un obiect //Condition// oferă un set de operații diferit și este asociat întotdeauna cu un //lock//. Lock-ul este creat implicit la instanțierea obiectului //Condition// sau poate fi pasat prin intermediul constructorului dacă mai multe obiecte //Condition// trebuie să partajeze același lock. | + | Un alt aspect care necesita resurse din plin, este utilizarea si accesul variabilelor de tip vectorial. De fiecare data cand programul face o referinta la un obiect de tipul X[i][j][k] compilatorul trebuie sa genereze expresii aritmetice complexe, pentru a calcula aceasta adresa, in cadrul vectorului muldimensional X. De exemplu, iata cum arata un vector bidimensional in limbajul C (salvat row-major): |
| - | Un obiect //Condition// oferă operațiile: | + | {{:asc:lab5:aij.jpg|}} |
| - | * //[[http://docs.python.org/3/library/threading.html#threading.Condition.acquire | acquire()]]// - blochează lock-ul | + | |
| - | * //[[http://docs.python.org/3/library/threading.html#threading.Condition.release | release()]]// - eliberează lock-ul | + | |
| - | * //[[http://docs.python.org/3/library/threading.html#threading.Condition.wait | wait()]]// | + | |
| - | * va bloca thread-ul apelant până la semnalizarea condiției prin notify de către alt thread | + | |
| - | * apelul wait realizează următorii pași în mod atomic: | + | |
| - | * eliberează lock-ul | + | |
| - | * așteaptă | + | |
| - | * blochează lock-ul atunci când thread-ul e trezit | + | |
| - | * //[[http://docs.python.org/3/library/threading.html#threading.Condition.notify | notify()]]//, //[[http://docs.python.org/3/library/threading.html#threading.Condition.notify_all | notify_all()]]// - deblochează un singur thread, respectiv pe toate. | + | |
| - | :!: Operațiile wait(), notify() și notify_all() trebuie întotdeauna apelate doar după blocarea prealabilă a lock-ului asociat. | + | Astfel, pentru ''N = 6, M = 4: @a[2][3] = @a[0][0] + 2*6 + 3 = @a[0][0] + 15'' |
| - | <note important> | + | In limbaje de programare ca FORTRAN-ul, formula este inversata, deoarece aceste limbaje salvează vectorii în format column-major: |
| - | Cele trei operații: //wait()//, //notify()// și //notify_all()// vor lăsa lock-ul asociat în starea blocat, deblocarea acestuia făcându-se manual cu metoda //release()//. De remarcat că după un //notify()// sau //notify_all()//, thread-urile blocate în //wait()// nu vor continua imediat, ele trebuind să aștepte până când lock-ul asociat devine și el disponibil. | + | |
| - | </note> | + | |
| - | <note> | + | ''@a[i][j] = @a[0][0] + j*M + i'' |
| - | Funcționarea unui //Condition// este asemănătoare cu a monitorului asociat fiecărui obiect Java, metodele //wait()//, //notify()// și //notify_all() / notifyAll()// având aceeași semantică în ambele limbaje. Apelarea metodelor //acquire()// și //release()// este înlocuită în Java de blocul //synchronize//. Pentru o asemănare mai mare cu Java, în Python puteți folosi instrucțiunea //with// pentru apelarea automată a metodelor //acquire()// și //release()//, precum în exemplul următor: | + | |
| - | ^ Java ^ Python ^ | + | Oricare ar fi asezarea vectorilor in memorie, accesele la vectori sunt scumpe din punctul de vedere al performantelor. Noi vom considera de aici inainte o asezare row-major, ca in limbajul C. Conform acestei formule, pentru vectori bidimensionali (matrice), fiecare acces presupune doua adunari si o inmultire (de numere intregi). Evident, pentru vectori cu mai multe dimensiuni, aceste costuri cresc considerabil. Astfel, in momentul in care compilatorul intalneste instructiunea: |
| - | | <code java> | + | |
| - | synchronize(c) { | + | ''suma += a[i][k] * b[k][j]'' |
| - | while(!check()) | + | |
| - | c.wait(); | + | se vor efectua implicit, suplimentar inmultiri si adunari in virgula mobila implicata de codul de mai sus, patru adunari si doua inmultiri in numere intregi pentru a calcula adresele necesare din vectorii a si b. Se intampla astfel destul de frecvent ca procesorul sa nu aiba date disponibile pentru a lucra in continuu, din cauza faptului ca overhead-ul pentru calculul adreselor este semnificativ. |
| + | |||
| + | Astfel, un mod de a spori viteza programului este renuntarea la accesele vectoriale prin derefentiere utilizand in acest scop pointeri. De exemplu: | ||
| + | |||
| + | <code cpp> | ||
| + | for (j=0;j<N;j++) | ||
| + | a[i][j] = 2; // 2*N adunari si N inmultiri | ||
| + | </code> | ||
| + | |||
| + | se va inlocui cu: | ||
| + | |||
| + | <code cpp> | ||
| + | double *ptr=&(a[i][0]); // 2 adunari si o inmultire | ||
| + | for (j=0;j<N;j++) { | ||
| + | *ptr = 2; | ||
| + | ptr++; // N adunari in numere intregi | ||
| } | } | ||
| - | </code> | <code python> | + | </code> |
| - | with c: | + | |
| - | while(not check()): | + | In mod similar se procedeaza si pentru cazul in care indexul incrementat este cel al liniilor si nu cel al coloanelor. In ambele cazuri, practic se va calcula "de mana" adresa in cadrul vectorului, exact in modul in care ar face-o compilatorul limbajului folosit. Totusi, rezolvarea noastra este mai rapida, deoarece ea tine cont de pozitia in care ne aflam in cadrul vectorului, lucru destul de complicat de facut automat. De exemplu, pentru a trece la urmatoarea coloana, e suficient sa adunam N pointer-ului, fata de recalcularea pornind de la @(a[0][0]) ce necesita doua adunari si o inmultire in intregi. Evident, facilitatile oferite de limbaje ca C-ul, ne vin in ajutor: astfel incrementarile de pointeri de tip char * vor face incrementarea cu un byte, in vreme ce pentru int * se va face cu patru bytes. Ca urmare a aspectelor prezentate mai sus, iata forma optimizata in care ajunge algoritmul nostru: |
| - | c.wait() | + | |
| - | + | <code cpp> | |
| - | </code> | | + | for(i = 0; i < N; i++){ |
| + | double *orig_pa = &a[i][0]; | ||
| + | for(j = 0; j < N; j++){ | ||
| + | double *pa = orig_pa; | ||
| + | double *pb = &b[0][j]; | ||
| + | register double suma = 0; | ||
| + | for(k = 0; k < N; k++){ | ||
| + | suma += *pa * *pb; | ||
| + | pa++; | ||
| + | pb += N; | ||
| + | } | ||
| + | c[i][j] = suma; | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | <note tip>Atentie! Codul de mai sus va da rezultate corecte doar daca matricile sunt declarate global sau pe stivă pentru că în felul acesta sunt stocate continuu în memorie (și are sens pb += N). Dacă alocați dinamic, atunci folosiți matrici liniarizate și adaptați acest cod pentru cazul lor.</note> | ||
| + | |||
| + | <note important> | ||
| + | Din primele doua optimizari se pot desprinde cateva concluzii. Prima ar fi ca optimizarea unui cod (din punct de vedere al performantelor), presupune utilizarea a cat mai putine constructii complexe (high-level), puse la dispozitie de limbajul folosit. Aceasta concluzie poate suna extrem de ciudat pentru cineva care porneste de la ideea ca facilitatile limbajelor de programare sunt acolo pentru a fi folosite. Da, este adevarat acest lucru, insa atunci cand vrei performanta, trebuie sa stii ce constructii sa eviti! Astfel, apare concluzia a doua: vectorii sunt concepte mai abstracte decat pointerii (ca implementare), asadar, utilizati pointeri cand vreti viteza. Viteza crescuta insa, va fi obtinuta cu pretul unui cod mult mai dificil de urmarit si de inteles, mai rau, mult mai greu de debug-at. Un cod complex si performant, de multe ori poate contine bug-uri extrem de subtile si greu de depistat. Asadar, e util sa stii exact ceea ce faci cand incepi sa faci astfel de optimizari! | ||
| </note> | </note> | ||
| - | Un obiect //Condition// este util atunci când pe lângă semnalizarea unei condiții este necesar și un lock pentru a sincroniza accesul la o resursă partajată. În acest caz, un obiect //Condition// este de preferat unui //Event// deoarece oferă acest lock în mod implicit, revenirea din //wait()// în momentul semnalizării condiției făcându-se cu lock-ul blocat. | + | ---- |
| - | ==== Queue ==== | + | ==== Activitate practica - Optimizare constantelor si al accesului la vectori ==== |
| - | Cozile sincronizate sunt implementate în Python în modulul [[http://docs.python.org/3/library/queue.html|Queue]] în clasele //Queue//, //LifoQueue// și //PriorityQueue//. Obiectele de aceste tipuri sunt folosite pentru implementarea comunicării între threaduri, după modelul producători-consumatori. | + | Intrebarea este acum: aduc ceva imbunatatiri optimizarile 1 si 2? Pentru a afla raspunsul la aceasta intrebare, va invitam sa implementati problema, cu optimizarile sugerate, si sa observati singuri ce se intampla. |
| - | Metodele oferite de aceste clase permit adăugarea și scoaterea de elemente într-un mod sincronizat (//[[https://docs.python.org/3/library/queue.html#Queue.Queue.put|put(item)]]// și //[[https://docs.python.org/3/library/queue.html#Queue.Queue.get|get()]]//) și interogarea stării cozii (//[[https://docs.python.org/3/library/queue.html#Queue.Queue.empty|empty()]]//, //[[https://docs.python.org/3/library/queue.html#Queue.Queue.qsize|qsize()]]// și //[[https://docs.python.org/3/library/queue.html#Queue.Queue.full|full()]]//). În plus față de acestea, putem implementa ușor modelul master-worker folosind metodele //[[https://docs.python.org/3/library/queue.html#Queue.Queue.task_done|task_done()]]// și //[[https://docs.python.org/3/library/queue.html#Queue.Queue.join|join()]]//, ca în [[https://docs.python.org/3/library/queue.html#Queue.Queue.join|exemplul]] din documentație. | + | ---- |
| - | ===== ThreadPoolExecutor ===== | + | ==== Optimizarea pentru accesul la memorie ==== |
| - | Python oferă începând cu versiunea 3.2 implementări pentru Executors și pool-uri de thread-uri sau de procese, în modulul [[https://docs.python.org/3/library/concurrent.futures.html?|concurrent.futures]]. Un alt modul care oferă obiecte pentru pool-uri este [[https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing|multiprocessing]] pentru pool-uri de procese și [[https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing.dummy|multiprocessing.dummy]] pentru pool-uri de threaduri. Pentru lucrul asincron pe thread-uri recomandăm însă obiectele din concurrent.futures. | + | Dupa cum ar trebui sa va fie destul de evident pana acum, din experienta voastra de programatori, memoria este in general cel mai problematic bottleneck. Optimizarile prezentate mai sus reduc timpul de executie intr-o oarecare masura, insa ele nu schimba in nici un fel modul in care memoria este accesata in cadrul algoritmului. Cu alte cuvinte, aceleasi locatii de memorie sunt accesate in aceeasi ordine, indiferent daca am operat sau nu optimizarile prezentate. O intrebare interesanta ar fi acum: ce se intampla, daca am schimba ordinea in care se executa buclele? S-ar obtine performante diferite? |
| + | Pentru problema noastra, care contine trei bucle, exista asadar sase secvente posibile, si anume: i-j-k, i-k-j, j-i-k, j-k-i, k-i-j, si k-j-i. Fiecare dintre aceste secvente corespunde unui tip diferit de acces la memorie pentru matricele considerate. Deoarece bucla interioara este cea mai des executata, ne vom concentra acum atentia un pic asupra ei. Operatia executata acolo ramane: | ||
| - | Ca și în alte limbaje (e.g. [[https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/concurrent/Executor.html|Java]]) folosirea unui Executor sau thread pool este modalitatea recomandată pentru lucrul asincron pe thread-uri. Avantajul principal este că elimină din overhead-ul creării și distrugerii de noi threaduri. Atunci când aveți de rulat un task asincron, de exemplu o operație I/O sau un call către un server, în loc să vă creați propriul thread, submiteți unui Executor funcția respectivă (un callable). | + | ''c[i][j] += a[i][k] * b[k][j]'' |
| - | Exemple cod pentru ThreadPoolExecutor găsiți chiar în [[https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor|documentație]] | + | Pentru fiecare dintre cele trei matrice, a, b si c, fiecare element poate fi accesat in trei moduri diferite, si anume: |
| + | * Constant: accesul nu depinde de indexul buclei interioare | ||
| + | * Secvential: accesul la memorie este contiguu (adica in celule succesive de memorie) | ||
| + | * Nesecvential: accesul la memorie nu este contiguu (celulele de memorie logic succesive, sunt de fapt adresate cu pauze de dimensiune N) | ||
| - | Pentru a submite joburi către ThreadPoolExecutor se folosesc metodele moștenite din clasa **Executor**: | + | Astfel, pentru cele sase configuratii, se obtine: |
| - | * [[https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit|submit]] - dă spre execuție un callable | + | |
| - | * [[https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map|map]] - aplică o funcție pe o structură de date iterabilă, cum se poate observa în exemplul următor | + | |
| - | <code Python| threadpool_example.py> | + | ^ Loop order ^ c[i][j] += ^ a[i][k] ^ * b[k][j] ^ |
| - | from concurrent.futures import ThreadPoolExecutor | + | | i-j-k: | Constant | Secvential | Nesecvential | |
| - | from concurrent.futures import as_completed | + | | i-k-j: | Secvential | Constant | Secvential | |
| - | from threading import current_thread | + | | j-i-k: | Constant | Secvential | Nesecvential | |
| - | import time, random | + | | j-k-i: | Nesecvential | Nesecvential | Constant | |
| + | | k-i-j: | Secvential | Constant | Secvential | | ||
| + | | k-j-i: | Nesecvential | Nesecvential | Constant | | ||
| - | data = ["lab1", "lab2", "lab3"] | + | Care sunt totusi, comparativ, performantele celor trei moduri de acces? In mod clar, accesul constant este mai bun decat cel secvential – aceste constante in cadrul unor bucle, sunt in general puse in registri, ducand la imbunatatirea performantelor algoritmului, dupa cum s-a aratat in optimizarea 1. Accesul secvential la randul sau, este mai bun decat cel nesecvential, in principal pentru ca utilizeaza considerabil mai bine cache-ul. |
| - | def modify_msg(msg): | + | <note important> |
| - | time.sleep(random.randint(1,5)) | + | Luand in considerare aceste observatii, putem concluziona ca: |
| - | return "Completed: [" + msg.title() + "] in thread " + str(current_thread()) | + | * Configuratiile k-i-j si i-k-j ar trebui sa aiba cele mai bune performante |
| + | * Configuratiile i-j-k si j-i-k ar trebui sa fie mai proaste decat primele, si | ||
| + | * Configuratiile j-k-i si k-j-i ar trebui sa fie cele mai proaste! | ||
| + | </note> | ||
| - | def main(): | + | ==== Activitate practica - Ordinea buclelor ==== |
| - | with ThreadPoolExecutor(max_workers = 2) as executor: | + | |
| - | results = executor.map(modify_msg, data) | + | |
| - | for result in results: | + | ---- |
| - | print(result) | + | |
| - | if __name__ == '__main__': | + | Efectiv, care este adevarul? Construiti singuri aceste scenarii si analizati aceasta problema! |
| - | main() | + | |
| + | ---- | ||
| + | |||
| + | Pentru a studia mai in detaliu problema, sa analizam un pic configuratia i-j-k (desi nu este cea mai buna configuratie, cum vedem de mai sus): | ||
| + | |||
| + | <code cpp> | ||
| + | for (i=0;i<N;i++){ | ||
| + | for (j=0;j<N;j++){ | ||
| + | sum=0; | ||
| + | for (k=0;k<N;k++) | ||
| + | sum+=a[i][k]*b[k][j]; | ||
| + | c[i][j] = sum; | ||
| + | } | ||
| + | } | ||
| </code> | </code> | ||
| - | Outputul programului va fi | + | Cate cache-miss-uri sunt generate in acest algoritm, cu aceasta secventa de acces la memorie? In mod evident, aceasta nu este o intrebare usoara. De exemplu: daca fiecare matrice ar fi de doua ori mai mare decat cache-ul, ar avea loc multe incarcari si eliberari de linii, ducand astfel la o formula complicata. Astfel, cel mai simplu aproximam, si consideram ca dimensiunea matricei este mult mai mare decat cea a Cache-ului. Astfel, fie C, numarul de elemente din matrice ce intra in Cache. |
| - | <code> | + | Astfel, considerand algoritmul de mai sus (fara optimizarea pentru constante): |
| - | Completed: [Lab1] in thread <Thread(ThreadPoolExecutor-0_0, started daemon 123145477799936)> | + | |
| - | Completed: [Lab2] in thread <Thread(ThreadPoolExecutor-0_1, started daemon 123145494589440)> | + | <code cpp> |
| - | Completed: [Lab3] in thread <Thread(ThreadPoolExecutor-0_0, started daemon 123145477799936)> | + | for (i=0;i<N;i++){ |
| + | // Citeste linia i pt a in Cache (Ra) | ||
| + | // Scrie linia i a lui c in Memorie (Wc) | ||
| + | for (j=0;j<N;j++){ | ||
| + | // Citeste coloana j a lui b in Cache (Rb) | ||
| + | for (k=0;k<N;k++){ | ||
| + | c[i][j] += a[i][k] * b[k][j]; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| </code> | </code> | ||
| - | ===== Studiu de caz - Bariera ===== | ||
| - | De multe ori este necesar ca un grup de thread-uri să ajungă toate într-un anumit punct al execuției (ex: fiecare thread a calculat un rezultat intermediar al algoritmului) și numai după aceea să își continue execuția (ex: rezultatele intermediare sunt partajate de toate thread-urile în partea următoare a algoritmului). Mecanismul de sincronizare potrivit pentru asemenea situații este **bariera**. | + | Astfel, daca L este dimensiunea unei linii de Cache: pentru (Ra) obtinem aproximativ N*(N/L) cache-miss-uri, pentru (Wc) la fel, iar pentru (Rb) un dezastruos N*N*N! Acest lucru se intampla deoarece, desi accesul la b este secvential intr-o coloana, matricea este salvata in memorie utilizand row-major! Concluzia este descurajatoare: 2N<sup>2</sup>/L + N<sup>3</sup> -> N<sup>3</sup> cache-miss-uri! Se adauga la acest aspect si cele 2N<sup>3</sup> operatii aritmetice, si se ajunge la raportul: operatii aritmetice / operatii cu memoria -> 2. Acest lucru este extrem de rau, deoarece noi stim de la (curs) si de la alte materii, ca arhitecturile calculatoarelor NU sunt echilibrate, si ca operatiile aritmetice sunt de ordine de marime mai rapide decat operatiile cu memoria. De aceea, memoria ramane in continuare bottleneck-ul pentru aceasta implementare a inmultirii de matrice. Pentru a obtine performante mai bune, este necesara obtinerea unui raport considerabil mai mare. |
| - | Începând cu Python 3.2 în modulul //threading// a fost introdusă clasa //[[https://docs.python.org/3/library/threading.html#barrier-objects|Barrier]]//, acesta fiind o barieră reentrantă implementată folosind variabile condiție ([[https://github.com/python/cpython/blob/master/Lib/threading.py#L580|cod sursă]]). În această secțiune vom prezenta două variante pentru implementarea unui astfel de obiect. | + | Cum se face insa, ca pentru N<sup>2</sup> elemente intr-o matrice, ajungem la N<sup>3</sup> cache-miss-uri? Pai am stabilit ca acest lucru se datoreaza accesului ineficient al lui b, deoarece se incearca incarcarea coloana cu coloana a matricei! |
| - | Ce trebuie să ofere o barieră? | + | Concluzia acestei analize este ca nu putem spune, doar dupa numarul de operatii efectuate si dimensiunea datelor folosite, daca un algoritm va suferi sau nu din cauza unui bottleneck la memorie. |
| - | * un **mecanism de blocare** a thread-urilor - [[https://docs.python.org/3/library/threading.html#threading.Barrier.wait|Barrier#wait()]] | + | |
| - | * un **contor** al numărului de thread-uri care au ajuns/mai trebuie să ajungă la barieră - [[https://docs.python.org/3/library/threading.html#threading.Barrier.n_waiting|Barrier#n_waiting]] | + | |
| - | * **deblocarea** tuturor thread-urilor atunci când a ajuns și ultimul dintre ele la barieră | + | |
| + | Solutia este: utilizarea mai ingenioasa a cache-ului. | ||
| - | ==== Barieră - varianta semafor ==== | + | Acest lucru se poate realiza prin reorganizarea operatiilor din cadrul inmultirii de matrice pentru a obtine mai multe cache-hit-uri. Faptul ca adunarea si inmultirea sunt atat operatii asociative, cat si comutative face posibila aceasta reordonare a operatiilor. Acesta este un subiect de cercetare asupra caruia si-au indreptat atentia numerosi cercetatori de-a lungul timpului, generand o multitudine de algoritmi si de teoreme matematice care sa ii sustina. In orice caz, daca vom considera r = raportul intre operatiile aritmetice si operatiile la memorie (cu cache-miss-uri), este evident ca se doreste un r maxim, pentru a elimina bottleneck-ul de la memorie. S-a aratat ca orice reorganizare a acestui algoritm este limitata la r = O(sqrt(C)), unde C este dimensiunea Cache-ului (in numar de elemente ce intra in Cache). Acest lucru arata ca r nu scaleaza cu dimensiunea matricei N, indiferent de impartirea intuitiva a lui 2N<sup>3</sup> la N<sup>2</sup>... |
| - | === Bariera ne-reentrantă === | + | ==== Solutia: “Blocked Matrix Multiplication” ==== |
| - | Putem implementa o barieră folosind un semafor inițializat cu 0 (**mecanismul de blocare/deblocare**) și un contor al numărului de thread-uri care mai trebuie să ajungă la barieră (**contorul**), inițializat cu numărul de thread-uri utilizate. Semaforul este folosit pentru a bloca execuția thread-urilor. Contorul este decrementat de fiecare thread care ajunge la barieră și reprezintă numărul de thread-uri care au mai rămas de ajuns. Fiind o variabilă partajată, modificarea lui trebuie bineînțeles protejată de un //lock//. În momentul în care ultimul thread decrementează contorul, acesta va avea valoarea 0, semnalizând faptul că toate thread-urile au ajuns la barieră. Ultimul thread va incrementa astfel semaforul (**deblocarea**) și va debloca toate thread-urile blocate. | + | Pentru a rezolva problema accesului in b pentru coloane intregi, se va trece la accesarea unui subset a unei coloane in b, sau a mai multor coloane la un moment dat. Pentru o mai buna intelegere, urmariti desenele de mai jos: |
| - | + | ||
| - | <code python simple_barrier_semaphore.py> | + | |
| - | from threading import * | + | {{:asc:lab5:c_axb_1.jpg|}} |
| - | class SimpleBarrier(): | + | Ideea de baza este refolosirea cat mai buna a elementelor aflate in cache (pentru matricea b). Astfel odata cu calculul lui c[i][j], de ce nu am calcula si c[i][j+1], daca tot se afla in cache si coloana j+1. Acest lucru presupune insa reordonarea operatiilor astfel: calculeaza primii b termeni pentru c[i][j], calculeaza primii b termeni pentru c[i][j+1], calculeaza urmatorii b termeni pentru c[i][j], calculeaza urmatorii b termeni pentru c[i][j+1], etc. |
| - | def __init__(self, num_threads): | + | |
| - | self.num_threads = num_threads | + | |
| - | self.count_threads = self.num_threads # contorizeaza numarul de thread-uri ramase | + | |
| - | self.count_lock = Lock() # protejeaza accesarea/modificarea contorului | + | |
| - | self.threads_sem = Semaphore(0) # blocheaza thread-urile ajunse | + | |
| - | + | ||
| - | def wait(self): | + | |
| - | with self.count_lock: | + | |
| - | self.count_threads -= 1 | + | |
| - | if self.count_threads == 0: # a ajuns la bariera si ultimul thread | + | |
| - | for i in range(self.num_threads): | + | |
| - | self.threads_sem.release() # incrementarea semaforului va debloca num_threads thread-uri | + | |
| - | self.threads_sem.acquire() # num_threads-1 threaduri se blocheaza aici | + | |
| - | # contorul semaforului se decrementeaza de num_threads ori | + | |
| - | class MyThread(Thread): | + | {{:asc:lab5:c_axb_2.jpg|}} |
| - | def __init__(self, tid, barrier): | + | |
| - | Thread.__init__(self) | + | In acest mod, de ce nu am calcula o intreaga sectiune de linie din c, folosind aceste reordonari de operatii? |
| - | self.tid = tid | + | |
| - | self.barrier = barrier | + | |
| - | + | ||
| - | def run(self): | + | |
| - | print ("I'm Thread " + str(self.tid) + " before\n") | + | |
| - | self.barrier.wait() | + | |
| - | print ("I'm Thread " + str(self.tid) + " after barrier\n") | + | |
| - | </code> | + | Ce s-ar intampla daca am incerca sa calculam o intreaga linie din c? |
| - | **De ce nu este reentrantă bariera cu un semafor?** | + | {{:asc:lab5:c_axb_3.jpg|}} |
| - | Fie cazul în care avem N thread-uri, iar acestea trebuie sincronizate prin barieră de mai multe ori: | + | Ar insemna ca trebuie sa incarcam toate coloanele lui b in memorie (cache), lucru pe care am incercat sa il evitam aici! Astfel, se vor refolosi doar acele blocuri din b ce au fost deja incarcate. De aici nu ne mai ramane decat sa utilizam intreaga linie de cache din b, si obtinem ideea de baza a algoritmului “Blocked Matrix Multiplication”: |
| - | * //N-1// thread-uri vor face //acquire// pe semafor | + | |
| - | * ultimul thread face //release// de //N// de ori pe semafor | + | |
| - | * unul din //release//-uri este pentru el | + | |
| - | * //N-1// thread-uri se deblochează și își continuă execuția; ultimul thread ar trebui să facă //acquire// și să nu se blocheze, doarece semaforul ar trebui să fie 1 | + | |
| - | * rulând în buclă însă, unul din thread-urile deblocate poate ajunge să facă //acquire// din nou, înainte ca ultimul thread să treacă de //acquire// | + | |
| - | * ultimul thread va rămâne blocat la //acquire//, urmând ca și celelalte thread-uri să se blocheze în al doilea acces al barierei; nici un thread nu mai face //release//, ducând astfel la deadlock | + | |
| - | === Bariera reentrantă === | + | {{:asc:lab5:c_axb_4.jpg|}} |
| + | |||
| + | Operatiile trebuie reordonate astfel: calculeaza primii b termeni pentru c[i][j] din blocul C, calculeaza urmatorii b termeni pentru c[i][j] din blocul C, ..., calculeaza ultimii b termeni pentru c[i][j] din blocul C. Generalizand: | ||
| - | Barierele reentrante (eng. //reusable barrier//) sunt utile în prelucrări 'step-by-step' și/sau bucle. Unele aplicații pot necesita ca thread-urile să execute anumite operații în buclă, cu rezultatele tuturor thread-urilor din iterația curentă necesare pentru începerea iterației următoare. În acest caz, după fiecare iterație, se folosește o sincronizare cu barieră reentrantă. | + | {{:asc:lab5:bmm.jpg|}} |
| - | Pentru a adapta bariera din secțiunea anterioară astfel încât să poată fi folosită de mai multe ori, avem nevoie de încă un semafor. Soluția aceasta se bazează pe necesitatea ca toate cele N thread-uri să treacă de //acquire()// înainte ca vreunul să revină la barieră. Astfel, partea de sincronizare este compusă din două etape, fiecare folosind câte un semafor. | + | Pentru a calcula blocul C<sub>22</sub> folosim formula: |
| - | Folosind implementarea de mai jos, garantăm că thread-urile ajung să se blocheze din nou pe primul semafor doar după ce **toate** au trecut în prealabil de acesta: | + | C<sub>22</sub> %%=%% A<sub>21</sub>B<sub>12</sub> + A<sub>22</sub>B<sub>22</sub> + A<sub>23</sub>B<sub>32</sub> + A<sub>24</sub>B<sub>42</sub> |
| - | * //N-1// thread-uri vor face //acquire// pe semaforul 1 | + | |
| - | * ultimul thread face //release// de //N// de ori pe semaforul 1 | + | |
| - | * unul din //release//-uri este pt el | + | |
| - | * //N-1// thread-uri se deblochează și fac //acquire// pe semaforul 2 | + | |
| - | * ultimul thread face și el //acquire// pe semaforul 1 și trece de acesta | + | |
| - | * ultimul thread face //release// de //N// de ori pe semaforul 2 | + | |
| - | * //N-1// thread-uri se deblochează și fac //acquire// pe semaforul 1 | + | |
| - | s.a.m.d.... | + | |
| - | <code python reusable_barrier_semaphore.py> | + | ce presupune patru inmultiri si patru adunari de matrice. Ideea este ca fiecare inmultire opereaza pe un block suficient de mic ca dimensiune astfel incat sa intre in Cache! |
| - | from threading import * | + | Versiunea inmultirii de matrice utilizand metoda bloc si ordonarea i-j-k devine: |
| - | class ReusableBarrier(): | + | <code cpp> |
| - | def __init__(self, num_threads): | + | for (i=0;i<N/b;i++){ |
| - | self.num_threads = num_threads | + | for (j=0;j<N/b;j++){ |
| - | self.count_threads1 = [self.num_threads] | + | for (k=0;k<N/b;k++){ |
| - | self.count_threads2 = [self.num_threads] | + | C[i][j] += A[i][k]*B[k][j] |
| - | self.count_lock = Lock() # protejam accesarea/modificarea contoarelor | + | } |
| - | self.threads_sem1 = Semaphore(0) # blocam thread-urile in prima etapa | + | } |
| - | self.threads_sem2 = Semaphore(0) # blocam thread-urile in a doua etapa | + | } |
| - | + | </code> | |
| - | def wait(self): | + | |
| - | self.phase(self.count_threads1, self.threads_sem1) | + | |
| - | self.phase(self.count_threads2, self.threads_sem2) | + | |
| - | + | ||
| - | def phase(self, count_threads, threads_sem): | + | |
| - | with self.count_lock: | + | |
| - | count_threads[0] -= 1 | + | |
| - | if count_threads[0] == 0: # a ajuns la bariera si ultimul thread | + | |
| - | for i in range(self.num_threads): | + | |
| - | threads_sem.release() # incrementarea semaforului va debloca num_threads thread-uri | + | |
| - | count_threads[0] = self.num_threads # reseteaza contorul | + | |
| - | threads_sem.acquire() # num_threads-1 threaduri se blocheaza aici | + | |
| - | # contorul semaforului se decrementeaza de num_threads ori | + | |
| - | class MyThread(Thread): | + | unde: |
| - | def __init__(self, tid, barrier): | + | * b este dimensiunea blocului (presupunem ca b divide N) |
| - | Thread.__init__(self) | + | * C[i][j] este un bloc al matricei C pe linia i si coloana j |
| - | self.tid = tid | + | * "+=" inseamna adunare de matrice |
| - | self.barrier = barrier | + | * si "*" inseamna inmultire de matrice |
| - | + | ||
| - | def run(self): | + | |
| - | for i in range(10): | + | |
| - | self.barrier.wait() | + | |
| - | print ("I'm Thread " + str(self.tid) + " after barrier, in step " + str(i) + "\n") | + | |
| + | Ce se intampla cu Cache-miss-urile acum? | ||
| + | <code cpp> | ||
| + | for (i=0;i<N/b;i++){ | ||
| + | for (j=0;j<N/b;j++){ | ||
| + | // Scrie blocul C[i][j] al lui c in Memorie (Wc) | ||
| + | for (k=0;k<N/b;k++){ | ||
| + | // Citeste blocul A[i][k] pt a in Cache (Ra) | ||
| + | // Citeste blocul B[k][j] pt b in Cache (Rb) | ||
| + | C[i][j] += A[i][k] * B[k][j]; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| </code> | </code> | ||
| - | ==== Barieră - varianta condition ==== | + | Pentru (Wc) avem acum (N/b)*(N/b)*b*b Cache-miss-uri, in vreme ce pentru (Ra) si (Rb) avem (N/b)*(N/b)*(N/b)*b*b, astfel ducand la N<sup>2</sup> + 2N<sup>3</sup>/b -> 2N<sup>3</sup>/b Cache-miss-uri pentru intregul algoritm. Combinand acest calcul cu faptul ca avem 2N<sup>3</sup> operatii aritmetice, rezulta un raport r = 2N<sup>3</sup> / 2N<sup>3</sup>/b -> b. Dupa cum am stabilit, r trebuie sa fie maxim (mai mare oricum decat 2-ul obtinut in varianta anterioara). Daca mergem pana la cazul extrem, il vom face pe b = N, dar asta nu este viabil, pentru ca atunci suntem din nou la cazul fara blocuri, de la care tocmai venim... |
| - | O altă utilizare a obiectului //Condition// poate fi văzută în implementarea barierei reentrante, ca **mecanism de blocare/deblocare**. Bariera poate fi implementată cu un singur obiect deoarece prezența implicită a lock-ului în operațiile obiectului //Condition//, împreună cu funcționarea atomică a metodei //wait()// ne permit evitarea problemelor ce apar la refolosirea obiectelor //Event//. Pe lângă notificarea thread-urilor de îndeplinirea condiție barierei, putem folosi obiectul //Condition// și pentru protejarea resursei partajate (**contorul** de thread-uri blocate), eliminând astfel necesitatea unui //Lock// separat. | + | Astfel, acest algoritm functioneaza doar daca blocurile intra in Cache. Acest lucru inseamna ca trei blocuri diferite, de dimensiune b*b, trebuie sa intre in Cache, pentru toate cele trei matrice (a, b si c). Daca C este dimensiunea Cache-ului in elemente de matrice, atunci trebuie sa fie 3b<sup>2</sup> ≤ C sau b ≤ √(C / 3) . Astfel, in cel mai bun caz, r-ul trebuie sa fie si el √(C / 3). |
| - | <code python reusable_barrier_condition.py> | + | Putem astfel spune, pentru diverse procesoare, cunoscand rata de operatii aritmetice la cache-miss-uri r, care este dimensiunea necesara a Cache-ului, pentru a rula acest algoritm, astfel incat procesorul sa NU astepte niciodata memoria: |
| - | from threading import * | + | /* TODO: PLEASE UPDATE THIS! |
| - | + | ^ Procesor ^ Dimensiune Cache (KB) ^ | |
| - | class ReusableBarrier(): | + | |Ultra 2i | 14.8 | |
| - | def __init__(self, num_threads): | + | |Ultra 3 | 4.7 | |
| - | self.num_threads = num_threads | + | |Pentium 3 | 0.9 | |
| - | self.count_threads = self.num_threads # contorizeaza numarul de thread-uri ramase | + | |Pentium 3M | 2.4 | |
| - | self.cond = Condition() # blocheaza/deblocheaza thread-urile | + | |Power 3 | 1.8 | |
| - | # protejeaza modificarea contorului | + | |Power 4 | 5.4 | |
| - | + | |Itanium 1 | 31.1 | | |
| - | def wait(self): | + | |Itanium 2 | 0.7 | |
| - | self.cond.acquire() # intra in regiunea critica | + | |
| - | self.count_threads -= 1; | + | |
| - | if self.count_threads == 0: | + | |
| - | self.cond.notify_all() # deblocheaza toate thread-urile | + | |
| - | self.count_threads = self.num_threads # reseteaza contorul | + | |
| - | else: | + | |
| - | self.cond.wait(); # blocheaza thread-ul eliberand in acelasi timp lock-ul | + | |
| - | self.cond.release(); # iese din regiunea critica | + | |
| + | */ | ||
| - | class MyThread(Thread): | + | ==== Activitate practica - BMM & Optimizare pentru Cache ==== |
| - | def __init__(self, tid, barrier): | + | |
| - | Thread.__init__(self) | + | De aceea incercati sa experimentati cele prezentate in acest laborator, in C. Pentru cei interesati, incercati completarea tabelului de mai sus cu dimensiunea Cache-ului pentru procesoarele voastre personale. Acest lucru presupune evident, si o documentare asupra caracteristicilor sistemului propriu (determinarea r-ului, a dimensiunii Cache-ului etc.). |
| - | self.tid = tid | + | |
| - | self.barrier = barrier | + | <code cpp> |
| + | #include <stdio.h> | ||
| + | #include <stdlib.h> | ||
| + | #include <sys/time.h> | ||
| + | |||
| + | void BMMultiply(int n, double** a, double** b, double** c) | ||
| + | { | ||
| + | int bi=0; | ||
| + | int bj=0; | ||
| + | int bk=0; | ||
| + | int i=0; | ||
| + | int j=0; | ||
| + | int k=0; | ||
| + | // TODO: set block dimension blockSize | ||
| + | int blockSize=100; | ||
| + | |||
| + | for(bi=0; bi<n; bi+=blockSize) | ||
| + | for(bj=0; bj<n; bj+=blockSize) | ||
| + | for(bk=0; bk<n; bk+=blockSize) | ||
| + | for(i=0; i<blockSize; i++) | ||
| + | for(j=0; j<blockSize; j++) | ||
| + | for(k=0; k<blockSize; k++) | ||
| + | c[bi+i][bj+j] += a[bi+i][bk+k]*b[bk+k][bj+j]; | ||
| + | } | ||
| - | def run(self): | + | int main(void) |
| - | for i in range(10): | + | { |
| - | self.barrier.wait() | + | int n; |
| - | print ("I'm Thread " + str(self.tid) + " after barrier, in step " + str(i) + "\n") | + | double** A; |
| + | double** B; | ||
| + | double** C; | ||
| + | int numreps = 10; | ||
| + | int i=0; | ||
| + | int j=0; | ||
| + | struct timeval tv1, tv2; | ||
| + | struct timezone tz; | ||
| + | double elapsed; | ||
| + | // TODO: set matrix dimension n | ||
| + | n = 500; | ||
| + | // allocate memory for the matrices | ||
| + | |||
| + | // TODO: allocate matrices A, B & C | ||
| + | ///////////////////// Matrix A ////////////////////////// | ||
| + | // TODO ... | ||
| + | |||
| + | ///////////////////// Matrix B ////////////////////////// | ||
| + | // TODO ... | ||
| + | |||
| + | ///////////////////// Matrix C ////////////////////////// | ||
| + | // TODO ... | ||
| + | |||
| + | // Initialize matrices A & B | ||
| + | for(i=0; i<n; i++) | ||
| + | { | ||
| + | for(j=0; j<n; j++) | ||
| + | { | ||
| + | A[i][j] = 1; | ||
| + | B[i][j] = 2; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | //multiply matrices | ||
| + | |||
| + | printf("Multiply matrices %d times...\n", numreps); | ||
| + | for (i=0; i<numreps; i++) | ||
| + | { | ||
| + | gettimeofday(&tv1, &tz); | ||
| + | BMMultiply(n,A,B,C); | ||
| + | gettimeofday(&tv2, &tz); | ||
| + | elapsed += (double) (tv2.tv_sec-tv1.tv_sec) + (double) (tv2.tv_usec-tv1.tv_usec) * 1.e-6; | ||
| + | } | ||
| + | printf("Time = %lf \n",elapsed); | ||
| + | |||
| + | //deallocate memory for matrices A, B & C | ||
| + | // TODO ... | ||
| + | |||
| + | return 0; | ||
| + | } | ||
| </code> | </code> | ||
| - | Puteți găsi aici modul de implementare a [[https://github.com/python/cpython/blob/master/Lib/threading.py#L580|barierei cu condition]] din sursele oficiale. | + | * [[https://www.linkedin.com/posts/eric-vyacheslav-156273169_this-is-the-best-matmul-animation-on-the-activity-7317502251646758913-mggn/?utm_source=share&utm_medium=member_android&rcm=ACoAADXXE9UB_GxnGDBMJ5zHUnOXdPwsl36scYY | O vizualizare foarte inspirata a BMM-ului.]] |
| - | ===== Exerciții ===== | + | * [[https://github.com/wentasah/mmul-anim/tree/master | Repo-ul de Github al vizualizarii BMM]] |
| - | + | ||
| - | ** Task 1-Condition ** | + | |
| - | + | ||
| - | Pornind de la fișierul ''event.py'', înlocuiți obiectele //Event// ''work_available'' și ''result_available'' cu o singură variabilă condiție, păstrand funcționalitatea programului intactă. | + | |
| - | + | ||
| - | ** Task 2-Events ** | + | |
| - | + | ||
| - | Rulați fișierul ''broken-event.py''. Încercați diferite valori pentru //sleep()//-ul de la linia 47. Încercați eliminarea apelului //sleep()//. Ce observați? Precizați secvența _minimă_ de intercalare a apelurilor ''set'' și ''wait'' pe cele două obiecte //Event// care generează comportamentul observat. | + | |
| - | * Folosiți ''Ctrl+\'' pentru a opri un program blocat. | + | |
| - | * Folosiți //sleep()// pentru a forța diferite intercalări ale thread-urilor. | + | |
| - | * Folosiți instrucțiuni //print// înaintea metodelor care lucrează cu ''Event''-uri pentru a avea o idee asupra ordinii operațiilor. | + | |
| - | * :!: Datorită intercalărilor thread-urilor este posibil ca //print//-urile să nu reflecte ordinea exactă a operațiilor. Rulați de mai multe ori pentru a putea prinde problema. | + | |
| - | + | ||
| - | ** Task 3-ThreadPoolExecutor ** | + | |
| - | + | ||
| - | Folosind un pool de thread-uri căutați o secvență de ADN într-un set de eșantioane de ADN (DNA samples). Creați-vă un modul în care: | + | |
| - | -Folosiți modulul ''random'' pentru a genera 100 sample-uri de DNA de lungime 10000 | + | |
| - | * [[https://docs.python.org/3/library/random.html#random.choice|random.choice(seq)]] întoarce un element random dintr-o secvența data (în Python, tipurile secvența (sequence types) sunt și listele și stringurile, deci puteți folosi șirul 'ATGC' ca input pt choice()). | + | |
| - | * folosiți [[https://docs.python.org/3/library/random.html#random.seed|seed]] cu un parametru pentru a genera tot timpul aceleași secvențe random și a va testa codul mai ușor. By default seed-ul este luat pe baza timpului curent, deci se va schimbă între rulări. | + | |
| - | * "the pythonic way"-încercați să generați secvențele într-o singură linie (one liner cu [[asc:laboratoare:01#liste_tupluri_și_elemente_de_programare_functionala|list comprehensions]]) | + | |
| - | -Definiți-vă un subșir cu mai mult de 10 elemente. Recomandăm să afișați sample-urile obținute random și să selectați un subșir din una din ele. | + | |
| - | -Folosiți un ''ThreadPoolExecutor'' cu un număr maxim de threaduri, de exemplu 30 | + | |
| - | -Funcția pe care o execută fiecare thread: | + | |
| - | * primește ca argument indexul unui sample | + | |
| - | * caută prima apariție a subșirului în sample-ul dat. | + | |
| - | * întoarce un mesaj, de exemplu "DNA sequence found în sample 1" | + | |
| - | -Folosind [[https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit|submit]] sau [[https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map|map]] dați spre execuție funcția de căutare. | + | |
| - | -Afișați rezultatele | + | |
| - | * păstrați ce întorc apelurile către submit sau rezultatul lui map într-o variabilă și apoi iterați pe elementele din ea, afișând fiecare rezultat) | + | |
| - | * puteți vedea în [[asc:laboratoare:03#threadpoolexecutor|exemplul din laborator]] cum se afișează rezultatele unui map, sau în documentație pentru [[https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor-example|submit]] | + | |
| - | * dacă nu sunteți familiari cu sintaxa de one-liners folosită în documentație, puteți folosi simple for-uri, va creați o lista goală înainte, și la fiecare iterație din for adăugați în ea rezultatul metodei submit. După aceea mai faceți un for în care afișați elementele din lista, apelând .result() pe fiecare din ele. | + | |
| - | + | ||
| - | + | ||
| - | + | ===== In loc de concluzie ===== | |
| + | Intelegerea reala a comportamentului unei aplicatii (algoritm), din punctul de vedere al utilizarii cache-ului (si al performantelor in general), este o chestiune complexa, ce necesita multa rabdare si cunostinte diverse. Deseori, aproximatii utile pot fi folosite pentru a imbunatati unele aspecte ale implementarii curente. Utilizarea blocurilor este intalnita deseori in algoritmi si aplicatii ce necesita performante crescute. | ||
| + | |||
| + | |||
| + | ===== Exercitii ===== | ||
| + | |||
| + | |||
| + | **Task 0** - Rulați ''task01'', ''task02'' si ''task03''ca exemple pentru [[#activitate_practica_-_optimizare_constantelor_si_al_accesului_la_vectori | Optimizarea constantelor si al accesului la vectori]] folosind matrici liniarizate. | ||
| + | |||
| + | **Task 1** - Implemenati [[#activitate_practica_-_ordinea_buclelor | Ordonarea buclelor]] folosind matrici liniarizate in ''task11.c'' si ''task12.c''. Rulati si observati diferentele. | ||
| + | |||
| + | **Task 2** - Implementati [[#activitate_practica_-_bmm_optimizare_pentru_cache | Optimizari pentru Cache]] folosind matrici liniarizate. | ||
| + | * ''task21.c'' Implementati BMM | ||
| + | * ''task22.c'' Implementati BMM alaturi de [[#activitate_practica_-_optimizare_constantelor_si_al_accesului_la_vectori | Optimizarea constantelor si al accesului la vectori]]. | ||
| + | |||
| + | |||
| + | **Task 3** - In fisierul ''task3.c'' implementati [[#activitate_practica_-_ordinea_buclelor | Ordonarea buclelor]] i-k-j folosind [[#activitate_practica_-_optimizare_constantelor_si_al_accesului_la_vectori | Optimizarea constantelor si al accesului la vectori]] | ||
| + | |||
| + | |||
| + | **Task 4** - (Bonus) In general, nu recomandam alocarea matricelor ca vectori de vectori. Ca bonus va sugeram sa realizati un test unde se face acest tip de alocare si se verifica "performantele" obtinute. | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | <note important> | ||
| + | Toate rularile din acest laborator trebuiesc facute cu optiunea -O0 (O zero), adica ''gcc -O0 -o binary source-file.c'' | ||
| + | |||
| + | Motivul principal este ca optimizarile de compilator ar putea sa ascunda optimizarile pe care le veti incerca (si reusi) voi la laborator. | ||
| + | </note> | ||
| ===== Resurse ===== | ===== Resurse ===== | ||
| + | * Responsabilul acestui laborator: [[emil.slusanschi@cs.pub.ro|Emil Slușanschi]] | ||
| + | * {{:asc:laboratoare:lab09.zip|Schelet Laborator 9}} | ||
| + | <hidden> * {{:asc:lab5:sol:lab5_sol.tar.gz|Soluție Laborator 9}} </hidden> | ||
| - | * <html><a class="media mediafile mf_pdf" href=":asc:lab3:index?do=export_pdf">PDF laborator</a></html> | ||
| - | * {{:asc:lab3:lab3-skel.zip|Schelet laborator}} | ||
| - | * {{:asc:lab3:lab3-sol.zip|Soluție laborator}} | ||
| + | ==== Discutii interesante ==== | ||
| - | ==== Referințe ==== | + | * [[http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array | De ce este mai rapida procesarea unui vector ordonat?]] |
| - | * [[https://docs.python.org/3/library/threading.html| modulul threading]] - Thread, Lock, Semaphore, Condition, Event, Barrier | + | * {{:asc:lab5:what_every_programmer_should_know_about_memory_by_ulrich_drepper_.pdf|What every programmer should know about memory.pdf}} |
| - | * [[http://docs.python.org/3/library/queue.html#module-Queue| modulul Queue]] | + | |
| - | * [[https://docs.python.org/3/library/concurrent.futures.html#module-concurrent.futures| modulul concurrent.futures]] | + | |
| - | * <html><a class="media mediafile mf_pdf" href="http://greenteapress.com/semaphores/LittleBookOfSemaphores.pdf">Little book of semaphores</a></html> - capitolele 3.5 //Barrier// și 3.6 //Reusable Barrier// | + | |
| + | |||
| + | |||
| + | ==== Valgrind ==== | ||
| + | * http://valgrind.org/docs/manual/cg-manual.html | ||
| + | |||
| + | ==== Referințe ==== | ||
| + | <hidden> | ||
| + | * [[https://github.com/metallurgix | Exemple inmultire matrice]] <-- outdated | ||
| + | </hidden> | ||