Quelle est la différence dans OpenMP entre:
#pragma omp parallel sections
{
#pragma omp section
{
fct1();
}
#pragma omp section
{
fct2();
}
}
et :
#pragma omp parallel
{
#pragma omp single
{
#pragma omp task
fct1();
#pragma omp task
fct2();
}
}
Je ne suis pas sûr que le deuxième code soit correct ...
La différence entre les tâches et les sections réside dans le délai d'exécution du code. Les sections sont incluses dans la construction sections
et (sauf si la clause nowait
a été spécifiée), les threads ne la laisseront pas avant que toutes les sections aient été exécutées:
[ sections ]
Thread 0: -------< section 1 >---->*------
Thread 1: -------< section 2 >*------
Thread 2: ------------------------>*------
... *
Thread N-1: ---------------------->*------
Ici, les threads N
rencontrent une construction sections
avec deux sections, la seconde prenant plus de temps que la première. Les deux premiers threads exécutent chacun une section. Les autres threads N-2
Attendent simplement à la barrière implicite à la fin de la construction des sections (montrez ici comme *
).
Les tâches sont mises en file d'attente et exécutées chaque fois que possible aux points dits de planification des tâches. Dans certaines conditions, le runtime peut être autorisé à déplacer la tâche entre les threads, même au milieu de leur durée de vie. Ces tâches sont appelées non liées et une tâche non liée peut commencer à s'exécuter dans un thread, puis à un certain moment de la planification, elle peut être migrée par le runtime vers un autre thread.
Pourtant, les tâches et les sections sont à bien des égards similaires. Par exemple, les deux fragments de code suivants obtiennent essentiellement le même résultat:
// sections
...
#pragma omp sections
{
#pragma omp section
foo();
#pragma omp section
bar();
}
...
// tasks
...
#pragma omp single nowait
{
#pragma omp task
foo();
#pragma omp task
bar();
}
#pragma omp taskwait
...
taskwait
fonctionne très comme barrier
mais pour les tâches - il garantit que le flux d'exécution en cours sera suspendu jusqu'à ce que toutes les tâches en file d'attente aient été exécutées. Il s'agit d'un point de planification, c'est-à-dire qu'il permet aux threads de traiter les tâches. La construction single
est nécessaire pour que les tâches soient créées par un seul thread. S'il n'y avait pas de construction single
, chaque tâche serait créée num_threads
Fois, ce qui pourrait ne pas être ce que l'on souhaite. La clause nowait
dans la construction single
indique aux autres threads de ne pas attendre que la construction single
soit exécutée (c'est-à-dire supprime la barrière implicite à la fin de la single
construct). Ils ont donc frappé le taskwait
immédiatement et ont commencé les tâches de traitement.
taskwait
est un point de planification explicite illustré ici pour plus de clarté. Il existe également des points d'ordonnancement implicites, notamment à l'intérieur de la synchronisation de la barrière, qu'ils soient explicites ou implicites. Par conséquent, le code ci-dessus pourrait également s'écrire simplement:
// tasks
...
#pragma omp single
{
#pragma omp task
foo();
#pragma omp task
bar();
}
...
Voici un scénario possible de ce qui pourrait se produire s'il y a trois threads:
+--+-->[ task queue ]--+
| | |
| | +-----------+
| | |
Thread 0: --< single >-| v |-----
Thread 1: -------->|< foo() >|-----
Thread 2: -------->|< bar() >|-----
Ici, dans le | ... |
Se trouve l'action du point de planification (soit la directive taskwait
ou la barrière implicite). En gros, les threads 1
Et 2
Suspendent ce qu'ils font à ce stade et commencent le traitement des tâches à partir de la file d'attente. Une fois toutes les tâches traitées, les threads reprennent leur flux d'exécution normal. Notez que les threads 1
Et 2
Peuvent atteindre le point de planification avant que le thread 0
Ait quitté la construction single
, donc les |
S de gauche n'a pas besoin d'être aligné (cela est représenté sur le schéma ci-dessus).
Il peut également arriver que le thread 1
Soit en mesure de terminer le traitement de la tâche foo()
et d'en demander une autre avant même que les autres threads puissent demander des tâches. Ainsi, foo()
et bar()
peuvent être exécutées par le même thread:
+--+-->[ task queue ]--+
| | |
| | +------------+
| | |
Thread 0: --< single >-| v |---
Thread 1: --------->|< foo() >< bar() >|---
Thread 2: --------------------->| |---
Il est également possible que le thread isolé exécute la deuxième tâche si le thread 2 arrive trop tard:
+--+-->[ task queue ]--+
| | |
| | +------------+
| | |
Thread 0: --< single >-| v < bar() >|---
Thread 1: --------->|< foo() > |---
Thread 2: ----------------->| |---
Dans certains cas, le compilateur ou le runtime OpenMP peut même contourner complètement la file d'attente des tâches et exécuter les tâches en série:
Thread 0: --< single: foo(); bar() >*---
Thread 1: ------------------------->*---
Thread 2: ------------------------->*---
Si aucun point de planification des tâches n'est présent dans le code de la région, le runtime OpenMP peut démarrer les tâches chaque fois qu'il le juge approprié. Par exemple, il est possible que toutes les tâches soient différées jusqu'à ce que la barrière à la fin de la région parallel
soit atteinte.