640 likes | 831 Views
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
E N D
Parallel Programmingin 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 • OpenMP fonctionne avec Fortran, C, ou C++
Modèle à mémoire partagée Processeur Processeur Processeur Processeur Mémoire 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é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
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.
Comment le système sait-il combien de threads il faut créer? Variable de l’environnement: OMP_NUM_THREADS 4 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érodu 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 • La modification d’une valeur est effective aussi pour les autres itérations.
Sections critiques Exemple: Approximation de 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;
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
Pragma “critical” • Section critique: portion de code qui ne peut être exécuté que par un seul thread à la fois. • On met#pragma omp criticaldevant le bloc de code C.
Exemple 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; #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>)
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.
Exemple for (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 for for (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” ou export 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:
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
Exemple for (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