Sari la conținutul principal

Exerciții

  1. Compilați și executați codul din fișierul example.c din scheletul de laborator, pe care îl găsiți pe acest repository de GitHub, pe care trebuie să îl clonați, folosind comanda git clone. Schimbați numărul de thread-uri și observați cum se schimbă comportamentul programului.
  2. Schimbați numărul de thread-uri din cod ca să fie egal cu numărul de core-uri de pe mașina pe care rulați, astfel încât, rulând codul pe un alt calculator, numărul de thread-uri să se schimbe automat. Verificați sugestia 1 de mai jos pentru informații suplimentare.
  3. Modificați funcția f astfel încât mesajul "Hello World" să fie afișat iterativ de 100 de ori de fiecare thread, împreună cu indicele iterației și id-ul tread-ului. Întrebare: Codul afișează mesajele în ordinea așteptată de voi? Rulați de mai multe ori. Ce observați cu privire la ordinea afișărilor realizate de același thread? Dar despre cele din thread-uri diferite?
  4. Modificați programul astfel încât să creeze două thread-uri, fiecare thread rulând propria sa funcție.
  5. Pornind de la codul din fișierul add_parallel.c din arhiva de laborator, paralelizați incrementarea elementelor unui vector cu 100. Acest lucru va presupune împărțirea iterației de adunare la toate thread-urile într-un mod cât mai echitabil. Verificați sugestia 2 de mai jos pentru informații suplimentare.
  6. Demonstrați că programul vostru scalează (adică durează mai puțin când rulați cu mai multe thread-uri). Folosiți add_serial.c ca referință pentru calculul accelerației. Verificați sugestia 3 și sugestia 4 de mai jos pentru informații suplimentare.
  7. Folosiți o metodă de a măsura timpul de execuție a unei bucăți din program pentru a măsura timpul de execuție doar pentru componenta paralelizată din program. Cum este speedup-ul calculat prin timpii calculați prin această metodă față de cel obținut cu timpii de la exercițiile anterioare? Verificați sugestia 5 de mai jos pentru informații suplimentare.
sugestie
  1. Pentru a putea obține numărul de core-uri de pe un calculator, se poate folosi funcția sysconf astfel:
#include <unistd.h>

long cores = sysconf(_SC_NPROCESSORS_CONF);
sugestie
  1. Pentru exercițiul 5, avem un vector de N elemente pe care vrem să-l împărțim în mod (aproximativ) egal la P thread-uri, unde fiecare thread are un ID de la 0 la P-1. Astfel, fiecare thread va itera pe câte o secțiune din vectorul inițial, fără a afecta operațiile celorlalte thread-uri. Este necesar deci să calculăm indexul de start și indexul de final (end) pentru fiecare thread. Un mod de a calcula aceste două valori poate fi următorul:
int start = ID * (double)N / P;
int end = min((ID + 1) * (double)N / P, N);
sugestie
  1. Pentru a putea observa mai bine scalabilitatea unui program, este necesar ca acesta să dureze măcar câteva secunde, deoarece, în caz contrar, timpul de inițializare, alte programe care rulează pe calculator, și overhead-ul cauzat de planificarea firelor de execuție, ar putea afecta timpii de execuție suficient cât să nu vedem scalabilitatea doar prin măsurarea timpului total de execuție. Mai mult, inițializarea serială a vectorului (în main) are o durată comparabilă cu execuția pe un thread a operației de paralelizat. De aceea, pentru exercițiul 5, se recomandă să creșteți durata de execuție a unui thread prin repetarea iterativă a operațiilor realizate în funcția de thread..

Pentru a măsura timpii de execuție, puteți folosi comanda time în linia de comandă, astfel:

$ time ./program
real 0m6.958s
user 0m6.745s
sys 0m0.010s
sugestie
  1. Ca să verificați dacă un program scalează, trebuie să:
  • alegeți o dimensiune a problemei (N) pentru care timpul de execuție secvențial să fie suficient de mare astfel încât variațiile să nu impacteze foarte mult rezultatul (în acest caz, alegem N astfel încât timpul de execuție să fie cel puțin câteva secunde)
  • măsurați timpul de execuție al programului serial (neparalelizat)
  • măsurați timpii de execuție pentru un număr variabil de thread-uri (2, 3, ..., câte procesoare aveți)
  • calculați Speedup-ul pentru fiecare configurație.

Timpii de execuție măsurați ar putea varia (pentru aceleași valori ale lui N și P) de la o rulare la alta. În acest caz, se recomandă să facem mai multe rulări și să folosim media valorilor măsurate (sau alți indicatori statistici relevanți).

sugestie
5. Găsiți aici o metodă de a măsura timpul scurs între 2 puncte dintr-un program.
#include <time.h>

struct timespec start, finish;
double elapsed;
clock_gettime(CLOCK_MONOTONIC, &start);

WORK();

clock_gettime(CLOCK_MONOTONIC, &finish);
elapsed = (finish.tv_sec - start.tv_sec);
elapsed += (finish.tv_nsec - start.tv_nsec) / 1000000000.0;
sugestie
  1. O metodă bună de a face debugging la un program C multi-threaded este utiliarul gdb. Pe lângă comenzile gdb pe care le știți deja, ar mai fi de interes comenzile info threads (care afișează informații despre thread-urile existente la momentul curent de timp) și thread <N> (care mută contextul de execuție pe thread-ul N).