This shows you the differences between two versions of the page.
app:laboratoare:03 [2022/10/22 17:01] florin.mihalache |
app:laboratoare:03 [2024/10/22 11:06] (current) alexandru.bala [Tasks (opțional)] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Laboratorul 3 - Advanced OpenMP ===== | + | ====== Laboratorul 3 - Advanced OpenMP ====== |
- | ==== Sections ==== | + | ===== Sections ===== |
Uneori dorim să distribuim ca thread-uri diferite să execute task-uri diferite în același timp. În această privință ne vine de ajutor conceptul de sections, prin care două sau mai multe thread-uri execută două sau mai multe sections corespunzătoare acestora (adică thread-urilor, fiecare thread cu un section). | Uneori dorim să distribuim ca thread-uri diferite să execute task-uri diferite în același timp. În această privință ne vine de ajutor conceptul de sections, prin care două sau mai multe thread-uri execută două sau mai multe sections corespunzătoare acestora (adică thread-urilor, fiecare thread cu un section). | ||
Line 56: | Line 56: | ||
{{ :app:laboratoare:sections.png?400 |}} | {{ :app:laboratoare:sections.png?400 |}} | ||
- | ==== Single ==== | + | ===== Single ===== |
Dacă dorim ca o secvență de cod (dintr-o bucată de cod paralelizat) să fie executat doar de un singur thread, folosim directiva ''SINGLE''. Aceasta este folosită, de regulă, în operații I/O. | Dacă dorim ca o secvență de cod (dintr-o bucată de cod paralelizat) să fie executat doar de un singur thread, folosim directiva ''SINGLE''. Aceasta este folosită, de regulă, în operații I/O. | ||
Line 70: | Line 70: | ||
</code> | </code> | ||
- | === Master === | + | ==== Master ==== |
Directiva ''MASTER'' este o particularizare a directivei ''SINGLE'', unde codul din zona paralelizată este executat de thread-ul master (cel cu id-ul 0). | Directiva ''MASTER'' este o particularizare a directivei ''SINGLE'', unde codul din zona paralelizată este executat de thread-ul master (cel cu id-ul 0). | ||
<code c> | <code c> | ||
Line 82: | Line 82: | ||
</code> | </code> | ||
- | ==== Construcții de sincronizare ==== | + | ===== Construcții de sincronizare ===== |
+ | ==== Mutex ==== | ||
+ | Pentru zonele critice, unde avem operații de read-write, folosim directiva ''#pragma omp critical'', care reprezintă un mutex, echivalentul lui ''pthread_mutex_t'' din pthreads, care asigură faptul că un singur thread accesează zona critică la un moment dat, thread-ul deținând lock-ul pe zona critică în momentul respectiv, și că celelalte thread-uri care nu au intrat încă în zona critică așteaptă eliberarea lock-ului de către thread-ul aflat în zona critică în acel moment. | ||
+ | |||
+ | Exemplu de folosire: | ||
+ | <code c> | ||
+ | #include <stdio.h> | ||
+ | #include <omp.h> | ||
+ | |||
+ | int main (int argc, char** argv) { | ||
+ | int thread_id, sum = 0; | ||
+ | #pragma omp parallel private(thread_id) shared(sum) | ||
+ | { | ||
+ | thread_id = omp_get_thread_num(); | ||
+ | #pragma omp critical | ||
+ | sum += thread_id; | ||
+ | } | ||
+ | printf("%d",sum); | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | ==== Barieră ==== | ||
+ | Un alt element de sincronizare îl reprezintă bariera, care asigură faptul că niciun thread gestionat de barieră nu trece mai departe de aceasta decât atunci cand toate thread-urile gestionate de barieră au ajuns la punctul unde se află bariera. | ||
+ | |||
+ | În OpenMP, pentru barieră avem directiva ''#pragma omp barrier'', echivalent cu ''pthread_barrier_t'' din pthreads. | ||
+ | |||
+ | Exemplu de folosire: | ||
+ | <code c> | ||
+ | #include <stdio.h> | ||
+ | #include <omp.h> | ||
+ | |||
+ | int main (int argc, char** argv) { | ||
+ | #pragma omp parallel | ||
+ | { | ||
+ | printf("First print by %d\n", omp_get_thread_num()); | ||
+ | #pragma omp barrier | ||
+ | printf("Second print by %d\n", omp_get_thread_num()); | ||
+ | } | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | ==== Reduction ==== | ||
+ | ''reduction'' este o directivă folosită pentru operații de tip reduce / fold pe arrays / colecții sau simple însumări / înmulțiri în cadrul unui loop. Mai precis, elementele dintr-un array sau indecșii unui loop sunt "acumulați" într-o singură variabilă, cu ajutorul unei operații, al cărui semn este precizat. | ||
+ | |||
+ | Tipar: ''reduction(operator_operatie:variabila_in_care_se_acumuleaza)'' | ||
+ | |||
+ | Exemplu de reduction: ''reduction(+:sum)'', unde se însumează indecșii unui loop în variabila sum | ||
+ | |||
+ | Exemplu de folosire de reduction: | ||
+ | <code c> | ||
+ | int sum = 0; | ||
+ | |||
+ | #pragma omp parallel for reduction(+:sum) private(i) | ||
+ | for (i = 1; i <= num_steps; i++) { | ||
+ | sum += i; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | ==== Atomic ==== | ||
+ | Directiva ''ATOMIC'' permite executarea unor instrucțiuni în mod atomic, instrucțiuni care provoacă race conditions între thread-uri, problemă pe care această directivă o rezolvă. | ||
+ | |||
+ | Exemplu de folosire: | ||
+ | <code c> | ||
+ | #include <stdio.h> | ||
+ | #include <omp.h> | ||
+ | |||
+ | int main (int argc, char** argv) { | ||
+ | int thread_id, sum = 0; | ||
+ | #pragma omp parallel private(thread_id) shared(sum) | ||
+ | { | ||
+ | thread_id = omp_get_thread_num(); | ||
+ | #pragma omp atomic | ||
+ | sum += thread_id; | ||
+ | } | ||
+ | printf("%d",sum); | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | ==== Ordered ==== | ||
+ | Directiva ''ORDERED'' este folosită în for-uri cu scopul de a distribui în ordine iterațiile către thread-uri. | ||
+ | |||
+ | Exemplu: | ||
+ | <code c> | ||
+ | #pragma omp parallel for ordered private(i) | ||
+ | for (i = 0; i < 10; i++) { | ||
+ | printf("** iteration %d thread no. %d\n", i, omp_get_thread_num()); | ||
+ | } | ||
+ | </code> | ||
+ | Afișare: | ||
+ | <code bash> | ||
+ | ** iteration 9 thread no. 7 | ||
+ | ** iteration 5 thread no. 3 | ||
+ | ** iteration 6 thread no. 4 | ||
+ | ** iteration 7 thread no. 5 | ||
+ | ** iteration 4 thread no. 2 | ||
+ | ** iteration 2 thread no. 1 | ||
+ | ** iteration 3 thread no. 1 | ||
+ | ** iteration 0 thread no. 0 | ||
+ | ** iteration 1 thread no. 0 | ||
+ | ** iteration 8 thread no. 6 | ||
+ | </code> | ||
+ | |||
+ | ==== Clauze legate de vizibilitatea variabilelor ==== | ||
+ | * ''SHARED'' | ||
+ | * ''PRIVATE'' - variabilă cu valoarea vizibilă doar în blocul paralel (diferă de ''THREADPRIVATE'') | ||
+ | * ''DEFAULT'' | ||
+ | * ''REDUCTION'' | ||
+ | * ''NONE'' | ||
+ | * ''THREADPRIVATE'' - fiecare thread are propriile sale copii ale unor variabile | ||
+ | * ''FIRSTPRIVATE'' - folosit pentru ca variabilele ''THREADPRIVATE'' să aibă valorile, inițial, din exterior (dinainte) | ||
+ | * ''LASTPRIVATE'' - invers ''FIRSTPRIVATE'', ultima valoare asignată unei variabile ''THREADPRIVATE'' e vizibilă după blocul paralelizat | ||
+ | * ''COPYPRIVATE'' - folosit în blocurile ''SINGLE'', pentru a face vizibilă valoarea atribuită unei variabile într-un bloc ''SINGLE'' pentru toate thread-urile | ||
+ | * ''COPYIN'' - asignarea unei variabile ''THREADPRIVATE'' este vizibilă tuturor thread-urilor | ||
+ | |||
+ | ===== Tasks ===== | ||
+ | Task-urile în OpenMP reprezintă un concept prin care putem să avem thread pools pentru paralelizarea de soluții ale căror dimensiune nu o știm (echivalent cu ''ExecutorService'' din Java). Un task este executat la un moment dat de către un thread din thread pool. | ||
+ | |||
+ | Pentru crearea unui task se folosește directiva ''TASK'': | ||
+ | <code c> | ||
+ | #pragma omp task [clause1 [[,] clause2, ...]] | ||
+ | </code> | ||
+ | Pentru sincronizarea task-urilor (în sensul să așteptăm toate rezultatele task-urilor, în stilul barierei), se folosește directiva ''TASKWAIT'' (exemplu de folosire în exemplul Fibonacci de mai jos). | ||
+ | |||
+ | În privința variabilelor dintr-un task, aici avem trei variante de variabile: | ||
+ | |||
+ | * ''shared'' - toate task-urile au acces la aceeași adresă a unei variabile, o modificare asupra variabilei din partea unui task va fi vizibilă către toate task-urile (uneori putem avea potențial de erori în acest caz). | ||
+ | * ''firstprivate'' - fiecare task va avea o copie a unei variabile inițializate cu o valoare înainte de crearea task-ului respectiv. | ||
+ | * ''private'' - aici putem să avem variabile care nu sunt inițializate înainte de crearea task-ului și care să fie inițializate în cadrul task-ului. | ||
+ | |||
+ | <code c> | ||
+ | void f () { | ||
+ | double x1 = 1.0; | ||
+ | double x2 = 2.0; | ||
+ | #pragma omp parallel firstprivate(x2) | ||
+ | { | ||
+ | double x3 = 3.0; // private to each implicit task due to scope | ||
+ | #pragma omp task | ||
+ | { | ||
+ | double x4 = 4.0; // private due to scope | ||
+ | // x1 : shared ( shared by all implicit tasks ) | ||
+ | // x2 : firstprivate ( due to “firstprivate(x2)” ) | ||
+ | // x3 : firstprivate ( not shared by all implicit tasks ) | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | Pentru paralelizarea unor probleme recursive (Fibonacci, parcurgeri, etc.), task-urile reprezintă o soluție optimă în acest caz. De asemenea, putem crea task-uri în cadrul unui task părinte. | ||
+ | |||
+ | Exemplu: | ||
+ | <code c> | ||
+ | #include <stdio.h> | ||
+ | #include <omp.h> | ||
+ | |||
+ | int fib(int n) { | ||
+ | int i, j; | ||
+ | printf("n = %d | Thread id = %d\n", n, omp_get_thread_num()); | ||
+ | |||
+ | if (n < 2) { | ||
+ | return n; | ||
+ | } | ||
+ | |||
+ | #pragma omp task shared(i) | ||
+ | i = fib(n - 1); | ||
+ | |||
+ | #pragma omp task shared(j) | ||
+ | j = fib(n - 2); | ||
+ | |||
+ | // se așteaptă să se termine task-urile de mai sus | ||
+ | #pragma omp taskwait | ||
+ | return i + j; | ||
+ | } | ||
+ | |||
+ | |||
+ | int main() { | ||
+ | int n = 10; | ||
+ | omp_set_num_threads(4); | ||
+ | |||
+ | #pragma omp parallel shared(n) | ||
+ | { | ||
+ | #pragma omp single | ||
+ | printf ("fib(%d) = %d\n", n, fib(n)); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | Se pot observa asemănări între tasks și sections în ceea ce privește modul de folosire (se pot folosi sections în cadrul problemelor recursive), însă diferențele dintre aceastea sunt următoarele: | ||
+ | * la sections instrucțiunile sunt executate imediat când thread-ul asociat acelui section ajunge în section-ul respectiv, când la tasks instrucțiunile pot fi executate după ce thread-ul asociat task-ului trece de task-ul respectiv | ||
+ | * la sections putem avea overhead și load balancing slab | ||
+ | |||
+ | ===== Exerciții ===== | ||
+ | * Paralelizați fișierul ''main.c'' din [[https://github.com/cs-pub-ro/app-labs/tree/master/lab3/skel | schelet]], unde se citește un fișier, unde pe prima linie se află numărul de elemente pentru un array și pe următoarea linie se află array-ul respectiv, se face suma numerelor (aici faceți în trei moduri, separat, cu ''reduction'', cu ''atomic'' și cu ''critical'', unde veți măsura timpii de execuție - hint, folosiți directiva master ca un singur thread să facă măsurătorile), iar la final, cu ajutorul ''sections'', scrieți timpii de execuție în trei fișiere (este deja implementată funcția de scriere în fișier). | ||
+ | |||
+ | <note tip>O să aveți nevoie de barieră la citire și înainte de scrierea în fișiere.</note> | ||
+ | |||
+ | <note>De probă, încercați să puneți ORDERED la for-urile paralelizate, pentru a vedea cum este afectată performanța.</note> | ||
+ | |||
+ | * Paralelizați folosind task-uri codul din [[https://github.com/cs-pub-ro/app-labs/blob/master/lab3/skel/tree.c | tree.c]] (folosiți task-uri în funcțiile ''preorder'' și ''height'' - la ultima trebuie să folosiți ''taskwait''). | ||
+ | |||
+ | ===== Resurse ===== | ||
+ | |||
+ | - [[https://stackoverflow.com/questions/18669296/c-openmp-parallel-for-loop-alternatives-to-stdvector | User-defined OpenMP Reduction]] | ||
+ | - [[https://stackoverflow.com/questions/18022133/difference-between-openmp-threadprivate-and-private |Difference between OpenMP threadprivate and private]] | ||
+ | - [[https://learn.microsoft.com/en-us/cpp/parallel/openmp/reference/openmp-clauses?view=msvc-170 | OpenMP Clauses]] |