Partager via


2. Directives

Les directives sont basées sur des directives #pragma définies dans les normes C et C++. Les compilateurs qui prennent en charge l’API C et C++ OpenMP incluent une option de ligne de commande qui active et autorise l’interprétation de toutes les directives du compilateur OpenMP.

2.1 Format des directives

La syntaxe d’une directive OpenMP est formellement spécifiée par la grammaire dans l’annexe C, et de manière informelle de la manière suivante :

#pragma omp directive-name  [clause[ [,] clause]...] new-line

Chaque directive commence par #pragma omp, afin de réduire le risque de conflit avec d’autres directives pragma (non OpenMP ou d’extensions de fournisseur à OpenMP) portant le même nom. Le reste de la directive suit les conventions des normes C et C++ pour les directives du compilateur. En particulier, l’espace blanc peut être utilisé avant et après #, et parfois l’espace blanc doit être utilisé pour séparer les mots d’une directive. Les jetons de prétraitement qui suivent #pragma omp sont soumis au remplacement des macros.

Les directives sont sensibles à la casse. L’ordre dans lequel les clauses apparaissent dans les directives n’est pas important. Les clauses sur les directives peuvent être répétées si nécessaire, sous réserve des restrictions répertoriées dans la description de chaque clause. Si la liste de variables variable-list apparaît dans une clause, elle doit spécifier uniquement des variables. Un seul nom de directive directive-name peut être spécifié par directive. Par exemple, la directive suivante n’est pas autorisée :

/* ERROR - multiple directive names not allowed */
#pragma omp parallel barrier

Une directive OpenMP s’applique à maximum une instruction réussie, qui doit être un bloc structuré.

2.2 Compilation conditionnelle

Le nom de macro _OPENMP est défini par les implémentations compatibles avec OpenMP comme constante décimale aaaamm, qui sera l’année et le mois de la spécification approuvée. Cette macro ne doit pas être l’objet d’un #define ou d’une directive de prétraitement #undef.

#ifdef _OPENMP
iam = omp_get_thread_num() + index;
#endif

Si les fournisseurs définissent des extensions pour OpenMP, ils peuvent spécifier des macros prédéfinies supplémentaires.

2.3 Construction parallel

La directive suivante définit une région parallèle, qui représente une région du programme à exécuter par de nombreux threads en parallèle. Cette directive est la construction fondamentale qui démarre l’exécution parallèle.

#pragma omp parallel [clause[ [, ]clause] ...] new-line   structured-block

La clause est l’une des clauses suivantes :

  • if( scalar-expression )
  • private( variable-list )
  • firstprivate( variable-list )
  • default(shared | none)
  • shared( variable-list )
  • copyin( variable-list )
  • reduction( operator : variable-list )
  • num_threads( integer-expression )

Lorsqu’un thread accède à une construction parallel, une équipe de threads est créée dans les cas suivants :

  • Aucune clause if n’est présente.
  • L’expression if prend une valeur différente de zéro.

Ce thread devient le thread principal de l’équipe, avec un nombre de threads de 0 et tous les threads de l’équipe, y compris le thread principal, exécutent la région en parallèle. Si la valeur de l’expression if est égale à zéro, la région est sérialisée.

Pour déterminer le nombre de threads demandés, les règles suivantes sont prises en compte dans l’ordre. La première règle dont la condition est remplie sera appliquée :

  1. Si la clause num_threads est présente, la valeur de l’expression entière est le nombre de threads demandés.

  2. Si la fonction de bibliothèque omp_set_num_threads a été appelée, la valeur de l’argument dans l’appel exécuté le plus récemment est le nombre de threads demandés.

  3. Si la variable d’environnement OMP_NUM_THREADS est définie, la valeur de cette variable d’environnement est le nombre de threads demandés.

  4. Si aucune des méthodes ci-dessus n’est utilisée, le nombre de threads demandés est défini par l’implémentation.

Si la clause num_threads est présente, elle remplace le nombre de threads demandés par la fonction de bibliothèque omp_set_num_threads ou la variable d’environnement OMP_NUM_THREADS uniquement pour la région parallèle à laquelle elle est appliquée. Les régions parallèles ultérieures ne sont pas affectées par celle-ci.

Le nombre de threads qui exécutent la région parallèle dépend également de l’ajustement dynamique du nombre de threads activé. Si l’ajustement dynamique est désactivé, le nombre demandé de threads exécute la région parallèle. Si l’ajustement dynamique est activé, le nombre demandé de threads est le nombre maximal de threads qui peuvent exécuter la région parallèle.

Si une région parallèle est rencontrée alors que l’ajustement dynamique du nombre de threads est désactivé et que le nombre de threads demandés pour la région parallèle est supérieur au nombre que le système d’exécution peut fournir, le comportement du programme est défini par l’implémentation. Une implémentation peut, par exemple, interrompre l’exécution du programme ou sérialiser la région parallèle.

La fonction de bibliothèque omp_set_dynamic et la variable d’environnement OMP_DYNAMIC peuvent être utilisées pour activer et désactiver l’ajustement dynamique du nombre de threads.

Le nombre de processeurs physiques hébergeant réellement les threads à un moment donné est défini par l’implémentation. Une fois créé, le nombre de threads de l’équipe reste constant pendant la durée de cette région parallèle. Il peut être modifié explicitement par l’utilisateur ou automatiquement par le système d’exécution d’une région parallèle à une autre.

Les instructions contenues dans l’étendue dynamique de la région parallèle sont exécutées par chaque thread, et chaque thread peut exécuter un chemin d’instructions différent des autres threads. Les directives rencontrées en dehors de l’étendue lexicale d’une région parallèle sont appelées des directives orphelines.

Une barrière implicite est présente à la fin d’une région parallèle. Seul le thread principal de l’équipe continue l’exécution à la fin d’une région parallèle.

Si un thread d’une équipe exécutant une région parallèle rencontre une autre construction parallel, il crée une équipe et devient le principal de cette nouvelle équipe. Les régions parallèles imbriquées sont sérialisées par défaut. Par conséquent, par défaut, une région parallèle imbriquée est exécutée par une équipe composée d’un thread. Le comportement par défaut peut être modifié à l’aide de la fonction de bibliothèque runtime omp_set_nested ou de la variable d’environnement OMP_NESTED. Toutefois, le nombre de threads d’une équipe qui exécutent une région parallèle imbriquée est défini par l’implémentation.

Les restrictions à la directive parallel sont les suivantes :

  • Tout au plus, une clause if peut apparaître sur la directive.

  • Il n’est pas spécifié si des effets secondaires dans l’expression If ou num_threads se produisent.

  • Un throw exécuté à l’intérieur d’une région parallèle doit entraîner la reprise de l’exécution dans l’étendue dynamique du même bloc structuré, et il doit être intercepté par le même thread qui a levé l’exception.

  • Seulement une clause num_threads peut apparaître sur la directive. L’expression num_threads est évaluée en dehors du contexte de la région parallèle et doit être évaluée à une valeur entière positive.

  • L’ordre d’évaluation des clauses if et num_threads n’est pas spécifié.

Références croisées

2.4 Constructions de partage de travail

Une construction de partage de travail distribue l’exécution de l’instruction associée parmi les membres de l’équipe qui la rencontrent. Les directives de partage de travail ne lancent pas de nouveaux threads et n’impliquent aucun obstacle à l’entrée d’une construction de partage de travail.

La séquence de constructions de partage de travail et des directives barrier rencontrées doit être la même pour chaque thread d’une équipe.

OpenMP définit les constructions de partage de travail suivantes, et ces constructions sont décrites dans les sections suivantes :

2.4.1 Construction For

La directive for identifie une construction de partage de travail itérative qui spécifie que les itérations de la boucle associée seront exécutées en parallèle. Les itérations de la boucle for sont réparties entre les threads qui existent déjà dans l’équipe exécutant la construction parallel à laquelle elle se lie. La syntaxe de la construction for est la suivante :

#pragma omp for [clause[[,] clause] ... ] new-line for-loop

La clause est l’une des clauses suivantes :

  • private( variable-list )
  • firstprivate( variable-list )
  • lastprivate( variable-list )
  • reduction( operator : variable-list )
  • ordered
  • schedule( kind [, chunk_size] )
  • nowait

La directive for place des restrictions sur la structure de la boucle for correspondante. Plus précisément, la boucle for correspondante doit avoir une forme canonique :

for ( init-expr ; var logical-op b ; incr-expr )

init-expr
Celui-ci peut avoir l'une des valeurs suivantes :

  • var = lb
  • integer-type var = lb

incr-expr
Celui-ci peut avoir l'une des valeurs suivantes :

  • ++ var
  • var ++
  • -- var
  • var --
  • var += incr
  • var -= incr
  • var = var + incr
  • var = incr + var
  • var = var - incr

var
Variable entière signée. Au lieu d’être partagée, cette variable est implicitement rendue privée pendant la durée du for. Ne modifiez pas cette variable dans le corps de l’instruction for. Sauf si lastprivate est spécifié pour la variable, sa valeur après la boucle est indéterminée.

logical-op
Celui-ci peut avoir l'une des valeurs suivantes :

  • <
  • <=
  • >
  • >=

lb, bet incr
Expressions entières invariantes de boucle. Il n’y a aucune synchronisation pendant l’évaluation de ces expressions, donc tous les effets secondaires évalués produisent des résultats indéterminés.

Le formulaire canonique permet de calculer le nombre d’itérations de boucles lors de l’entrée dans la boucle. Ce calcul est effectué avec des valeurs dans le type de var, après les promotions intégrales. En particulier, si la valeur de b - lb + incr ne peut être représentée dans ce type, le résultat est indéterminé. En outre, si logical-op est < ou <=, incr-expr doit entraîner l’augmentation de var sur chaque itération de la boucle. Si logical-op est > ou >=, incr-expr doit entraîner la diminution de var sur chaque itération de la boucle.

La clause schedule spécifie comment les itérations de la boucle for sont divisées entre les threads de l’équipe. La justesse d’un programme ne doit pas dépendre du thread qui exécute une itération particulière. La valeur de chunk_size, si elle est spécifiée, doit être une expression entière invariante de boucle avec une valeur positive. Il n’y a aucune synchronisation pendant l’évaluation de cette expression, donc tout effet secondaire évalué produit des résultats indéterminés. La planification kind peut être l’une des valeurs suivantes :

Tableau 2-1 : valeurs kind de la clause schedule

Valeur Description
static Lorsque schedule(static, chunk_size ) est spécifié, les itérations sont divisées en segments d’une taille spécifiée par chunk_size. Les segments sont attribués statiquement aux threads au sein de l’équipe à la manière d’un tourniquet dans l’ordre du numéro du thread. Lorsqu’aucun chunk_size n’est spécifié, l’espace d’itération est divisé en segments de taille approximativement égale, avec un segment affecté à chaque thread.
dynamic Lorsque schedule(dynamic, chunk_size ) est spécifié, les itérations sont divisées en une série de segments, chacune contenant des itérations chunk_size. Chaque segment est affecté à un thread qui attend une affectation. Le thread exécute le segment d’itérations, puis attend son affectation suivante, jusqu’à ce qu’aucun segment ne reste affecté. Le dernier segment à affecter peut avoir un plus petit nombre d’itérations. Lorsqu’aucun chunk_size n’est spécifié, la valeur par défaut est 1.
guidé Lorsque schedule(guided, chunk_size ) est spécifié, les itérations sont affectées aux threads en segments avec des tailles décroissantes. Lorsqu’un thread termine son segment d’itérations attribué, un autre segment lui est affecté dynamiquement, jusqu’à ce qu’il n’en reste plus. Pour un chunk_size de 1, la taille de chaque segment est approximativement le nombre d’itérations non attribuées divisé par le nombre de threads. Ces tailles diminuent de manière presque exponentielle jusqu’à 1. Pour un chunk_size dont la valeur k est supérieure à 1, les tailles diminuent de manière presque exponentielle jusqu’à k, sauf que le dernier bloc peut avoir moins de k itérations. Lorsqu’aucun chunk_size n’est spécifié, la valeur par défaut est 1.
runtime Lorsque schedule(runtime) est spécifié, la décision concernant la planification est différée jusqu’à l’exécution. La planification kind et la taille des segments peuvent être choisies au moment de l’exécution en définissant la variable d’environnement OMP_SCHEDULE. Si cette variable d’environnement n’est pas définie, la planification résultante est définie par l’implémentation. Lorsque schedule(runtime) est spécifié, chunk_size ne doit pas être spécifié.

En l’absence d’une clause schedule définie explicitement, le schedule par défaut est défini par l’implémentation.

Un programme conforme à OpenMP ne doit pas s’appuyer sur une planification particulière pour une exécution correcte. Un programme ne doit pas s’appuyer sur une planification kind précisément conforme à la description indiquée ci-dessus, car des variations sont possibles dans les implémentations de la même planification kind entre différents compilateurs. Les descriptions peuvent être utilisées pour sélectionner la planification appropriée pour une situation particulière.

La clause ordered doit être présente lorsque les directives ordered sont liées à la construction for.

Une barrière implicite est présente à la fin d’une construction for, sauf si une clause nowait est spécifiée.

Voici les restrictions à la directive for :

  • La boucle for doit être un bloc structuré et, en outre, son exécution ne doit pas être arrêtée par une instruction break.

  • Les valeurs des expressions de contrôle de boucle de la boucle for associée à une directive for doivent être identiques pour tous les threads de l’équipe.

  • La variable d’itération de boucle for doit avoir un type entier signé.

  • Seulement une clause schedule peut apparaître sur une directive for.

  • Seulement une clause ordered peut apparaître sur une directive for.

  • Seulement une clause nowait peut apparaître sur une directive for.

  • Il n’est pas spécifié si des effets secondaires dans les expressions chunk_size, lb, b ou incr se produisent, ou combien de fois ils se produisent.

  • La valeur de l’expression chunk_size doit être la même pour tous les threads de l’équipe.

Références croisées

2.4.2 Construction sections

La directive sections identifie une construction de partage de travail non itérative qui spécifie un ensemble de constructions à diviser entre les threads d’une équipe. Chaque section est exécutée une fois par un thread dans l’équipe. La syntaxe de la directive sections est la suivante :

#pragma omp sections [clause[[,] clause] ...] new-line
   {
   [#pragma omp section new-line]
      structured-block
   [#pragma omp section new-linestructured-block ]
...
}

La clause est l’une des clauses suivantes :

  • private( variable-list )
  • firstprivate( variable-list )
  • lastprivate( variable-list )
  • reduction( operator : variable-list )
  • nowait

Chaque section est précédée d’une directive section, bien que la directive section soit facultative pour la première section. Les directives section doivent apparaître dans l’étendue lexicale de la directive sections. Une barrière implicite est présente à la fin d’une construction sections, sauf si un nowait est spécifié.

Voici les restrictions à la directive sections :

  • Une directive section ne doit pas apparaître en dehors de l’étendue lexicale de la directive sections.

  • Seulement une clause nowait peut apparaître sur une directive sections.

Références croisées

  • Clauses private, firstprivate, lastprivate et reduction (section 2.7.2)

2.4.3 Construction single

La directive single identifie une construction qui spécifie que le bloc structuré associé est exécuté par un seul thread de l’équipe (pas nécessairement le thread principal). La syntaxe de la directive single est la suivante :

#pragma omp single [clause[[,] clause] ...] new-linestructured-block

La clause est l’une des clauses suivantes :

  • private( variable-list )
  • firstprivate( variable-list )
  • copyprivate( variable-list )
  • nowait

Une barrière implicite est présente après la construction single, sauf si une clause nowait est spécifiée.

Voici les restrictions à la directive single :

  • Seulement une clause nowait peut apparaître sur une directive single.
  • La clause copyprivate ne doit pas être utilisée avec la clause nowait.

Références croisées

2.5 Constructions de partage de travail et parallel combinées

Les constructions de partage de travail et parallel combinées sont des raccourcis pour spécifier une région parallèle qui n’a qu’une seule construction de partage de travail. La sémantique de ces directives est la même que la spécification explicite d’une directive parallel suivie d’une construction de partage de travail unique.

Les sections suivantes décrivent les constructions de partage de travail et parallel combinées :

2.5.1 Construction parallel for

La directive parallel for est un raccourci pour une région parallel qui ne contient qu’une seule directive for. La syntaxe de la directive parallel for est la suivante :

#pragma omp parallel for [clause[[,] clause] ...] new-linefor-loop

Cette directive autorise toutes les clauses de la directive parallel et de la directive for, à l’exception de la clause nowait, avec des significations et des restrictions identiques. La sémantique est la même que la spécification explicite d’une directive parallel immédiatement suivie d’une directive for.

Références croisées

2.5.2 Construction parallel sections

La directive parallel sections fournit un formulaire de raccourci permettant de spécifier une région parallel qui n’a qu’une seule directive sections. La sémantique est la même que la spécification explicite d’une directive parallel immédiatement suivie d’une directive sections. La syntaxe de la directive parallel sections est la suivante :

#pragma omp parallel sections  [clause[[,] clause] ...] new-line
   {
   [#pragma omp section new-line]
      structured-block
   [#pragma omp section new-linestructured-block  ]
   ...
}

La clause peut être l’une des clauses acceptées par les directives parallel et sections, à l’exception de la clause nowait.

Références croisées

2.6 Directives principales et de synchronisation

Ces opérations sont décrites dans les sections suivantes :

2.6.1 Construction master

La directive master identifie une construction qui spécifie un bloc structuré exécuté par le thread principal de l’équipe. La syntaxe de la directive master est la suivante :

#pragma omp master new-linestructured-block

Les autres threads de l’équipe n’exécutent pas le bloc structuré associé. Il n’existe aucune barrière implicite à l’entrée ou à la sortie de la construction master.

2.6.2 Construction critical

La directive critical identifie une construction qui limite l’exécution du bloc structuré associé à un seul thread à la fois. La syntaxe de la directive critical est la suivante :

#pragma omp critical [(name)]  new-linestructured-block

Un nom facultatif peut être utilisé pour identifier la région critique. Les identificateurs utilisés pour identifier une région critique ont une liaison externe et se trouvent dans un espace de noms distinct des espaces de noms utilisés par les étiquettes, les balises, les membres et les identificateurs ordinaires.

Un thread attend au début d’une région critique jusqu’à ce qu’aucun autre thread n’exécute une région critique (n’importe où dans le programme) avec le même nom. Toutes les directives critical sans nom correspondent au même nom non spécifié.

2.6.3 Directive barrier

La directive barrier synchronise tous les threads d’une équipe. En cas de rencontre, chaque thread de l’équipe attend jusqu’à ce que tous les autres aient atteint ce point. La syntaxe de la directive barrier est la suivante :

#pragma omp barrier new-line

Une fois que tous les threads de l’équipe ont rencontré la barrière, chaque thread de l’équipe commence à exécuter les instructions après la directive de barrière en parallèle. Étant donné que la directive barrier n’a pas d’instruction de langage C comprise dans sa syntaxe, certaines restrictions s’appliquent quant à sa disposition au sein d’un programme. Pour plus d’informations sur la grammaire formelle, consultez l’annexe C. L’exemple ci-dessous illustre ces restrictions.

/* ERROR - The barrier directive cannot be the immediate
*          substatement of an if statement
*/
if (x!=0)
   #pragma omp barrier
...

/* OK - The barrier directive is enclosed in a
*      compound statement.
*/
if (x!=0) {
   #pragma omp barrier
}

2.6.4 Construction atomic

La directive atomic garantit qu’un emplacement de mémoire spécifique est mis à jour atomiquement, plutôt que de l’exposer à la possibilité de plusieurs threads d’écriture simultanés. La syntaxe de la directive atomic est la suivante :

#pragma omp atomic new-lineexpression-stmt

L’instruction d’expression doit avoir l’une des formes suivantes :

  • x binop = expr
  • x ++
  • ++ x
  • x --
  • -- x

Dans les expressions précédentes :

  • x est une expression lvalue avec un type scalaire.

  • expr est une expression avec un type scalaire et ne fait pas référence à l’objet désigné par x.

  • binop n’est pas un opérateur surchargé et est l’un des +, *, -, /, &, ^, |, << ou >>.

Bien que ce soit défini par implémentation si une implémentation remplace toutes les directives atomic par des directives critical qui ont le même nom unique, la directive atomic permet une meilleure optimisation. Souvent, les instructions matérielles sont disponibles pour effectuer la mise à jour atomique avec le moins de surcharge.

Seul le chargement et le magasin de l’objet désigné par x sont atomiques. L’évaluation de expr n’est pas atomique. Pour éviter les conditions de concurrence, toutes les mises à jour de l’emplacement en parallèle doivent être protégées par la directive atomic, sauf celles qui sont connues pour être libres de conditions de concurrence.

Voici les restrictions à la directive atomic :

  • Toutes les références atomiques à l’emplacement de stockage x dans tout le programme nécessitent un type compatible.

Exemples

extern float a[], *p = a, b;
/* Protect against races among multiple updates. */
#pragma omp atomic
a[index[i]] += b;
/* Protect against races with updates through a. */
#pragma omp atomic
p[i] -= 1.0f;

extern union {int n; float x;} u;
/* ERROR - References through incompatible types. */
#pragma omp atomic
u.n++;
#pragma omp atomic
u.x -= 1.0f;

2.6.5 Directive flush

La directive flush, qu’elle soit explicite ou implicite, spécifie un point de séquence « inter-thread » auquel l’implémentation est requise pour s’assurer que tous les threads d’une équipe ont une vue cohérente de certains objets (spécifiés ci-dessous) en mémoire. Cela signifie que les évaluations précédentes des expressions qui font référence à ces objets sont complètes et que les évaluations suivantes n’ont pas encore commencé. Par exemple, les compilateurs doivent restaurer les valeurs des objets des registres vers la mémoire, et le matériel peut avoir besoin de vider des mémoires tampons d’écriture vers la mémoire et de recharger les valeurs des objets à partir de la mémoire.

La syntaxe de la directive flush est la suivante :

#pragma omp flush [(variable-list)]  new-line

Si les objets qui nécessitent la synchronisation peuvent tous être désignés par des variables, ces variables peuvent être spécifiées dans la liste variable-list facultative. Si un pointeur est présent dans la liste variable-list, le pointeur lui-même est vidé, et non l’objet auquel le pointeur fait référence.

Une directive flush sans variable-list synchronise tous les objets partagés, sauf les objets inaccessibles avec une durée de stockage automatique. (Cela peut impliquer une plus grosse surcharge qu’un flush avec une liste variable-list.) Une directive flush sans variable-list est implicite pour les directives suivantes :

  • barrier
  • À l’entrée et à la sortie de critical
  • À l’entrée et à la sortie de ordered
  • À l’entrée et à la sortie de parallel
  • À la sortie de for
  • À la sortie de sections
  • À la sortie de single
  • À l’entrée et à la sortie de parallel for
  • À l’entrée et à la sortie de parallel sections

La directive n’est pas implicite si une clause nowait est présente. Il convient de noter que la directive flush n’est pas implicite pour les éléments suivants :

  • À l’entrée de for
  • À l’entrée ou à la sortie de master
  • À l’entrée de sections
  • À l’entrée de single

Une référence qui accède à la valeur d’un objet avec un type qualifié volatile se comporte comme s’il y avait une directive flush spécifiant cet objet au point de séquence précédent. Une référence qui modifie la valeur d’un objet avec un type qualifié volatile se comporte comme s’il y avait une directive flush spécifiant cet objet au point de séquence précédent.

Étant donné que la directive flush n’a pas d’instruction de langage C comprise dans sa syntaxe, certaines restrictions s’appliquent quant à sa disposition au sein d’un programme. Pour plus d’informations sur la grammaire formelle, consultez l’annexe C. L’exemple ci-dessous illustre ces restrictions.

/* ERROR - The flush directive cannot be the immediate
*          substatement of an if statement.
*/
if (x!=0)
   #pragma omp flush (x)
...

/* OK - The flush directive is enclosed in a
*      compound statement
*/
if (x!=0) {
   #pragma omp flush (x)
}

Voici les restrictions à la directive flush :

  • Une variable spécifiée dans une directive flush ne doit pas avoir de type référence.

2.6.6 Construction ordered

Le bloc structuré suivant une directive ordered est exécuté dans l’ordre dans lequel les itérations seraient exécutées dans une boucle séquentielle. La syntaxe de la directive ordered est la suivante :

#pragma omp ordered new-linestructured-block

Une directive ordered doit se trouver dans l’étendue dynamique d’une construction for ou parallel for. La directive for ou parallel for à laquelle les liaisons de construction ordered doivent avoir une clause ordered spécifiée comme décrit dans la section 2.4.1. Dans l’exécution d’une construction for ou parallel for avec une clause ordered, les constructions ordered sont exécutées strictement dans l’ordre dans lequel elles seraient exécutées dans une exécution séquentielle de la boucle.

Voici les restrictions à la directive ordered :

  • Une itération d’une boucle avec une construction for ne doit pas exécuter la même directive ordered plus d’une fois, et elle ne doit pas exécuter plus d’une directive ordered.

2.7 Environnement des données

Cette section présente une directive et plusieurs clauses pour contrôler l’environnement de données pendant l’exécution de régions parallèles, de la manière suivante :

  • Une directive threadprivate est fournie pour créer des variables d’étendue de fichier, d’étendue d’espace de noms ou d’étendue de bloc statiques locales dans un thread.

  • Les clauses qui peuvent être spécifiées sur les directives pour contrôler les attributs de partage de variables pendant la durée des constructions parallel ou de partage de travail sont décrites dans la section 2.7.2.

2.7.1 Directive threadprivate

La directive threadprivate rend les variables d’étendue de fichier, d’étendue d’espace de noms ou d’étendue de bloc statique spécifiées dans la liste variable-list privée à un thread. variable-list est une liste séparée par des virgules de variables qui n’ont pas de type incomplet. La syntaxe de la directive threadprivate est la suivante :

#pragma omp threadprivate(variable-list) new-line

Chaque copie d’une variable threadprivate est initialisée une fois, à un point non spécifié dans le programme avant la première référence à cette copie, et à la manière habituelle (de la même manière que la copie principale serait initialisée dans une exécution en série du programme). Notez que si un objet est référencé dans un initialiseur explicite d’une variable threadprivate et que la valeur de l’objet est modifiée avant la première référence à une copie de la variable, le comportement n’est pas spécifié.

Comme pour toute variable privée, un thread ne doit pas référencer la copie d’un autre thread d’un objet threadprivate. Pendant les régions de séries et les régions principales du programme, les références se feront à la copie du thread principal de l’objet.

Une fois la première région parallèle exécutée, les données des objets threadprivate sont garanties d’être conservées uniquement si le mécanisme de threads dynamiques a été désactivé et si le nombre de threads reste inchangé pour toutes les régions parallèles.

Voici les restrictions à la directive threadprivate :

  • Une directive threadprivate pour les variables d’étendue de fichier ou d’étendue d’espace de noms doit apparaître en dehors de toute définition ou déclaration, et doit précéder de manière lexicale toutes les références à l’une des variables de sa liste.

  • Chaque variable de la liste de variables d’une directive threadprivate au niveau de l’étendue de fichier ou d’espace de noms doit faire référence à une déclaration de variable au niveau de l’étendue de fichier ou d’espace de noms qui précède la directive de manière lexicale.

  • Une directive threadprivate pour les variables d’étendue de bloc statique doit apparaître dans l’étendue de la variable et non dans une étendue imbriquée. La directive doit précéder de manière lexicale toutes les références à des variables de sa liste.

  • Chaque variable de la liste variable-list d’une directive threadprivate dans l’étendue de bloc doit faire référence à une déclaration de variable dans la même étendue qui précède la directive de manière lexicale. La déclaration de variable doit utiliser le spécificateur de classe de stockage statique.

  • Si une variable est spécifiée dans une directive threadprivate dans une unité de traduction, elle doit être spécifiée dans une directive threadprivate dans chaque unité de traduction dans laquelle elle est déclarée.

  • Une variable threadprivate ne doit apparaître dans aucune clause, à l’exception de la clause copyin, copyprivate, schedule, num_threads ou if.

  • L’adresse d’une variable threadprivate n’est pas une constante d’adresse.

  • Une variable threadprivate ne doit pas avoir de type incomplet ou de type référence.

  • Une variable threadprivate avec un type de classe non POD doit avoir un constructeur de copie accessible et non ambigu si elle est déclarée avec un initialiseur explicite.

L’exemple suivant montre pourquoi modifier une variable apparaissant dans un initialiseur peut provoquer un comportement non spécifié, et comment éviter ce problème à l’aide d’un objet auxiliaire et d’un constructeur de copie.

int x = 1;
T a(x);
const T b_aux(x); /* Capture value of x = 1 */
T b(b_aux);
#pragma omp threadprivate(a, b)

void f(int n) {
   x++;
   #pragma omp parallel for
   /* In each thread:
   * Object a is constructed from x (with value 1 or 2?)
   * Object b is copy-constructed from b_aux
   */
   for (int i=0; i<n; i++) {
      g(a, b); /* Value of a is unspecified. */
   }
}

Références croisées

2.7.2 Clauses d’attributs de partage de données

Plusieurs directives acceptent des clauses qui permettent à un utilisateur de contrôler les attributs de partage de variables pendant la durée de la région. Les clauses d’attribut de partage s’appliquent uniquement aux variables dans l’étendue lexicale de la directive sur laquelle la clause apparaît. Toutes les clauses suivantes ne sont pas autorisées sur toutes les directives. La liste des clauses valides sur une directive particulière est donnée dans la description de la directive.

Si une variable est visible lorsqu’une construction parallèle ou de partage de travail est rencontrée et que la variable n’est pas spécifiée dans une clause d’attribut de partage ou de directive threadprivate, la variable est partagée. Les variables statiques déclarées dans l’étendue dynamique d’une région parallèle sont partagées. La mémoire allouée au tas (par exemple, l’utilisation de malloc() en C ou C++, ou de l’opérateur new en C++) est partagée. (Le pointeur vers cette mémoire peut toutefois être privé ou partagé.) Les variables dont la durée de stockage automatique est déclarée dans l’étendue dynamique d’une région parallèle sont privées.

La plupart des clauses acceptent un argument variable-list, qui est une liste séparée par des virgules de variables visibles. Si une variable référencée dans une clause d’attribut de partage de données présente un type dérivé d’un modèle et qu’il n’existe aucune autre référence à cette variable dans le programme, le comportement n’est pas défini.

Toutes les variables qui apparaissent dans les clauses de directive doivent être visibles. Les clauses peuvent être répétées si nécessaire, mais aucune variable ne peut être spécifiée dans plus d’une clause, sauf qu’une variable peut être spécifiée à la fois dans une clause firstprivate et lastprivate.

Les sections suivantes décrivent les clauses d’attribut de partage de données :

2.7.2.1 private

La clause private déclare les variables de la liste variable-list qui doivent être privées pour chaque thread d’une équipe. La syntaxe de la clause private est la suivante :

private(variable-list)

Le comportement d’une variable spécifiée dans une clause private est le suivant. Un nouvel objet avec une durée de stockage automatique est alloué pour la construction. La taille et l’alignement du nouvel objet sont déterminés par le type de la variable. Cette allocation se produit une fois pour chaque thread de l’équipe, et un constructeur par défaut est appelé pour un objet de classe si nécessaire. Sinon, la valeur initiale est indéterminée. L’objet d’origine référencé par la variable a une valeur indéterminée lors de l’entrée de la construction, il ne doit pas être modifié dans l’étendue dynamique de la construction et il a une valeur indéterminée lors de la sortie de la construction.

Dans l’étendue lexicale de la construction de directive, la variable fait référence au nouvel objet privé alloué par le thread.

Voici les restrictions à la clause private :

  • Une variable avec un type de classe spécifié dans une clause private doit avoir un constructeur par défaut accessible et non ambigu.

  • Une variable spécifiée dans une clause private ne doit pas avoir de type qualifié const, sauf s’il a un type de classe avec un membre mutable.

  • Une variable spécifiée dans une clause private ne doit pas avoir de type incomplet ou de type référence.

  • Les variables qui apparaissent dans la clause reduction d’une directive parallel ne peuvent pas être spécifiées dans une clause private d’une directive de partage de travail qui est liée à la construction parallel.

2.7.2.2 firstprivate

La clause firstprivate fournit un surensemble des fonctionnalités fournies par la clause private. La syntaxe de la clause firstprivate est la suivante :

firstprivate(variable-list)

Les variables spécifiées dans la liste variable-list ont une sémantique de clause private, comme décrit dans la section 2.7.2.1. L’initialisation ou la construction se produit comme si elle était effectuée une fois par thread, avant l’exécution du thread de la construction. Pour une clause firstprivate sur une construction parallel, la valeur initiale du nouvel objet privé est la valeur de l’objet d’origine qui existe immédiatement avant la construction parallel pour le thread qui le rencontre. Pour une clause firstprivate sur une construction de partage de travail, la valeur initiale du nouvel objet privé pour chaque thread qui exécute la construction de partage de travail est la valeur de l’objet d’origine qui existe avant le moment où le même thread rencontre la construction de partage de travail. En outre, pour les objets C++, le nouvel objet privé pour chaque thread est construit par copie à partir de l’objet d’origine.

Voici les restrictions à la clause firstprivate :

  • Une variable spécifiée dans une clause firstprivate ne doit pas avoir de type incomplet ou de type référence.

  • Une variable avec un type de classe spécifié firstprivate doit avoir un constructeur de copie accessible et non ambigu.

  • Les variables privées dans une région parallèle ou qui apparaissent dans la clause reduction d’une directive parallel ne peuvent pas être spécifiées dans une clause firstprivate d’une directive de partage de travail qui est liée à la construction parallel.

2.7.2.3 lastprivate

La clause lastprivate fournit un surensemble des fonctionnalités fournies par la clause private. La syntaxe de la clause lastprivate est la suivante :

lastprivate(variable-list)

Les variables spécifiées dans la liste variable-list ont une sémantique de clause private. Lorsqu’une clause lastprivate apparaît sur la directive qui identifie une construction de partage de travail, la valeur de chaque variable lastprivate à partir de la dernière itération séquentielle de la boucle associée, ou la dernière directive de section lexicale, est affectée à l’objet d’origine de la variable. Les variables qui ne sont pas affectées à une valeur par la dernière itération de for ou parallel for, ou par la dernière section lexicale de la directive sections ou parallel sections, ont des valeurs indéterminées après la construction. Les sous-objets non attribués ont également une valeur indéterminée après la construction.

Voici les restrictions à la clause lastprivate :

  • Toutes les restrictions pour private s’appliquent.

  • Une variable avec un type de classe spécifié lastprivate doit avoir un opérateur d’assignation de copie accessible et non ambigu.

  • Les variables privées dans une région parallèle ou qui apparaissent dans la clause reduction d’une directive parallel ne peuvent pas être spécifiées dans une clause lastprivate d’une directive de partage de travail qui est liée à la construction parallel.

2.7.2.4 shared

Cette clause partage les variables qui apparaissent dans la liste variable-list parmi tous les threads d’une équipe. Tous les threads d’une équipe accèdent à la même zone de stockage pour les variables shared.

La syntaxe de la clause shared est la suivante :

shared(variable-list)

2.7.2.5 default

La clause default permet à l’utilisateur d’affecter les attributs de partage de données des variables. La syntaxe de la clause default est la suivante :

default(shared | none)

Spécifier default(shared) équivaut à répertorier explicitement chaque variable actuellement visible dans une clause shared, sauf si elle est qualifiée threadprivate ou const. En l’absence d’une clause de default explicite, le comportement par défaut est le même que si default(shared) était spécifié.

Spécifier default(none) nécessite qu’au moins l’une des valeurs suivantes soit vraie pour chaque référence à une variable dans l’étendue lexicale de la construction parallèle :

  • La variable est explicitement répertoriée dans une clause d’attribut de partage de données d’une construction qui contient la référence.

  • La variable est déclarée dans la construction parallèle.

  • La variable est threadprivate.

  • La variable dispose d’un type qualifié const.

  • La variable est la variable de contrôle de boucle pour une boucle for qui suit immédiatement une directive for ou parallel for, et la référence de variable apparaît à l’intérieur de la boucle.

Spécifier une variable sur une clause firstprivate, lastprivate ou reduction d’une directive entourée provoque une référence implicite à la variable dans le contexte englobant. Ces références implicites sont également soumises aux exigences répertoriées ci-dessus.

Une seule clause default peut être spécifiée sur une directive parallel.

Un attribut de partage de données par défaut d’une variable peut être substitué à l’aide des clauses private, firstprivate, lastprivate, reduction et shared, comme illustré dans l’exemple suivant :

#pragma  omp  parallel  for  default(shared)  firstprivate(i)\
   private(x)  private(r)  lastprivate(i)

2.7.2.6 reduction

Cette clause effectue une réduction sur les variables scalaires qui apparaissent dans la liste variable-list, avec l’opérateur op. La syntaxe de la clause reduction est la suivante :

reduction( op : variable-list )

Une réduction est généralement spécifiée pour une instruction sous l’une des formes suivantes :

  • x = x op expr
  • x binop = expr
  • x = expr op x (sauf soustraction)
  • x ++
  • ++ x
  • x --
  • -- x

où :

x
Une des variables de réduction spécifiées dans la liste.

variable-list
Liste séparée par des virgules de variables de réduction scalaire.

expr
Expression avec un type scalaire qui ne fait pas référence à x.

op
Pas un opérateur surchargé, mais un de +, *, -, &, ^, |, && ou ||.

binop
Pas un opérateur surchargé, mais un de +, *, -, &, ^, ou |.

Voici un exemple de la clause reduction :

#pragma omp parallel for reduction(+: a, y) reduction(||: am)
for (i=0; i<n; i++) {
   a += b[i];
   y = sum(y, c[i]);
   am = am || b[i] == c[i];
}

Comme illustré dans l’exemple, un opérateur peut être masqué à l’intérieur d’un appel de fonction. L’utilisateur doit faire attention à ce que l’opérateur spécifié dans la clause reduction corresponde à l’opération de réduction.

Bien que les opérandes de droite de l’opérateur || n’ait aucun effet secondaire dans cet exemple, elles est autorisées, mais doivent être utilisées avec précaution. Dans ce contexte, un effet secondaire qui est garanti de ne pas se produire pendant l’exécution séquentielle de la boucle peut se produire pendant l’exécution parallèle. Cette différence peut se produire, car l’ordre d’exécution des itérations est indéterminé.

L’opérateur est utilisé pour déterminer la valeur initiale de toutes les variables privées utilisées par le compilateur pour la réduction et pour déterminer l’opérateur de finalisation. Spécifier l’opérateur permet explicitement à l’instruction de réduction d’être en dehors de l’étendue lexicale de la construction. Un nombre quelconque de clauses reduction peut être spécifié sur la directive, mais une variable peut apparaître dans maximum une clause reduction pour cette directive.

Une copie privée de chaque variable dans la liste variable-list est créée, une pour chaque thread, comme si la clause private avait été utilisée. La copie privée est initialisée en fonction de l’opérateur (voir le tableau suivant).

À la fin de la région pour laquelle la clause reduction a été spécifiée, l’objet d’origine est mis à jour pour refléter le résultat de la combinaison de sa valeur d’origine avec la valeur finale de chacune des copies privées à l’aide de l’opérateur spécifié. Les opérateurs de réduction sont tous associatifs (à l’exception de la soustraction), et le compilateur peut librement réassocier le calcul de la valeur finale. (Les résultats partiels d’une réduction de soustraction sont ajoutés pour former la valeur finale.)

La valeur de l’objet d’origine devient indéterminée lorsque le premier thread atteint la clause conteneur, et le reste jusqu’à ce que le calcul de réduction soit terminé. Normalement, le calcul se termine à la fin de la construction. Toutefois, si la clause reduction est utilisée sur une construction à laquelle nowait est également appliquée, la valeur de l’objet d’origine reste indéterminée jusqu’à ce qu’une synchronisation de barrière ait été effectuée pour s’assurer que tous les threads ont terminé la clause reduction.

Le tableau suivant répertorie les opérateurs valides et leurs valeurs d’initialisation canonique. La valeur d’initialisation réelle est cohérente avec le type de données de la variable de réduction.

Opérateur Initialisation
+ 0
* 1
- 0
& ~0
| 0
^ 0
&& 1
|| 0

Voici les restrictions à la clause reduction :

  • Le type des variables de la clause reduction doit être valide pour l’opérateur de réduction, sauf que les types pointeur et référence ne sont jamais autorisés.

  • Une variable spécifiée dans la clause reduction ne doit pas être qualifiée const.

  • Les variables privées dans une région parallèle ou qui apparaissent dans la clause reduction d’une directive parallel ne peuvent pas être spécifiées dans une clause reduction d’une directive de partage de travail qui est liée à la construction parallel.

    #pragma omp parallel private(y)
    { /* ERROR - private variable y cannot be specified
                  in a reduction clause */
        #pragma omp for reduction(+: y)
        for (i=0; i<n; i++)
           y += b[i];
    }
    
    /* ERROR - variable x cannot be specified in both
                a shared and a reduction clause */
    #pragma omp parallel for shared(x) reduction(+: x)
    

2.7.2.7 copyin

La clause copyin fournit un mécanisme permettant d’affecter la même valeur aux variables threadprivate pour chaque thread de l’équipe exécutant la région parallèle. Pour chaque variable spécifiée dans une clause copyin, la valeur de la variable dans le thread principal de l’équipe est copiée, comme si par affectation, aux copies privées de thread au début de la région parallèle. La syntaxe de la clause copyin est la suivante :

copyin(
variable-list
)

Voici les restrictions à la clause copyin :

  • Une variable spécifiée dans la clause copyin doit avoir un opérateur d’assignation de copie accessible et non ambigu.

  • Une variable spécifiée dans la clause copyin doit être une variable threadprivate.

2.7.2.8 copyprivate

La clause copyprivate fournit un mécanisme permettant d’utiliser une variable privée pour diffuser une valeur d’un membre d’une équipe vers les autres membres. Il s’agit d’une alternative à l’utilisation d’une variable partagée pour la valeur lorsqu’une telle variable partagée est fournie (par exemple, dans une récursivité nécessitant une variable différente à chaque niveau). La clause copyprivate ne peut apparaître que dans la directive single.

La syntaxe de la clause copyprivate est la suivante :

copyprivate(
variable-list
)

L’effet de la clause copyprivate sur les variables de sa liste variable-list se produit après l’exécution du bloc structuré associé à la construction single, et avant que les threads de l’équipe n’aient laissé la barrière à la fin de la construction. Ensuite, dans tous les autres threads de l’équipe, chaque variable de la liste variable-list est définie (comme si par affectation) avec la valeur de la variable correspondante dans le thread qui a exécuté le bloc structuré de la construction.

Voici les restrictions à la clause copyprivate :

  • Une variable spécifiée dans la clause copyprivate ne doit pas apparaître dans une clause private ou firstprivate pour la même directive single.

  • Si une directive single avec une clause copyprivate est rencontrée dans l’étendue dynamique d’une région parallèle, toutes les variables spécifiées dans la clause copyprivate doivent être privées dans le contexte englobant.

  • Une variable spécifiée dans la clause copyprivate doit avoir un opérateur d’assignation de copie accessible et non ambigu.

2.8 Liaison de directives

La liaison dynamique de directives doit respecter les règles suivantes :

  • Les directives for, sections, single, master et barrier sont liées au parallel englobant dynamiquement, se celui-ci existe, quelle que soit la valeur d’une clause if éventuellement présente sur cette directive. Si aucune région parallèle n’est en cours d’exécution, les directives sont exécutées par une équipe composée uniquement du thread principal.

  • La directive ordered est liée au for englobant dynamiquement.

  • La directive atomic applique l’accès exclusif vis-à-vis des directives atomic dans tous les threads, pas seulement l’équipe actuelle.

  • La directive critical applique l’accès exclusif vis-à-vis des directives critical dans tous les threads, pas seulement l’équipe actuelle.

  • Une directive ne peut jamais se lier à une directive en dehors du plus proche parallel englobant dynamiquement.

2.9 Imbrication de directives

L’imbrication dynamique de directives doit respecter les règles suivantes :

  • Une directive parallel dynamiquement à l’intérieur d’une autre parallel établit logiquement une nouvelle équipe, composée uniquement du thread actuel, sauf si le parallélisme imbriqué est activé.

  • Les directives for, sections et single qui se lient à la même parallel ne sont pas autorisées à s’imbriquer entre elles.

  • Les directives critical portant le même nom ne sont pas autorisées à s’imbriquer entre elles. Notez que cette restriction n’est pas suffisante pour empêcher le blocage.

  • Les directives for, sections et single ne sont pas autorisées dans l’étendue dynamique des régions critical, ordered et master si les directives sont liées au même parallel que les régions.

  • Les directives barrier ne sont pas autorisées dans l’étendue dynamique des régions for, ordered, sections, single, master et critical si les directives sont liées au même parallel que les régions.

  • Les directives master ne sont pas autorisées dans l’étendue dynamique des régions for, sections et single si les directives master sont liées au même parallel que les directives de partage de travail.

  • Les directives ordered ne sont pas autorisées dans l’étendue dynamique des régions critical si les directives sont liées au même parallel que les régions.

  • Toute directive autorisée lorsqu’elle est exécutée dynamiquement à l’intérieur d’une région parallèle est également autorisée lorsqu’elle est exécutée en dehors d’une région parallèle. Lorsqu’elle est exécutée dynamiquement en dehors d’une région parallèle spécifiée par l’utilisateur, la directive est exécutée par une équipe composée uniquement du thread principal.