OpenMP
Description des directives OpenMP pour la parallélisation à mémoire partagée
OpenMP
Directives OpenMP
Compilation et exécution
Constructions parallèles
Synchronisations
Clauses des directives
Bibliothèque de routines
Variables d'environnement
Programmes exemples
Tuning
Avantages et inconvénients
Directives OpenMP
Toutes les directives OpenMP insérées dans le source sont identifiées à l'aide d'un préfixe, une sentinelle, qui est un commentaire Fortran : !$OMP, C$OMP et *$OMP.La syntaxe d'une directive est la suivante :
Les caractères entre [ ] sont optionnels. Les clauses sont écrites dans un ordre quelconque après le nom de la directive. Les directives peuvent s'étendre sur plusieurs lignes en respectant la syntaxe Fortran pour les continuations. Exemple :préfixe directive [clause[[,] clause] ... ]
!23456789
!$OMP PARALLEL DO SHARED (A, B, C)!$OMP PARALLEL DO &
!$OMP SHARED (A, B, C)!$OMP PARALLELDOSHARED(A,B,C)
Ces directives OpenMP sont reconnues et équivalentes. Toutefois il est préférable de n'utiliser que la forme !$OMP car elle seule est supportée par les formats fixe et libre du Fortran. En format libre, la poursuite d'une directive sur plusieurs lignes s'écrit :
L'appel à des functions OpenMP, la déclaration de variables se font aussi avec des sentinelles pour qu'elles ne soient prises en compte qu'en cas de compilation parallèle : ils sont vus comme des commentaires dans le cas d'une compilation séquentielle ; par exemple :!23456789
!$OMP PARALLEL DO &
!$OMP SHARED (U, V, W)
La variable nbproc est initialisée à un, elle contient le nombre de threads de l'application. Si la compilation du code active les directives OpenMP, les sentinelles !$ sont remplacées par des caractères blancs et ce qui les suit n'est plus vu comme un commentaire. Dans ce cas, la function OMP_NUM_THREADS est déclarée et employée ; elle renvoye le nombre de threads stocké dans nbproc.INTEGER :: nbproc!$ INTEGER :: OMP_NUM_THREADS
!$ EXTERNAL OMP_NUM_THREADSnbproc = 1
!$ nbproc = OMP_NUM_THREADS()
Compilation et exécution
La compilation d'un code avec des directives OpenMP nécessite les options suivantes :- -qsmp=omp (compilateur xlf), -openmp (compilateur Intel), -fopenmp (compilateur Gnu) : multiprocessing, i.e. compilation d'un code parallèle en mémoire partagée,
- -O3 : niveau minimal pour une bonne efficacité desoptimisations.
L'exécution du code parallélisé par OpenMP se fait comme un code séquentiel, pour déterminer le nombre de threads (processus) du code, le développeur a deux possibilités :
- valeur fixe mise dans le code lors de son écriture (fonction OMP_SET_NUM_THREADS),
- valeur mise dans la variable d'environnement OMP_NUM_THREADS puis lue dans le code par un appel à OMP_GET_NUM_THREADS.
Constructions parallèles
Les directives PARALLEL et END PARALLEL définissent une région parallèle, c'est-à-dire un bloc de code qui doit être exécuté en parallèle par plusieurs threads. C'est la construction parallèle fondamentale d'OpenMP. Ces directives ont la syntaxe suivante :La clause est l'une des suivantes :!$OMP PARALLEL [clause[[,] clause]...]block
!$OMP END PARALLEL
- PRIVATE (liste)
- SHARED (liste)
- DEFAULT (PRIVATE | SHARED | NONE)
- FIRSTPRIVATE (liste)
- REDUCTION ( {operateur|intrinsèque}:liste)
- IF (expression_scalaire_logique)
- COPYIN (liste)
Lorsqu'une tâche arrive dans une région parallèle, elle créée une équipe de threads dont elle devient le maitre (master thread) et prend le numéro 0 au sein de celle-ci. Le nombre de threads dans l'équipe est contrôlé par des variables d'environnement (par exemple OMP_NUM_THREADS) ou bien par des routines de la bibliothèque OpenMP (par exemple OMP_SET_NUM_THREADS). Une fois créée, le nombre de tâches dans l'équipe est constant pour toute la zone parallèle, mais il peut être modifié par l'utilisateur ou par des appels à la bibliothèque OpenMP entre deux régions parallèles.
block désigne un groupe structuré d'instructions Fortran. On ne peut pas entrer ou sortir d'un tel bloc d'instructions au moyen d'un goto. Le code contenu dans le block est exécuté par chaque tâche dans la région parallèle.
La directive END PARALLEL clôt la région parallèle. Il y a une barrière de synchronisation intrinsèque à ce niveau. Ensuite, seule la master thread continue l'exécution du programme.
Il faut noter que les deux directives PARALLEL et END PARALLEL doivent apparaître dans le même sous-programme.
Exemple :
INTEGER :: IAM, NP
INTEGER :: IPOINTS, NPOINTS
REAL(rp) :: X
!
IAM = 0
NP = 1
!$OMP PARALLEL DEFAULT (NONE) &
!$OMP SHARED (X, NPOINTS) &
!$OMP PRIVATE (IAM, NP, IPOINTS)
!$ IAM = OMP_GET_THREAD_NUM ()
!$ NP = OMP_GET_NUM_THREADS ()
IPOINTS = NPOINTS / NP
CALL SUBDOMAIN (X, IAM, IPOINTS)
!$OMP END PARALLEL
Si une clause IF est présente, la région concernée n'est exécutée en parallèle que si l'expression_scalaire_logique est vraie. Autrement, la région parallèle est exécutée de manière séquentielle. Une seule clause IF peut apparaitre dans la directive.
Dans une région parallèle, il est possible de définir la répartition du travail entre les différentes threads de l'équipe, par exemple la distribution des itérations d'une boucle parallélisée. Il existe plusieurs manières de le faire :
La directive DO indique que les itérations qui figurent dans la boucle DO qui suit immédiatement seront exécutées en parallèle. Les paramètres de la boucle (valeurs initiale et finale du compteur et le pas d'incrémentation) doivent être fixés : il s'agit d'une boucle contrôlée. Elle ne peut pas s'appliquer à des boucles de longueur aléatoire du type DO WHILE. Sa syntaxe est la suivante :Les clauses sont parmi les suivantes :!$OMP DO [clause[[,] clause]...]do_loop
[!$OMP END DO [NOWAIT]]
- PRIVATE (liste)
- FIRSTPRIVATE (liste)
- LASTPRIVATE (liste)
- REDUCTION ( {operateur|intrinsèque}:liste)
- SCHEDULE (type[,chunk])
- ORDERED
La clause SCHEDULE spécifie la manière de partager les itérations entre les threads. Au sein de cette clause, chunk est un entier et type peut être :
- STATIC : SCHEDULE (STATIC,chunk) impose un découpage du nombre d'itérations en blocs de taille chunk. Ces blocs sont attribués aux différentes threads de manière statique en suivant une distribution de type round-robin dans l'ordre des numéros des threads. En l'absence de chunk, les itérations sont réparties parmi les threads en bloc continu.
- DYNAMIC : SCHEDULE (DYNAMIC,chunk) impose le découpage du nombre d'itérations en blocs de taille chunk. Lorsque chaque thread a fini son bloc, elle reçoit dynamiquement le prochain groupe. Lorsque chunk est omis, la valeur par défaut est 1.
- GUIDED : SCHEDULE (GUIDED,chunk) impose une décroissance exponentielle de la taille chunk des paquets d'itérations qui sont distribués. Lorsque chunk est omis, la valeur par défaut est 1.
- RUNTIME : SCHEDULE (RUNTIME) indique que la distribution (type et chunk) des itérations sera effectuée durant l'exécution et ces paramètres seront fixés à l'aide de la variable d'environnementOMP_SCHEDULE. Le paramètre chunk doit être omis.
La clause ORDERED agit de la même manière que la directive ORDERED, voir la rubrique Synchronisations .
La directive END DO est optionnelle et seule la boucle suivant la directive DO est sujette à celle-ci et donc parallélisée. Si NOWAIT complète la directive END DO alors les threads ne sont pas synchronisées à la fin de la boucle parallèle ; cela permet aux premières qui ont fini leurs itérations de poursuivre l'exécution sans attendre les dernières threads de l'équipe. Autrement, la directive doit suivre immédiatement la fin de la boucle.
Exemple :
Le compteur d'une boucle parallélisée peut apparaitre dans une clause LASTPRIVATE d'une boucle, alors une variable de même nom doit être spécifiée dans la clause SHARED de la région parallèle qui englobe cette boucle.!$OMP PARALLEL
!$OMP DO
DO I = 2, N
B (I) = 0.5_rp * ( A(I) + A (I-1) )
END DO
!$OMP END DO NOWAIT
!$OMP DO
DO I = 1, M
Y(I) = SQRT( Z(I) )
END DO
!$OMP END DO NOWAIT
!$OMP END PARALLEL
On ne peut pas entrer ou sortir d'une boucle munie d'une directive DO au moyen d'un goto. Une seule clause SCHEDULE ou ORDERED peut apparaitre dans une directive DO. La directive SECTIONS permet de distribuer du travail de manière non itérative, i.e. en le répartissant parmi les threads de l'équipe. Chaque section n'est exécutée qu'une fois et par une seule tâche. Le format de cette directive est le suivant :
La clause est l'une des suivantes :!$OMP SECTIONS [clause[[,] clause]...][!$OMP SECTION]
block
[!$OMP SECTION
block]
....
!$OMP END SECTIONS [NOWAIT]
- PRIVATE (liste)
- FIRSTPRIVATE (liste)
- LASTPRIVATE (liste)
- REDUCTION ( {operateur|intrinsèque}:liste)
Chaque section doit être précédée de la directive SECTION, bien qu'elle soit optionnelle pour la première. La directive SECTION ne peut apparaitre qu'entre les deux directives SECTIONS et END SECTIONS. La dernière section s'achève à la directive END SECTIONS. Les premières threads qui terminent leur section attendent les dernières à la barrière (implicite) du END SECTIONS à moins que NOWAIT ne soit présent.
Les instructions entourées par le couple de directives SECTIONS / END PARALLEL SECTIONS doivent former un bloc structuré (i.e. sans entrée ou sortie par goto). Il en est de même pour chacune des sections le constituant.
Exemple :
La directive SINGLE impose l'exécution du code imbriqué dans les directives SINGLE / END SINGLE par une seule thread dans l'équipe. La thread qui exécute cette zone est la première qui atteint la directive SINGLE. On ne peut ni l'imposer, ni savoir à l'avance celle qui exécutera les instructions. La syntaxe est la suivante :!$OMP PARALLEL SECTIONS
!$OMP SECTION
CALL XAXIS
!$OMP SECTION
CALL YAXIS
!$OMP SECTION
CALL ZAXIS
!$OMP END PARALLEL SECTIONS
La clause est l'une des suivantes :!$OMP SINGLE [clause[[,] clause]...]block
!$OMP END SINGLE [NOWAIT]
- PRIVATE (liste)
- FIRSTPRIVATE (liste)
Les instructions entourées par le couple de directives SINGLE / END SINGLE doivent former un bloc structuré (i.e. sans entrée ou sortie par goto).
Exemple :
!$OMP PARALLEL DEFAULT (SHARED)
CALL WORK (X)
!$OMP BARRIER
!$OMP SINGLE
CALL OUTPUT (X)
CALL INPUT (Y)
!$OMP END SINGLE
CALL WORK (Y)
!$OMP END PARALLEL
Il est possible de simplifier les directives si la région parallèle ne contient qu'une seule construction à exécuter :
Cette directive fournit une version courte d'une boucle DO à exécuter en parallèle. Son format est :La clause doit être acceptée par les directives PARALLEL et DO avec leurs contraintes respectives.!$OMP PARALLEL DO [clause[[,] clause]...]do_loop
[!$OMP END PARALLEL DO]
Exemple :
!$OMP PARALLEL DO
DO I = 2, N
B (I) = 0.5_rp * ( A(I) + A (I-1) )
END DO
!$OMP END PARALLEL DO
Synchronisations
Il existe plusieurs mécanismes de synchronisation des threads d'une équipe. Le bloc d'instructions compris entre les directives MASTER et END MASTER sont exécutées par la master thread. Le format de cette directive est le suivant :Les autres threads de l'équipe ignorent le bloc inclus et continuent l'exécution. Il n'y a pas de barrière implicite à l'entrée ou à la sortie d'une telle section, qui doit être un bloc structuré. Les directives CRITICAL et END CRITICAL restreignent l'accès d'un bloc du code à une seule thread à la fois. Leur syntaxe est :!$OMP MASTERblock
!$OMP END MASTER
L'argument optionel nom identifie la section critique. Une thread attend au début d'une section critique jusqu'à ce qu'aucune autre thread de l'équipe n'exécute une section critique du même nom. Toutes les directives CRITICAL correspondent à la même région critique. Leurs noms sont des entités globales du programme. L'argument nom doit figurer dans les deux directives CRITICAL et END CRITICAL. block est un bloc d'instructions structuré. La directive BARRIER synchronise toutes les threads dans une équipe. Quand une thread rencontre cette directive, elle attend que toutes les autres threads de l'équipe atteignent le même endroit. Cette directive s'écrit :!$OMP CRITICAL [(nom)]block
!$OMP END CRITICAL [(nom)]
La directive ATOMIC assure qu'une zone mémoire ne peut être mise à jour simultanément par plusieurs threads. Elle ne s'applique qu'à une seule instruction qui suit immédiatement la directive. Elle s'écrit :!$OMP BARRIER
Elle s'applique seulement pour l'une des situations suivantes :!$OMP ATOMIC
- x = x opérateur expression
- x = expression opérateur x
- x = fonction intrinsèque (x, expression)
- x = fonction intrinsèque (expression, x)
- x est une variable scalaire de type simple
- expression est une expression scalaire ne faisant pas référence à x
- fonction intrinsèque est une fonction parmi MAX, MIN, IAND, IOR, IEOR
- opérateur désigne l'un des opérandes suivants : +, -, *, /, .AND., .OR., .EQV., .NEQV.
Toutes les références à l'adresse de stockage x doivent être de même type
Exemple :
Ici la mise à jour de Y n'est pas concernée par la directive. La directive FLUSH permet d'imposer des points de synchronisation où les variables visibles par les threads sont réellement écrites en mémoire. Par exemple, le compilateur met à jour les valeurs en mémoire à partir des registres et les tampons des fichiers sont écrits. Cela concerne entre autres :!$OMP PARALLEL DO DEFAULT (NONE) &
!$OMP PRIVATE (XLOCAL, YLOCAL, I) &
!$OMP SHARED (X, Y, INDEX, N)
DO I = 1, N
CALL WORK (XLOCAL, YLOCAL)
!$OMP ATOMIC
X( INDEX(I) ) = X( INDEX (I) ) + XLOCAL
Y (I) = Y (I) + YLOCAL
END DO
!$OMP END PARALLEL DO
- les variables globalement visibles (common),
- les variables locales dotées de l'attribut SAVE,
- les variables locales sans l'attribut SAVE mais déclarées SHARED.
La directive doit être placée de manière précise, La liste optionnelle correspond aux variables qui doivent être mises à jour. Cette directive est implicite pour les directives suivantes :!$OMP FLUSH [(liste)]
- BARRIER,
- CRITICAL et END CRITICAL,
- END DO,
- END PARALLEL,
- END SECTIONS,
- END SINGLE,
- ORDERED et END ORDERED;
Exemple :
Une section de code comprise entre les directives ORDERED et END ORDERED est exécutée dans le même ordre que pour une exécution séquentielle de la boucle. Le format est le suivant :IAM = 0
!$OMP PARALLEL DEFAULT(PRIVATE), SHARED (ISYNC)
!$ IAM = OMP_GET_THREAD_NUM()
INEIGH = IAM + 1
IF ( INEIGH == OMP_NUM_THREADS() ) INEIGH = 0
ISYNC (IAM) = 0
!$OMP BARRIER
CALL WORK()
! Synchronisation avec une autre thread
ISYNC(IAM) = 1
!$OMP FLUSH (ISYNC)
DO WHILE ( ISYNC(INEIGH) .==. 0 )
!$OMP FLUSH (ISYNC)
END DO
!$OMP END PARALLEL
Une directive ORDERED ne peut apparaitre qu'après une directive DO ou PARALLEL DO. Dans ce cas, la directive DO doit impérativement contenir la clause ORDERED. Une seule thread peut accéder à une section ORDERED à la fois et cela se fait dans l'ordre des itérations de la boucle et seulement lorsque toutes les itérations précédentes sont finies. Cela exécute de telles sections de code en séquentiel mais permet l'exécution en parallèle des portions de code à l'extérieur de ces directives. Les sections ORDERED correspondant à des boucles DO différentes sont indépendantes.!$OMP ORDEREDblock
!$OMP END ORDERED
La région de code incluse entre les directives ORDERED et END ORDERED doit être un bloc structuré.
Clauses des directives
Plusieurs directives OpenMP admettent des clauses pour permettre au développeur de contrôler la visibilité des variables au sein de la construction parallèle. Toutes les clauses qui suivent ne s'appliquent pas à toutes les directives. Chaque clause accepte une liste d'arguments, c'est-à-dire une liste de noms de variables ou de blocs commons (noms écrits entre slashes "/") séparés par des virgules ; toutefois cela ne concerne pas des champs d'objets. La clause PRIVATE rend ses arguments privés à chaque thread. Le format est :Le comportement d'une variable déclarée dans une clause PRIVATE est le suivant :PRIVATE (liste)
- un nouvel objet du même type est déclaré pour chaque thread de l'équipe; il n'est plus associé (au sens du stockage) à la variable initiale ;
- toutes les références à l'objet initial sont remplacées par des références à l'objet privé et cela dans la zone couverte par la clause ;
- les variables déclarées PRIVATE sont indéfinies pour chaque thread entrant la construction parallèle ;
La clause DEFAULT permet au développeur d'imposer un attribut PRIVATE,SHARED ou NONE à toutes les variables se situant dans la région parallèle. Les variables ayant la clause THREADPRIVATE ne sont pas concernées. La syntaxe est :SHARED (liste)
Les clauses PRIVATE, SHARED ou NONE ont les effects suivants :DEFAULT (PRIVATE | SHARED | NONE)
- préciser la clause DEFAULT (PRIVATE) rend privées à chaque thread toutes les variables de la région parallèle(blocs commons inclus mais pas les variables sous la clause THREADPRIVATE) comme si toutes les variables étaient dans une clause PRIVATE;
- préciser la clause DEFAULT (SHARED) rend communes à l'ensemble des threads toutes les variables de la région parallèle. Cette clause est activée par défaut en l'absence de clause DEFAULT ;
- préciser la clause DEFAULT (NONE) inhibe toute clause implicite, PRIVATE ou SHARED. Dans ce cas, toutes les variables de la région parallèle doivent être déclarées au moyen de clauses PRIVATE, SHARED, FIRSPRIVATE, LASTPRIVATE ou REDUCTION.
Il est fortement conseillé d'employer la clause DEFAULT(NONE). En effet elle joue un rôle équivalent à l'instruction IMPLICIT NONE du Fortran et impose ainsi une déclaration explicite de la visibilité pour chaque variable utilisée au sein de la région parallèle. Elle permet de ne rien oublier mais aussi de ne pas s'appuyer sur la visibilité par défault qui peut varier d'une implémentation à une autre.
Exemple :
La clause FIRSTPRIVATE englobe la clause PRIVATE et initialise les copies privées des variables avec la valeur de l'objet initial avant la construction parallèle. Sa syntaxe est :!$OMP PARALLEL DO DEFAULT(PRIVATE) &
!$OMP FIRSTPRIVATE (I) &
!$OMP SHARED (X) &
!$OMP SHARED (R) &
!$OMP LASTPRIVATE (I)
La clause LASTPRIVATE englobe la clause PRIVATE. Son action dépend de la construction parallèle sur laquelle elle agit :FIRSTPRIVATE (liste)
- dans le cas d'une directive DO, la thread qui exécute la dernière itération de la boucle (au sens séquentiel) met à jour les variables arguments de la clause ;
- dans le cas d'une directive SECTIONS, la thread qui exécute la dernière SECTION (au sens lexical) met à jour les variables arguments.
La clause REDUCTION effectue une opération de réduction sur les variables figurant dans sa liste d'arguments. Il 'sagit de calculer une somme, un maximum, etc ... sur un ensemble de données d'un ou plusieurs tableaux. Pour cela elle utilise l'opérateur opérateur ou la fonction intrinsèque intrinsèque, où opérateur est : +, *, -, .AND., .OR., .EQV., .NEQV., et intrinsèque une fonction parmi MAX,MIN, IAND, IOR, IEOR. Le format est le suivant :LASTPRIVATE (liste)
Les variables qui figurent dans la liste doivent être des variables scalaires de type intrinsèque et dotées de l'attribut SHARED dans la construction parallèle. Une copie locale de chaque variable de la liste est créée pour chaque thread comme sous l'effet d'une clause PRIVATE. La copie privée est initialisée selon le tableau suivant (adaptation selon le type de la variable) :REDUCTION ({opérateur|intrinsèque}:liste)
| Opérateur / fonction intrinsèque | Initialisation |
|---|---|
A la fin de la REDUCTION, la variable partagée est mise à jour en combinant la valeur originale de la variable de réduction avec la valeur finale de chacune de ses copies en utilisant l'opérateur spécifié. Hormis la soustraction, tous les opérateurs sont associatifs et le compilateur peut réordonner les calculs de la valeur finale (dans le cas d'une soustraction, il y a cumul des différentes contributions). La valeur d'une variable partagée devient indéfinie dès qu'une thread atteint la clause de REDUCTION et le demeure jusqu'à ce que le calcul de réduction soit terminé. Normalement, le calcul est fini à la fin de la REDUCTION; cependant si la clause REDUCTION est utilisée dans une construction munie d'une clause NOWAIT alors la variable partagée demeure indéfinie jusqu'à ce qu'une barrière de synchronisation soit atteinte pour s'assurer que toutes les threads ont bien accompli la clause REDUCTION.
Cette clause s'applique seulement dans une construction parallèle où la variable à réduire, x, n'est employée que dans l'une des situations suivantes :
- x = x opérateur expression
- x = expression opérateur x (sauf pour la soustraction)
- x = fonction intrinsèque (x, expression)
- x = fonction intrinsèque (expression, x)
Un bloc common n'est pas obligatoirement concerné dans son intégralité. Les noms des variables apparaissant dans le THREADPRIVATE bloc common figurent dans l'argument liste.COPYIN (liste)
Exemple :
Les deux blocs commons BLK1 et FIELDS sont déclarés THREADPRIVATE mais une seule variable dans le bloc common FIELDS sera concernée.COMMON /BLK1/ SCRATCH
COMMON /FIELDS/ XFIELD, YFIELD, ZFIELD!$OMP THREADPRIVATE (/BLK1/,/FIELDS/)
!$OMP DEFAULT (PRIVATE), COPYIN (/BLK1/, ZFIELD)
La directive THREADPRIVATE, bien que n'étant pas une clause, permet d'agir sur la visibilité de variables. Elle rend les blocs commons privés à une thread mais globalement visibles à l'intérieur de celle-ci. Elle doit figurer dans la partie des déclarations du sous-programme juste après les blocs commons concernés. Chaque thread possède ainsi sa propre copie du bloc common et une modification des valeurs par une thread n'est pas visible par les autres threads. Le format de cette directive est :
où cb est le nom d'un bloc common à rendre privé pour une thread.!$OMP THREADPRIVATE (/cb/[,/cb/] ...)
Il faut noter qu'un bloc common THREADPRIVATE (ou bien les variables qui le constituent) ne peut pas apparaître dans une autre clause que COPYIN, i.e. cela concerne les clauses
PRIVATE, FIRSTPRIVATE, LASTPRIVATE, SHARED, REDUCTION.Il n'est pas affecté par la clause DEFAULT.
Remarques :
De manière générale, les compteurs des boucles sont automatiquement privés, même en présence de la règle implicite DEFAULT (SHARED).
Si un bloc common est déclaré PRIVATE, FIRSTPRIVATE, LASTPRIVATE, aucun de ses éléments ne peut être déclaré à l'aide d'un autre attribut.
Les clauses peuvent être répétées si nécessaire mais chaque variable ne peut apparaître explicitement que dans uneclause par directive, à l'exception de :
- une variable peut être déclarée à la fois FIRSTPRIVATE et LASTPRIVATE,
- les variables affectées par la clause implicite DEFAULT peuvent figurer explicitement dans une clause pour passer outre cette déclaration.
Bibliothèque de routines OpenMP
La bibliothèque de routines OpenMP permet de controler et d'interroger l'environnement parallèle de l'exécution. Ce sont des routines externes. D'autre part, dans les déclarations suivantes l'expression expression_scalaire_entière correspond au type entier par défaut et l'expression expression_scalaire_logique est le type logique par défaut. Les valeurs retournées par les functions sont aussi les types par défaut. La routine OMP_SET_NUM_THREADS permet de fixer le nombre de threads pour la prochaine région parallèle. Sa séquence d'appel est :L'expression expression_scalaire_entière est évaluée et sa valeur utilisée comme le nombre de threads pour la prochaine région parallèle. Ce sous-programme n'a d'effet que s'il est appelé à partir d'une portion séquentielle du code. En cas d'appel d'une portion du code ou la function OMP_IN_PARALLEL renvoie la valeur .TRUE. le comportement de la routine n'est pas défini. Si l'ajustement dynamique du nombre de threads est activé, les appels à OMP_SET_NUM_THREADS fixent le nombre maximal de threads utilisés dans les prochaines régions parallèles.SUBROUTINE OMP_SET_NUM_THREADS (expression_scalaire_entière)
Un appel à OMP_SET_NUM_THREADS a priorité sur la variable d'environnement OMP_NUM_THREADS. La routine OMP_GET_NUM_THREADS renvoie le nombre de threads actuellement dans l'équipe exécutant une région parallèle d'ou provient l'appel. En cas d'appel d'une partie séquentielle du programme, la valeur retournée est 1. Son format est :
Si le nombre de threads n'est pas explicitement fixé par le développeur alors la valeur par défaut dépen du calculateur. Par exemple, en C-shell on initialise la variable OMP_NUM_THREADS (voir la section sur les variables d'environnement) de la manière suivante :INTEGER FUNCTION OMP_GET_NUM_THREADS ()
setenv OMP_NUM_THREADS 10
Dans le code, on a l'appel suivant à la fonction OMP_GET_NUM_THREADS, effectué par un seul processus, dans une région parallèle :
La function OMP_GET_MAX_THREADS renvoie la valeur maximale que peut transmettre la function OMP_GET_NUM_THREADS. Si la routine OMP_SET_NUM_THREADS est appelée pour changer le nombre de threads, les appels ultérieurs à OMP_GET_MAX_THREADS fourniront la nouvelle valeur. Cette function peut être appelée d'une zone séquentielle ou parallèle, i.e. elle a une portée globale. Son format est :NBTHRDS = 1
!$OMP PARALLEL DEFAULT (SHARED)
!$OMP SINGLE
!$ CALL OMP_GET_NUM_THREADS (NBTHDS)
!$OMP END SINGLE
....
....
!$OMP END PARALLEL
La function OMP_GET_NUM_PROCS renvoie le nombre de processeurs disponibles pour le programme. Le format de cette function est :INTEGER FUNCTION OMP_GET_MAX_THREADS ()
La function OMP_IN_PARALLEL renvoie la valeur .TRUE. si elle est appelée à partir d'une région exécutée en parallèle et .FALSE. dans le cas contraire. Une région parallèle qui est exécutée en séquentiel n'est pas considérée comme une région exécutée en parallèle. Le format de cette function est le suivant :INTEGER FUNCTION OMP_GET_NUM_PROCS ()
Cette function a une portée globale. La subroutine OMP_SET_DYNAMIC active ou désactive l'ajustement dynamique du nombre de threads disponibles pour l'exécution des régions parallèles. Le format de la subroutine est :LOGICAL FUNCTION OMP_IN_PARALLEL ()
Si l'expression_scalaire_logique est évaluée à .TRUE. alors le nombre de threads utilisables pour exécuter les prochaines régions parallèles peut être ajusté automatiquement par l'environnement d'exécution. Le nombre de threads ainsi fixé perdure après la sortie de chaque région parallèle et peut être connu à l'aide de la function OMP_GET_NUM_THREADS. Si l'expression_scalaire_logique est évaluée .FALSE. l'ajustement dynamique est désactivé.SUBROUTINE OMP_SET_DYNAMIC (expression_scalaire_logique)
Un appel à OMP_SET_DYNAMIC a priorité sur la variable d'environnement OMP_DYNAMIC. La valeur par défaut dépend du calculateur. Par conséquent, l'exécution d'un code nécessitant un nombre précis de threads doit se faire avec cette fonctionnalité explicitement désactivée.
La function OMP_GET_DYNAMIC renvoie .TRUE. si l'ajustement dynamique du nombre de threads est activé, autrement elle renvoie .FALSE. . Le format de cette function est :Si la mise en oeuvre ne supporte pas l'ajustement dynamique alors la function renvoie toujours .FALSE. . La subroutine OMP_SET_NESTED active ou désactive l'imbrication de régions parallèles. Le format de cette subroutine est le suivant :LOGICAL FUNCTION OMP_GET_DYNAMIC ()
Si l'expression expression_scalaire_logique est évaluée .FALSE., ce qui est la valeur par défaut, alors le parallélisme imbriqué (i.e. une région parallèle construite à l'intérieur d'une autre région parallèle) est désactivé et les régions parallèles "internes" sont exécutées en séquentiel par la thread courante. Dans le cas contraire, l'expression_scalaire_logique est évaluée .TRUE., alors le parallélisme imbriqué est activé et les régions parallèles "internes" peuvent construire des threads supplémentaires pour former une nouvelle équipe. Cet appel a priorité sur la variable d'environnement OMP_NESTED. Quand le parallélisme imbriqué est activé, le nombre de threads utilisées pour les régions parallèles "internes" dépend du calculateur. Sur le cluster IBM, le parallélisme imbriqué est supporté mais est déconseillée. La function OMP_GET_NESTED renvoie la valeur .TRUE. si le parallélisme imbriqué est activé et .FALSE. dans le cas contraire. Le format de cette function est :SUBROUTINE OMP_SET_NESTED (expression_scalaire_logique)
Si la mise en oeuvre ne supporte pas l'ajustement dynamique alors la function renvoie toujours .FALSE. .LOGICAL FUNCTION OMP_GET_NESTED ()
Variables d'environnement
Ces variables permettent de contrôler l'environnement d'exécution de l'extérieur du code. Les noms des variables d'environnement doivent être écrits en MAJUSCULES. Par contre les valeurs qui leur sont assignées sont insensibles aux majuscules/minuscules. Cette variable ne s'applique qu'aux directives DO et PARALLEL DO qui ont la clause SCHEDULE mise à RUNTIME. Le type de SCHEDULE (type) et la taille des paquets d'itérations (chunk) pour de telles boucles sont fixés en affectant à cette variable d'environnement n'importe quel type reconnu muni d'un chunk optionnel. Pour les directives DO et PARALLEL DO qui ont un autre type de clause SCHEDULE, cette variable est ignorée. La valeur par défaut dépend du calculateur.Exemple :
La variable d'environnement OMP_NUM_THREADS fixe le nombre de threads qui seront utilisées durant l'exécution, à moins que ce nombre ne soit changé par un appel à la subroutine OMP_SET_NUM_THREADS. Lorsque l'ajustement dynamique du nombre de threads est activé, la valeur est le nombre maximal de threads, mais la valeur par défaut dépend du calculateur.setenv OMP_SCHEDULE "GUIDED,4"
setenv OMP_SCHEDULE "dynamic"
Exemple :
Cette variable active ou désactive l'ajustement dynamique du nombre de threads disponibles pour l'exécution des régions parallèles. Si elle est mise à TRUE le nombre de threads utilisées peut être fixé depuis l'environnement d'exécution pour mieux utiliser les ressources du système. Si elle est mise à FALSE l'ajustement dynamique est inhibé. La condition par défaut dépend du calculateur. De plus la valeur peut être modifiée par un appel à la subroutine OMP_SET_DYNAMIC.setenv OMP_NUM_THREADS 16
Exemple :
La variable d'environnement OMP_NESTED active ou désactive le parallélisme imbriqué. Si elle est mise à TRUE, le parallélisme imbriqué est activé; autrement si elle est mise à FALSE, il est désactivé. La valeur par défaut est FALSE.setenv OMP_DYNAMIC TRUE
Exemple :
setenv OMP_NESTED TRUE
Programmes exemples
Exemple simple de programme OpenMP
Exemples de parallélisations d'EDP
Tuning
On peut optimiser les performances de son application de différentes manières.
Lorsque l'on écrit le code, il faut garder à l'esprit que les synchronisations des threads, les barrières, les latences liées aux accès concurrents aux mêmes données ralentissent l'exécution et font partie de ce que l'on appelle l'overhead, i.e. le surcoût lié à la parallélisation et cela fait baisser les performances. Il est donc souhaitable de les limiter au strict minimum.
Il faut aussi répartir équitablement le travail entre ces threads (load-balancing).
Voir les exemples pratiques de parallélisation.
Ensuite il faut adapter le nombre de threads à la masse de calculs. Il ne sert à rien de lancer un code sur 40 threads, si dès 10 ou 12, les performances stagnent. Pour déterminer le "bon" nombre de threads, on fait quelques runs en augmentant le nombre de threads (par exemple des puissances de 2 : 2, 4, 8, ...) et en traçant la courbe x=nombre de threads, y = accélération ou efficacité du code.
Cela donne déjà une indication : lorsque le tracé obtenu fléchit, i.e. ce n'est plus une droite, on a atteint le nombre optimal de threads.
Enfin, on peut faire appel à des variables d'environnement pour essayer de limiter la concurrence entre les processus pour accéder aux ressources, comme par exemple la bande passante entre les processeurs et la mémoire.
Contrairement à MPI, les threads OpenMP partagent souvent leurs données et les accès concurrents peuvent être donc fréquents.
Par exemple, sur l'architecture IBM Power5 les variables d'environnement que l'on peut utiliser sont les suivantes :
setenv AIXTHREAD_SCOPE SAIXTHREAD_SCOPE indique que l'on souhaite mettre une seule thread par processeur physique.
setenv SPINLOOPTIME 1000000
setenv YIELDLOOPTIME 1000000
SPINLOOPTIME indique au système que la thread doit rester active et verrouiller le processeur.
YIELDLOOPTIME complète l'action de SPINLOOPTIME.
Remarque :
Ces variables d'nevironnement sont déjà mises en place dans l'environnement LoadLeveler : il n'est pas nécessaire de les remettre dans vos scripts de soumission.
Dans le cas particulier des architectures des clusters Power5 et iDataPlex du CRIHAN, le nombre de threads OpenMP à utiliser dot être inférieur ou égal à 8, car les noeuds de calcul ne contiennent que 8 coeurs (unités de calcul).
En mode interactif sur les frontales de connexion, il vaut mieux se limiter à 4 threads.
Avantages et inconvénients d'OpenMP
Avantages :
- Pas de gestion explicite des communications entre processus
- Respect de la sémantique du code séquentiel
- Ecriture incrémentale du programme parallèle
- Langage puissant
- Portabilité limitée aux machines parallèles SMP