Outils pour utilisateurs

Outils du site


lang:cpp:openmp

Compilation

Compilateur

CMake

find_package(OpenMP)
if(OpenMP_CXX_FOUND)
  target_link_libraries(MyTarget PUBLIC OpenMP::OpenMP_CXX)
endif()

Modern CMake Archive du 01/30/2020 le 04/03/2020

Généralités de #pragma

Définitions

  • team : représente le groupe de threads utilisé par OpenMP.

Fonctions OpenMP

  • omp_get_thread_num() : le nième thread de la team à partir de 0.
  • omp_get_num_threads() : nombre de threads dans la team.

#pragma omp parallel

Doit être placé juste avant un bloc {}.

#pragma omp parallel
{
  for (int64_t i = 0; i < 4000000000; i++)
    {}
}

Le code va s'exécute en parallèle. Avec cette seule instruction, la boucle for est exécutée 8*4000000000.

#pragma omp for

Généralités

Doit être placé juste avant une boucle for.

La boucle va être décomposée en n blocs (n étant le nombre de threads du CPU). L'ordre d'exécution des blocs est séquentiel car l'instruction ne précise pas l'exécution en parallèle.

#pragma omp for
for (int n = 0; n < 10; ++n)
  printf(" %d", n);

#pragma omp parallel for

Doit être placé juste avant une boucle for.

Là, la boucle est découpée et exécutée en parallèle, ce qui est le but recherché.

#pragma omp parallel for
for (int64_t i = 0; i < 4000000000; i++)
  {}

C'est l'équivalent au code ci-dessous :

#pragma omp parallel
{
  #pragma omp for
  for (int64_t i = 0; i < 4000000000; i++)
    {}
}

if

#pragma est exécutée si la condition est vraie.

extern int parallelism_enabled;
#pragma omp parallel for if(parallelism_enabled)

num_threads(X)

Indique le nombre de threads de la team.

#pragma omp parallel num_threads(3)

Gestion des variables

La variable est locale (private) à chaque thread :

  int n,m;
  #pragma omp parallel private(n)
    printf("%d %#010zx\n", omp_get_thread_num(), &n);
  printf("%#010zx\n", &m);

L'adresse de la variable n est différente pour chaque thread.

Rendu :

4 0x7f341ff72e14
0 0x7ffd31aadaf4
7 0x7f341e76fe14
3 0x7f3420773e14
1 0x7f3421775e14
6 0x7f341ef70e14
5 0x7f341f771e14
2 0x7f3420f74e14
0x7ffd31aadb44

On voit qu'un bloc de taille 0x801000 est alloué pour que thread.

Taille des blocs

  • static

Par défaut, tous les threads s'occupent de la même taille de données (static).

#pragma omp for schedule(static)
  • dynamic

Le thread interroge OpenMP pour savoir quelle plage de données il doit exécuter puis une fois terminée, il interroge OpenMP à nouveau.

Utile quand chaque élément dans la boucle n'a pas un temps d'exécution fixe ou quand on utilise ordered pour éviter de bloquer les threads suivants.

La taille de chaque bloc peut aussi être imposée.

#pragma omp for schedule(dynamic, 3)
  • guided

C'est un mix entre static et dynamic.

  • runtime

C'est OpenMP qui décide à l'exécution quelle est la meilleure stratégie à appliquer.

  • monolithic / nonmonolithic
    • monolithic : les blocs sont exécutés par ordre croissant,
    • nonmonolithic : les blocs sont exécutés par ordre non forcément croissant,
#pragma omp for schedule(nonmonotonic:dynamic)
  • Quelques tests

Les cas 1, 2 et 3 présentent les résultats pour comparer la différence entre for, parallel et parallel for.

Les cas 4, 5, 6 et 7 montrent la différence entre les différents types de schedule et l'impact de ordered

int main()
{
  #pragma …
  for (int i = 0; i < 6; i++)
  {
    printf("%d %d\n", omp_get_thread_num(), i);
  }
}
  • 1 #pragma omp for : la boucle for est exécutée dans la team (de 1 thread) car pas de parallel.
0 0
0 1
0 2
0 3
0 4
0 5
  • 2 #pragma omp parallel num_threads(3) : la boucle for est bien dupliquée et exécutée en parallèle.
0 0
0 1
2 0
2 1
0 2
0 3
0 4
0 5
1 0
1 1
1 2
1 3
1 4
1 5
2 2
2 3
2 4
2 5
  • 3 #pragma omp parallel for num_threads(3) : la boucle for est exécutée en parallèle sans duplication.
0 0
0 1
2 4
1 2
1 3
2 5
  • 4 #pragma omp parallel for schedule(dynamic) num_threads(3) : la boucle for est exécutée en parallèle sans duplication.
1 2
1 3
1 4
1 5
0 0
2 1

Ici, c'est le thread 0 qui fait tout. Cela peut s'expliquer par le délai très court pour exécuter chaque thread. Avec dynamic, OpenMP a décidé de créer des tailles de blocs de 1. C'est le deuxième thread (n°1) qui s'exécute en premier et qui interroge OpenMP pour connaitre son bloc suivant. Il le fait 3 fois d'affilé et termine la boucle. C'est uniquement après que les autres threads s'exécutent. Avec une boucle plus grande (30 itérations), on a :

0 0
0 3
0 4
0 5
2 1
2 7
2 8
2 9
1 2
1 11
1 12
0 6
0 14
1 13
2 10
0 15
1 16
1 19
1 20
1 21
1 22
1 23
1 24
1 25
1 26
1 27
1 28
1 29
0 18
2 17

On voit que au départ chaque thread exécute une itération de la boucle mais que le travail est réparti entre chaque thread de la team.

  • 5 #pragma omp parallel for schedule(nonmonotonic:dynamic) num_threads(3) : les blocs de la boucle for sont exécutée sans obligation de les lancer dans l'ordre. J'ai pas vraiment vu de réel différence avec cet exemple.
  • 6 #pragma omp parallel for schedule(static) num_threads(3)
int main()
{
  #pragma omp parallel for ordered schedule(static) num_threads(3)
  for (int i = 0; i < 6; i++)
  {
    printf("%d %d\n", omp_get_thread_num(), i);
    #pragma omp ordered
    { 
      printf("%d\n", i);
    }
  }
}

Rendu :

0 0
0
1 2
2 4
0 1
1
2
1 3
3
4
2 5
5

On voit que les threads n°1 et 2 sont dans l'attente du thread n°0 à partir de la ligne 4. Ici, le problème est d'utiliser schedule(static). L'utilisation de schedule(dynamic) va créer des boucles de taille 1 pour réduire ces attentes.

  • 7 #pragma omp parallel for schedule(dynamic) num_threads(3)
0 0
0
0 3
2 2
1 1
1
1 4
2
3
4
2 5
5

Avec dynamic, les threads attendent moins.

Dans notre cas particulier, l'utilisation de dynamic est plus lente car le processeur va passer plus de temps à changer de thread que d'exécuter le contenu de la boucle. Il faut donc que chaque itération de la boucle ait une charge assez soutenue.

collapse

Fusionne les boucles imbriquées.

#pragma omp parallel for collapse(2) num_threads(3)
for(int y=0; y<4; ++y)
  for(int x=0; x<3; ++x)
  {
    printf("%d %d %d\n", omp_get_thread_num(), x, y);
  }

Les bornes de la boucle intérieure ne doit pas dépendre de la variable de la boucle externe.

0 0 0
0 1 0
0 2 0
0 0 1
2 2 2
2 0 3
2 1 3
2 2 3
1 1 1
1 2 1
1 0 2
1 1 2

reduction

Est utilisé si une variable commune est modifiée lors de la parallélisation.

int main()
{
int sum(0);
#pragma omp parallel for schedule(...) num_threads(3) reduction(+:sum)
for(int x=0; x<100000000; ++x)
{
  sum += 1;
}
printf("%d\n", sum);
}
scheduleVariable reductionRésultat Temps
static int non 50231932 0m0,431s
static int oui 1000000000m0,071s
static std::atomic<int>non 1000000000m1,886s
dynamic int non 51166934 0m3,087s
dynamic int oui 1000000000m1,890s
dynamic std::atomic<int>non 1000000000m3,812s

#pragma omp sections

Défini des blocs de code qui peuvent s'exécuter en parallèle dans la team. Il faut donc le coupler avec #pragma omp parallel.

Ci-dessous, les 3 blocs s'exécutent en parallèle.

#pragma omp parallel sections
{
  {
    Work1();
  }
  #pragma omp section
  {
    Work2();
    Work3();
  }
  #pragma omp section
  {
    Work4();
  }
}

#pragma omp simd

SIMD Vectorization with OpenMP Archive du 09/03/2019

Cela parallélise les instructions de calcul pour utiliser au mieux les SSE* et autres. On peut autant y faire des calculs flottants qu'en entier. Le fonctionnement est similaire aux calculs CUDA :

#pragma omp simd
for(int n=0; n<size; ++n)
  sinTable[n] = std::sin(2 * M_PI * n / size);

aligned

SSE2 a besoin que les variables soient alignées en multiple de 16 octets. On peut dire à OpenMP que les variables sont toujours correctement alignées. Mais dans le cas contraire, les calculs seront faux.

On peut déclarer soit au niveau de la variable, soit au niveau de la fonction.

#pragma omp declare simd aligned(a,b:16)
void add_arrays(float *__restrict__ a, float *__restrict__ b)
{
  #pragma omp simd aligned(a,b:16)
  for(int n=0; n<8; ++n) a[n] += b[n];
}	

__restrict__ permet de dire que le contenu ne change pas et que personne ne pointe dessus sauf la variable utilisée.

safelen

Limite le nombre de calculs en parallèle via simd. Utile si deux tableaux se superposent.

simdlen

Taille des blocs SIMD à calculer en même temps.

linear

Incrémente pour chaque boucle une variable.

#pragma omp simd linear(b:2)
for(int n=0; n<8; ++n) array[n] = b;

Ne marche pas avec GCC 8.

uniform

Indique d'une variable est une constante.

#pragma omp task

Déclare des tâches qui seront exécutée dans un thread parallèle.

On utilise taskwait pour attendre que les tâches soient terminées.

#pragma omp single

Impose l'exécution d'un bloc par un seul thread. Ci-dessous, la boucle est exécutée une seule fois son contenu est exécuté en parallèle grâce à #pragma omp task.

<#pragma omp parallel
{
  #pragma omp single
  {
    #pragma omp task
    {
      printf("%d\n", omp_get_thread_num());
    }
    #pragma omp task
    {
      printf("%d\n", omp_get_thread_num());
    }
    #pragma omp task
    {
      printf("%d\n", omp_get_thread_num());
    }
    #pragma omp taskwait
    printf("Fin %d\n", omp_get_thread_num());
  }
}

Bugs / messages d'erreur

Charger dynamiquement une DLL qui est liée à OpenMP

OpenMP gère ses threads comme Windows. Quand un thread a terminé ce qu'il avait à faire, il reste dans la pool et est stocké en vue d'une éventuelle réutilisation.

Dans le cas de la DLL de OpenMP, si on décharge la DLL ayant chargé OpenMP et que le pool de threads n'est pas vide, il y a un crash.

Solution : définir obligatoirement la variable d'environnement OMP_WAIT_POLICY à passive pour que la durée avant libération de la mémoire des threads après leur mort soit nulle.

Selon le code source de gcc (libgomp\env.c) et la fonction parse_wait_policy :

OMP_WAIT_POLICYDurée de vie du pool
Non défini. 3 ms
active 5 minutes
passive 0 ms

VC++: crash when freeing a DLL built with openMP Archive du 23/12/2015 le 16/10/2019

lang/cpp/openmp.txt · Dernière modification : 2022/09/05 23:05 de root