parallel programming in c with mpi and openmp
DESCRIPTION
Parallel Programming in C with MPI and OpenMP. Chapitre 17. Open MP. Michael J. Quinn. OpenMP. OpenMP: Interface de programmation (API) pour le calcul parallèle sur architecture à mémoire partagée. Directives pour le compilateur Bibliothèque logicielle Variables de l ’ environnement - PowerPoint PPT PresentationTRANSCRIPT
Parallel Programmingin C with MPI and OpenMPMichael J. Quinn
Chapitre 17Open MP
OpenMP OpenMP: Interface de programmation (API)
pour le calcul parallèle sur architecture à mémoire partagée. Directives pour le compilateur Bibliothèque logicielle Variables de l’environnement
OpenMP fonctionne avec Fortran, C, ou C++
Modèle à mémoire partagée
P r o c es s o r P r o c es s o r P r o c es s o r P r o c es s o r
M em o r y
Les processeurs interagissent et se synchronisent à l’aide de variables partagées.
Parallélisme avec Fork et Join
Initialement un seul thread est actif (maître) Le maître exécute le code séquentiel. Fork: Le maître crée ou active des threads
additionnels afin d’exécuter du code en parallèle.
Join: À la fin du code parallèle, les threads sont éliminés ou suspendus et le flot de contrôle retourne à l’unique thread maître.
Parallélisme avec Fork et Join
Time
f o rk
jo in
M as ter T h r ead
fo rk
jo in
O th er th r ead s
Parallélisation incrémentielle
Programme séquentiel: Cas particulier d’un programme parallèle à mémoire partagée.
Parallélisation incrémentielle: On transforme un programe séquentiel en programme parallèle de façon graduelle.
Le parallélisme incrémentiel est un avantage important de la programmation parallèle à mémoire partagée.
Boucle for parallèle En C le parallélisme de données est souvent exprimé à l’aide de boucles for:
for (i = first; i < size; i += prime)
marked[i] = 1;
Avec OpenMP il est facile d’indiquer quand une boucle doit être exécuté en parallèle. Le compilateur se charge de transformer le code séquentiel en code parallèle:
création des threads affectation des itérations aux threads.
Pragmas Pragma: Directive au compilateur C ou C++ Signifie “pragmatic information”
Permet au programmeur de communiquer avec le compilateur
Le compilateur est libre d’ignorer les directives
Syntaxe:#pragma omp <reste du pragma>
Parallel for#pragma omp parallel for [clause [[,] clause …]
for (i = 0; i < N; i++)
a[i] = b[i] + c[i];
Le compilateur doit être en mesure de vérifier si le système d’exécution aura l’information nécessaire à l’ordonnancement des itérations de la boucle. Indépendance des itérations Nombre d’itérations
Forme canonique d’une boucle
parallel for
Variables privées et partagées
Variable partagée: Même adresse mémoire pour tous les threads
Variable privée: Différentes adresses mémoire pour différents threads.
Un thread ne peut pas accéder à une variable privée appartenant à un autre thread.
Par défaut, dans un “parallel for”, les variables sont partagées sauf l’indice de boucle.
Variables privées et partagées
in t m ain ( in t a rg c , c h ar * ar g v [ ] ){
in t b [ 3 ] ; c h ar * c p tr ;
in t i;
c p tr = m allo c (1 ) ;# p r ag m a o m p p ara lle l f o r
f o r ( i = 0 ; i < 3 ; i+ + ) b [ i] = i;
Heap
S tac k
cptrb i
ii
M as te r Thre ad (Thre ad 0 )
Thre ad 1…}
Comment le système sait-il combien de threads il faut
créer?Variable de l’environnement: OMP_NUM_THREADS4 fonctions utiles: omp_get_num_procs
omp_set_num_threads
omp_get_num_threads
omp_get_thread_num
Fonction omp_get_num_procs
Retourne le nombre de processeurs (physique ou virtuels) disponibles par le programme parallèle.
int omp_get_num_procs (void)
Fonction omp_set_num_threads
Le nombre de threads actifs dans les section de code parallèle sera égal au paramètre de la fonction
Peut être appelé à plusieurs endroits dans le programme.
void omp_set_num_threads (int t)
Fonction omp_get_num_threads
Retourne le nombre de threads actifs.
int omp_get_num_threads (void)
Fonction omp_get_thread_num
Retourne le numéro du thread.
int omp_get_thread_num(void)
Déclarer des variables privées
Exemple: Algorithme de Floyd
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
a[i][j] = MIN(a[i][j],a[i][k]+a[k][j]);
N’importe laquelle des deux boucles peut être exécutée en parallèle (exécuter les deux en parallèle nécessite trop de threads) On préfère paralléliser la boucle extérieure pour minimiser le nombre de fork/join Chaque thread doit alors posséder sa propre copie de la variable j
Clause “private” Clause: Composante optionnelle à un
pragma Clause “Private”: indique au compilateur de
créer une ou plusieurs variables privées.
private ( <variable list> )
Exemple
#pragma omp parallel for private(j)for (i = 0; i < n; i++) for (j = 0; j < n; j++) a[i][j] = MIN(a[i][j],a[i][k]+a[k][j]);
Clause “firstprivate” Pour créer une variable privée dont la valeur
initiale est identique à celle du thread maître avant d’entrée dans la boucle.
Les variable sont initialisées une seule fois pour chaque thread et non pas à chaque itération
La modification d’une valeur est effective aussi pour les autres itérations exécutées par un thread donné.
Sections critiques
double area, pi, x;int i, n;...area = 0.0;for (i = 0; i < n; i++) { x += (i+0.5)/n; area += 4.0/(1.0 + x*x);}pi = area / n;
Exemple: Approximation de
Condition de concurrence
Si on ne fait que paralléliser la boucle...
double area, pi, x;int i, n;...area = 0.0;#pragma omp parallel for private(x)for (i = 0; i < n; i++) { x = (i+0.5)/n; area += 4.0/(1.0 + x*x);}pi = area / n;
Condition de concurrence
... On obtient une condition de concurrence pour modifier la variable area
T h r ead A T h r ead BValu e o f a r ea
1 1 .6 6 7+ 3 .7 6 5
+ 3 .5 6 3
1 1 .6 6 7
1 5 .4 3 2
1 5 .2 3 0
Pragma “critical” Section critique: portion de code qui ne
peut être exécuté que par un seul thread à la fois.
On met
#pragma omp critical
devant le bloc de code C.
Exempledouble area, pi, x;int i, n;...area = 0.0;#pragma omp parallel for private(x)for (i = 0; i < n; i++) { x = (i+0.5)/n;#pragma omp critical area += 4.0/(1.0 + x*x);}pi = area / n;
Correct mais inefficace!
Réductions Une réduction est l’application d’une opération
associative sur les éléments d’un vecteur Les réductions sont si courantes que OpenMP
fourni un mécanisme facilitant son application. On peut ajouter une clause de réduction au
pragma parallel for On doit spécifier l’opération de réduction et la
variable sur laquelle s’applique la réduction OpenMP s’occupe de stocker les résultats
partiels dans des variables privées.
Clause “Reduction” La clause réduction a la syntaxe
suivante:reduction (<op> :<variable>)
Opérateur Valeur initiale+ 0
- 0
* 1
& Tous les bits à 1
| Tous les bits à 0
^ Tous les bits à 0
&& 1
|| 0
max Maximum possible
min Minimum possible
Exemple 1
double area, pi, x;int i, n;...area = 0.0;#pragma omp parallel for private(x) reduction(+:area)for (i = 0; i < n; i++) { x = (i + 0.5)/n; area += 4.0/(1.0 + x*x);}pi = area / n;
Exemple 2#include <math.h> void reduction1(float *x, int *y, int n) { int i, b, c; float a, d; a = 0.0; b = 0; c = y[0]; d = x[0]; #pragma omp parallel for private(i) shared(x, y, n) \ reduction(+:a) reduction(^:b) \ reduction(min:c) reduction(max:d) for (i=0; i<n; i++) { a += x[i]; b ^= y[i]; if(c>y[i])c=y[i]; d = fmaxf(d,x[i]); }}
Amélioration de la performance #1
Quelques fois, transformer une boucle for séquentielle en boucle for parallèle peut dégrader les performances
Le problème est que la transformation peut ajouter trop de “fork” et “join” par rapport au reste du calcul.
Quelques fois, inverser deux boucles inbriquées peut aider si: Le parallélisme est dans la boucle interne Après l’inversion, la boucle extérieure peut être
parallélisée L’inversion n’augmente pas trop les défauts de
caches.
Exemplefor (i=1; i<m; i++) for (j=0; j<n; j++) a[i][j]= 2*a[i-1][j];
for (i=1; i<m; i++)#pragma omp parallel for for (j=0; j<n; j++) a[i][j]= 2*a[i-1][j];
#pragma omp parallel forfor (j=0; j<n; j++) for (j=1; j<m; i++) a[i][j]= 2*a[i-1][j];
Plusieurs fork/join
Plus de défauts de cache
Amélioration de la performance #2
Lorsqu’une boucle a peu d’itérations, le temps supplémentaire des fork/join devient plus grand que le temps que l’on veut sauver par le parallélisme
La clause if indique au compilateur d’utiliser le parallélisme sous certaines conditions
#pragma omp parallel for if(n > 5000)
Amélioration de la performance #3
Il est possible de choisir de quelle façon les itérations d’une boucle for seront affectées aux threads à l’aide de la clause schedule
On parlera d’ordonnancement des itérations Il y a deux principaux types
d’ordonnancement: Statique: L’ordonnancement est déterminé
avant l’exécution Dynamique: L’ordonnancement est faite en
cours d’exécution
Ordonnancement statique ou dynamique
Ordonnancement statique Pas de charge de travail supplémentaire La charge de travail peut être mal
équilibrée Ordonnancement dynamique
Charge de travail supplémentaire Peut équilibrer la charge de travail
Segments (chunks) Un segment est une suite d’itérations
contiguës Augmenter la taille des segments réduit la
charge supplémentaire de travail Décroitre la taille des segments permet de
mieux équilibrer la charge de travail entre les threads.
Clause “schedule” Syntaxe:
schedule (<type>[,<segment> ])
Types permis: static: ordonnancement statique dynamic: ordonnancement dynamique guided: La taille des segments décroit
graduellement runtime: Le type est choisit à l’exécution en
fonction de la variable de l’environnement OMP_SCHEDULE
Options schedule(static): La taille des segments est
environ n/t schedule(static,C): La taille des segments
est C schedule(dynamic): Une itération à la fois schedule(dynamic,C): C itérations à la fois
Options (suite) schedule(guided, C):
Ordonnancement dynamique, la taille des segments diminue graduellement jusqu’à C
schedule(guided): C=1 schedule(runtime): Dépend de la
variable OMP_SCHEDULE; Exemple en Unix:setenv OMP_SCHEDULE “static,1”
ouexport OMP_SCHEDULE=“static,1”
Autres formes de parallélisme
Jusqu’à maintenant, l’emphase a été mise sur la parallélisation des boucles for.
Parallélisme de données Nous allons voir d’autres situations favorables
au parallélisme de données:
Traitement d’une liste de tâches
Code séquentiel (1/2)int main (int argc, char *argv[]){ struct job_struct *job_ptr; struct task_struct *task_ptr; ... task_ptr = get_next_task (&job_ptr); while (task_ptr != NULL) { complete_task (task_ptr); task_ptr = get_next_task (&job_ptr); } ...}
Code séquentiel (2/2)struct task_struct* get_next_task(
struct job_struct **job_ptr ) { struct task_struct *answer;
if (*job_ptr == NULL) answer = NULL; else { answer = (*job_ptr)->task; *job_ptr = (*job_ptr)->next; } return answer;}
Stratégie de parallélisation
Chaque thread prend la prochaine tâche dans la liste et la complète. Cela est répété jusqu’à ce qu’il n’y ait plus de tâche.
On doit s’assurer que deux threads ne prennent pas la même tâche.
On doit donc définir une section critique.
Le pragma “parallel” Précède un bloc de code devant être
exécuté par tous les threads.#pragma omp parallel
Note: Tous les threads exécutent le même code
Code parallel (1/2)int main (int argc, char *argv[]){ struct job_struct *job_ptr; struct task_struct *task_ptr; ...#pragma omp parallel private(task_ptr) { task_ptr = get_next_task (&job_ptr); while (task_ptr != NULL) { complete_task (task_ptr); task_ptr = get_next_task (&job_ptr); } } ...}
Code parallel (2/2)
char *get_next_task(struct job_struct **job_ptr) { struct task_struct *answer;#pragma omp critical { if (*job_ptr == NULL) answer = NULL; else { answer = (*job_ptr)->task; *job_ptr = (*job_ptr)->next; } } return answer;}
Le pragma “for” Le pragma “parallel” demande à tous les
threads d’exécuter tout le code dans le bloc.
Si le bloc contient une boucle for que l’on voudrait diviser entre les threads alors on peut utiliser le pragma “for”
#pragma omp for
Exemplefor (i = 0; i < m; i++) { low = a[i]; high = b[i]; if (low > high) { printf ("Exiting (%d)\n", i); break; } for (j = low; j < high; j++) c[j] = (c[j] - a[i])/b[i];}
• La première boucle for ne peut pas être parallélisée• Paralléliser la seconde boucle est inefficace• Le pragma « parallel » seul est insuffisant
Exemple#pragma omp parallel private(i,j){for (i = 0; i < m; i++) { low = a[i]; high = b[i]; if (low > high) { printf ("Exiting (%d)\n", i); break; }#pragma omp for for (j = low; j < high; j++) c[j] = (c[j] - a[i])/b[i];}
Le pragma “single” Dans certaines situation on veut qu’un seul
thread exécute une certaine instruction Par exemple, un message en sortie
C’est le rôle du pragma “single”
Syntaxe:#pragma omp single
Exemple#pragma omp parallel private(i,j,low,high)for (i = 0; i < m; i++) { low = a[i]; high = b[i]; if (low > high) {#pragma omp single printf ("Exiting (%d)\n", i); break; }#pragma omp for for (j = low; j < high; j++) c[j] = (c[j] - a[i])/b[i];}
Clause “nowait” Le compilateur place une barrière de
synchronisation à la fin de chaque bloc couvert par un pragma de type single, parallel ou for.
La plupart du temps cela est nécessaire On peut enlever la barrière à l’aide de la
clause nowait
Exemple#pragma omp parallel private(i,j, low, high){for (i = 0; i < m; i++) { low = a[i]; high = b[i];
#pragma omp single if (low > high) { printf ("Exiting (%d)\n", i); break; }#pragma omp for nowait for (j = low; j < high; j++) c[j] = (c[j] - a[i])/b[i];}
Parallélisme de contrôle Nous n’avons vu jusqu’à maintenant que le
parallélisme de données OpenMP permet ausi d’affecter différentes
portions du code à différents threads
Exemple v = alpha(); w = beta(); x = gamma(v, w); y = delta(); printf ("%6.2f\n", epsilon(x,y));
a lp h a b eta
g am m a d e lta
ep s ilo n
Le pragma “parallel sections”
Précède un bloc contenant k blocs devant être exécutés en parallèle par k threads (1 thread par section)
Syntaxe:
#pragma omp parallel sections
Le pragma “section” Précède chacun des blocs à l’intérieur d’un
bloc couvert par le pragma “parallel sections”
Peut être omis pour le premier bloc Syntaxe:
#pragma omp section
Exemple#pragma omp parallel sections {#pragma omp section /* Optionnel */ v = alpha();#pragma omp section w = beta();#pragma omp section y = delta(); } x = gamma(v, w); printf ("%6.2f\n", epsilon(x,y));
Autre approche
alp h a b e ta
g am m a d elta
ep s ilo n
Deux blocs:• alpha et beta• gamma et delta
Le pragma “sections” Apparaît à l’intérieur d’un bloc de code
couvert par le pragma “parallel” Possède la même signification que le
pragma “parallel sections” Ajoute de la flexibilité à la façon
d ’organiser le parallélisme
Exemple#pragma omp parallel { #pragma omp sections { v = alpha(); #pragma omp section w = beta(); } #pragma omp sections { x = gamma(v, w); #pragma omp section y = delta(); } } printf ("%6.2f\n", epsilon(x,y));
Quelques limitations avant OpenMP 3.0
2 exemples:
1.Parcours d’une liste chaînée2.Appels récursifs
Parcours d’une liste chaînée
Une solution serait de transformer la liste en tableau: (perte de temps)
Parcours d’une liste chaînée
Autre solution: (les threads boucles inutilement)
Appels récursifs
• La création récursive des threads requiert trop de ressources• Comment obtenir une charge de travail équilibrée ?
La directive « task »#pragma omp task bloc d’instructions
Une tâche est une instance d’une partie de code exécutable et de la mémoire associée.
Une tâche est générée lorsqu’un thread rencontre une directive task ou parallel
La directive « task »#pragma omp task bloc d’instructions
Lorsqu’un thread rencontre une directive task il créé une tâche avec le bloc d’instructions suivant qu’il peut exécuter immédiatement ou reporter à plus tard.
Une tâche reportée peut être exécutée par n’importe quel thread du groupe actifs
Exemple 1#pragma omp parallel {#pragma omp sections {#pragma omp section printf("1 ");#pragma omp section printf("2 ");#pragma omp section printf("3 ");#pragma omp section printf("4 "); }}
bash$ ./a.out3 1 2 4
Exemple 2#pragma omp parallel {#pragma omp task printf("1 %d: ", omp_get_thread_num());#pragma omp task printf("2 %d: ", omp_get_thread_num());#pragma omp task printf("3 %d: ", omp_get_thread_num());#pragma omp task printf("4 %d: ", omp_get_thread_num()); }
bash$ ./a.out1 0: 2 0: 3 0: 4 0: 1 0: 2 0: 3 0: 4 6: 1 6: 2 6: 1 6: 1 6: 2 0: 3 6: 2 7: 3 7: 4 2: 4 6: 1 2: 1 2: 1 6: 4 7: 3 3: 2 2: 2 6: 3 0: 3 0: 4 2: 2 4: 4 5: 4 7: 3 6:
Exemple 3#pragma omp parallel { #pragma omp single {#pragma omp task printf("1 %d: ", omp_get_thread_num());#pragma omp task printf("2 %d: ", omp_get_thread_num());#pragma omp task printf("3 %d: ", omp_get_thread_num());#pragma omp task printf("4 %d: ", omp_get_thread_num()); }}
bash$ ./a.out1 6: 2 7: 3 5: 4 1:
Ex. Parcours d’une liste chaînée
Ex. Parcours de plusieurs listes
Ex. Fonction récursiveint fib(int n){ int a,b; if (n<2) return n;#pragma omp task shared(a) a=fib(n-1);#pragma omp task shared(b) b=fib(n-2);#pragma omp taskwait return a+b;}
Comment doit-on appeler fib la première fois?