Personal tools
You are here: Home Calcul Technique Documentation IBM cluster p575 (Power5) OpenMP
Document Actions

OpenMP

Description des directives OpenMP pour la parallélisation à mémoire partagée

OpenMP

Directives OpenMP

Compilation et exécution

Constructions parallèles

  • Directive DO
  • Directive SECTIONS
  • Directive SINGLE
  • Directive PARALLEL DO
  • Synchronisations

  • Directive MASTER
  • Directive CRITICAL
  • Directive BARRIER
  • Directive ATOMIC
  • Directive FLUSH
  • Directive ORDERED
  • Clauses des directives

  • Clause PRIVATE
  • Clause SHARED
  • Clause DEFAULT
  • Clause FIRSTPRIVATE
  • Clause LASTPRIVATE
  • Clause REDUCTION
  • Clause COPYIN
  • Bibliothèque de routines

  • Subroutine OMP_SET_NUM_THREADS
  • Function OMP_GET_NUM_THREADS
  • Function OMP_GET_MAX_THREADS
  • FunctionOMP_GET_NUM_PROCS
  • Function OMP_IN_PARALLEL
  • Subroutine OMP_SET_DYNAMIC
  • Function OMP_GET_DYNAMIC
  • Subroutine OMP_SET_NESTED
  • Function OMP_GET_NESTED
  • Variables d'environnement

  • OMP_SCHEDULE
  • OMP_NUM_THREADS
  • OMP_DYNAMIC
  • OMP_NESTED
  • 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 :
    préfixe   directive   [clause[[,] clause] ... ]
    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 :
    !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 :

    !23456789
    !$OMP PARALLEL DO &
    !$OMP SHARED (U, V, W)
    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 :
          INTEGER  ::  nbproc
    !$    INTEGER  ::  OMP_NUM_THREADS
    !$    EXTERNAL     OMP_NUM_THREADS
          nbproc = 1
    !$    nbproc = OMP_NUM_THREADS()
    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.


    flechehaut

    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.


    flechehaut

    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 :
    !$OMP PARALLEL [clause[[,] clause]...]

    block

    !$OMP END PARALLEL

    La clause est l'une des suivantes :
      • PRIVATE (liste)
      • SHARED (liste)
      • DEFAULT (PRIVATE | SHARED | NONE)
      • FIRSTPRIVATE (liste)
      • REDUCTION ( {operateur|intrinsèque}:liste)
      • IF (expression_scalaire_logique)
      • COPYIN (liste)
    Elles sont décrites dans la rubrique  Clauses des directives .

    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 :

    • Directive DO
    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 :
    !$OMP DO [clause[[,] clause]...]

    do_loop

    [!$OMP END DO [NOWAIT]]

    Les clauses sont parmi les suivantes :
      • PRIVATE (liste)
      • FIRSTPRIVATE (liste)
      • LASTPRIVATE (liste)
      • REDUCTION ( {operateur|intrinsèque}:liste)
      • SCHEDULE (type[,chunk])
      • ORDERED
    Les clauses FIRSTPRIVATE, LASTPRIVATE et REDUCTION sont décrites dans la rubrique  Clause des directives .
    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.
    En l'absence de clause SCHEDULE la distribution par défaut dépend de l'architecture.

    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 :

    !$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
    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.
    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.
    • Directive SECTIONS
    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 :
    !$OMP SECTIONS [clause[[,] clause]...]

    [!$OMP SECTION]

    block

    [!$OMP SECTION

    block]

    ....

    !$OMP END SECTIONS [NOWAIT]

    La clause est l'une des suivantes :
      • PRIVATE (liste)
      • FIRSTPRIVATE (liste)
      • LASTPRIVATE (liste)
      • REDUCTION ( {operateur|intrinsèque}:liste)
    Ces clauses sont décrites dans la rubrique  Clause des directives .

    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 :

    !$OMP PARALLEL SECTIONS
    !$OMP SECTION
          CALL XAXIS
    !$OMP SECTION
          CALL YAXIS
    !$OMP SECTION
          CALL ZAXIS
    !$OMP END PARALLEL SECTIONS
    • Directive SINGLE
    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 SINGLE [clause[[,] clause]...]

    block

    !$OMP END SINGLE [NOWAIT]

    La clause est l'une des suivantes :
      • PRIVATE (liste)
      • FIRSTPRIVATE (liste)
    Elles sont décrites dans la rubrique  Clause des directives .

    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 :

    • Directive PARALLEL DO
    Cette directive fournit une version courte d'une boucle DO à exécuter en parallèle. Son format est :
    !$OMP PARALLEL DO [clause[[,] clause]...]

    do_loop

    [!$OMP END PARALLEL DO]

    La clause doit être acceptée par les directives PARALLEL et DO avec leurs contraintes respectives.
    Exemple :
    !$OMP PARALLEL DO
          DO I = 2, N
             B (I) = 0.5_rp * ( A(I) + A (I-1) )
          END DO
    !$OMP END PARALLEL DO


    flechehaut

    Synchronisations

    Il existe plusieurs mécanismes de synchronisation des threads d'une équipe.
    • Directive MASTER
    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 :
    !$OMP MASTER

    block

    !$OMP END MASTER

    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é.
    • Directive CRITICAL
    Les directives CRITICAL et END CRITICAL restreignent l'accès d'un bloc du code à une seule thread à la fois. Leur syntaxe est :
    !$OMP CRITICAL [(nom)]

    block

    !$OMP END CRITICAL [(nom)]

    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é.
    • Directive BARRIER
    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 BARRIER
    • Directive ATOMIC
    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 ATOMIC
    Elle s'applique seulement pour l'une des situations suivantes :
      • x = x  opérateur  expression
      • x = expression  opérateur  x
      • x = fonction intrinsèque (x, expression)
      • x = fonction intrinsèque (expression, x)
    Avec les définitions suivantes :
    • 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.
    Seules la lecture et l'écriture de x se font de manière séquentielle, l'évaluation de expression n'est pas concernée. Ainsi les mises à jour des valeurs doivent être protégées par la directive ATOMIC. La fonction intrinsèque, l'opérateur opérateur et l'assignement doivent être de véritables fonctions intrinsèques, opérateurs intrinsèques et assignements intrinsèques.
    Toutes les références à l'adresse de stockage x doivent être de même type
    Exemple :
    !$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
    Ici la mise à jour de Y n'est pas concernée par la directive.
    • Directive FLUSH
    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 :
      • 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.
    Les accès mémoire ultérieurs à de telles variables fournissent les dernières valeurs de ces données. La syntaxe est :
    !$OMP FLUSH [(liste)]
    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 :
      • BARRIER,
      • CRITICAL et END CRITICAL,
      • END DO,
      • END PARALLEL,
      • END SECTIONS,
      • END SINGLE,
      • ORDERED et END ORDERED;
    mais ne l'est plus en cas de présence d'un NOWAIT.
    Exemple :
          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
    • Directive ORDERED
    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 :
    !$OMP ORDERED

    block

    !$OMP END ORDERED

    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.
    La région de code incluse entre les directives ORDERED et END ORDERED doit être un bloc structuré.


    flechehaut

    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.
    • Clause PRIVATE
    La clause PRIVATE rend ses arguments privés à chaque thread.  Le format est :
    PRIVATE (liste)
    Le comportement d'une variable déclarée dans une clause PRIVATE est le suivant :
      • 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 ;
    • Clause SHARED
    La clause SHARED rend ses arguments visibles par toutes les threads de l'équipe. Celles-ci accèdent toutes à la même donnée. Le format est :
    SHARED (liste)
    • Clause DEFAULT
    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 :
    DEFAULT (PRIVATE | SHARED | NONE)
    Les clauses PRIVATE, SHARED ou NONE ont les effects suivants :
      • 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.
    Une seule clause DEFAULT peut apparaitre dans une région parallèle. De plus, certaines variables peuvent avoir leur déclaration spécifique qui écrase la clause par défaut.

    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 :

    !$OMP PARALLEL DO DEFAULT(PRIVATE) &
    !$OMP FIRSTPRIVATE (I) &
    !$OMP SHARED (X) &
    !$OMP SHARED (R) &
    !$OMP LASTPRIVATE (I)
    • Clause FIRSTPRIVATE
    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 :
    FIRSTPRIVATE (liste)
    • Clause LASTPRIVATE
    La clause LASTPRIVATE englobe la clause PRIVATE. Son action dépend de la construction parallèle sur laquelle elle agit :
      • 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.
    Les champs des objets qui ne sont pas initialisés par la dernière itération de la directive DO ou par la dernière SECTION (au sens séquentiel) de la directive SECTIONS, n'ont pas de valeurs attribuées à la fin de la construction parallèle. Sa syntaxe est :
    LASTPRIVATE (liste)
    • Clause REDUCTION
    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 :
    REDUCTION ({opérateur|intrinsèque}: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) :
    Opérateur / fonction intrinsèque Initialisation
    +
    0
    *
    1
    -
    0
    .AND.
    .TRUE.
    .OR.
    .FALSE.
    .EQV.
    .TRUE.
    .NEQV.
    .FALSE.
    MAX
    plus petit nombre représentable
    MIN
    plus grand nombre représentable
    IAND
    tous les bits à 1
    IOR
    0
    IEOR
    0

    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)
    • Clause COPYIN
    La clause COPYIN s'applique uniquement aux blocs commons qui sont déclarés THREADPRIVATE. Une telle clause dans une région parallèle spécifie que les données de la master thread seront dupliquées dans les copies privées du bloc common pour les threads de l'équipe à l'entrée de la région parallèle. Sa syntaxe est la suivante :
    COPYIN (liste)
    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.
    Exemple :
          COMMON /BLK1/ SCRATCH
          COMMON /FIELDS/ XFIELD, YFIELD, ZFIELD

    !$OMP THREADPRIVATE (/BLK1/,/FIELDS/)
    !$OMP DEFAULT (PRIVATE), COPYIN (/BLK1/, ZFIELD)

    Les deux blocs commons BLK1 et FIELDS sont déclarés THREADPRIVATE mais une seule variable dans le bloc common FIELDS sera concernée.

    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 :

    !$OMP THREADPRIVATE (/cb/[,/cb/] ...)
    cb est le nom d'un bloc common à rendre privé pour une thread.
    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.


    flechehaut

    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.
    • Subroutine OMP_SET_NUM_THREADS
    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 :
    SUBROUTINE OMP_SET_NUM_THREADS (expression_scalaire_entière)
    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.
    Un appel à OMP_SET_NUM_THREADS a priorité sur la variable d'environnement OMP_NUM_THREADS.
    • Function OMP_GET_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 :
    INTEGER FUNCTION OMP_GET_NUM_THREADS ()
    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 :

           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 :

          NBTHRDS = 1
    !$OMP PARALLEL DEFAULT (SHARED)
    !$OMP SINGLE
    !$    CALL OMP_GET_NUM_THREADS (NBTHDS)
    !$OMP END SINGLE
          ....
          ....
    !$OMP END PARALLEL
    • Function OMP_GET_MAX_THREADS
    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 :
    INTEGER FUNCTION OMP_GET_MAX_THREADS ()
    • Function OMP_GET_NUM_PROCS
    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_NUM_PROCS ()
    • Function OMP_IN_PARALLEL
    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 :
    LOGICAL FUNCTION OMP_IN_PARALLEL ()
    Cette function a une portée globale.
    • Subroutine OMP_SET_DYNAMIC
    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 :
    SUBROUTINE OMP_SET_DYNAMIC (expression_scalaire_logique)
    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é.

    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.

    • Function OMP_GET_DYNAMIC
    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 :
    LOGICAL FUNCTION OMP_GET_DYNAMIC ()
    Si la mise en oeuvre ne supporte pas l'ajustement dynamique alors la function renvoie toujours .FALSE. .
    • Subroutine OMP_SET_NESTED
    La subroutine OMP_SET_NESTED active ou désactive l'imbrication de régions parallèles. Le format de cette subroutine est le suivant :
    SUBROUTINE OMP_SET_NESTED (expression_scalaire_logique)
    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.
    • Function OMP_GET_NESTED
    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 :
    LOGICAL FUNCTION OMP_GET_NESTED ()
    Si la mise en oeuvre ne supporte pas l'ajustement dynamique alors la function renvoie toujours .FALSE. .


    flechehaut

    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.
    • Variable OMP_SCHEDULE
    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 :
    setenv OMP_SCHEDULE "GUIDED,4"
    setenv OMP_SCHEDULE "dynamic"
    • Variable OMP_NUM_THREADS
    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.
    Exemple :
    setenv OMP_NUM_THREADS 16
    • Variable OMP_DYNAMIC
    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.
    Exemple :
    setenv OMP_DYNAMIC TRUE
    • Variable OMP_NESTED
    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.
    Exemple :
    setenv OMP_NESTED TRUE


    flechehaut

    Programmes exemples

    Exemple simple de programme OpenMP

    Exemples de parallélisations d'EDP


    flechehaut

    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 S
    setenv SPINLOOPTIME 1000000
    setenv YIELDLOOPTIME 1000000
    AIXTHREAD_SCOPE indique que l'on souhaite mettre une seule thread par processeur physique.
    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.


    flechehaut

    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
    Inconvénients :
    • Portabilité limitée aux machines parallèles SMP

    flechehaut

    Powered by Plone CMS, the Open Source Content Management System

    This site conforms to the following standards: