This shows you the differences between two versions of the page.
app:laboratoare:03 [2022/10/22 17:07] florin.mihalache [Construcții de sincronizare] |
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 === | + | ==== 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. | 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. | ||
Line 105: | Line 105: | ||
</code> | </code> | ||
- | === Barieră === | + | ==== Barieră ==== |
- | Un alt element de sincronizare 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. | + | 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. | În OpenMP, pentru barieră avem directiva ''#pragma omp barrier'', echivalent cu ''pthread_barrier_t'' din pthreads. | ||
Line 127: | Line 127: | ||
</code> | </code> | ||
- | === Reduction === | + | ==== 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. | ''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)'' | Tipar: ''reduction(operator_operatie:variabila_in_care_se_acumuleaza)'' | ||
- | Exemplu de reduction: ''reduction(+:sum)'', unde se însumează elementele unui array în variabila sum | + | Exemplu de reduction: ''reduction(+:sum)'', unde se însumează indecșii unui loop în variabila sum |
Exemplu de folosire de reduction: | Exemplu de folosire de reduction: | ||
Line 144: | Line 144: | ||
</code> | </code> | ||
- | === Atomic === | + | ==== 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ă. | 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ă. | ||
Line 166: | Line 166: | ||
</code> | </code> | ||
- | === Ordered === | + | ==== Ordered ==== |
- | Directiva ''ORDERED'' este folosit în for-uri cu scopul de a distribui în ordine iterațiile către thread-uri. | + | Directiva ''ORDERED'' este folosită în for-uri cu scopul de a distribui în ordine iterațiile către thread-uri. |
Exemplu: | Exemplu: | ||
Line 201: | Line 201: | ||
* ''COPYPRIVATE'' - folosit în blocurile ''SINGLE'', pentru a face vizibilă valoarea atribuită unei variabile într-un bloc ''SINGLE'' pentru toate thread-urile | * ''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 | * ''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]] |