Parallélisme de boucle dans OpenMP

Paramètres

Clause Paramètre
privé Liste de variables privées séparées par des virgules
premierprivé Comme private, mais initialisé à la valeur de la variable avant d’entrer dans la boucle
dernierprivé Comme private, mais la variable obtiendra la valeur correspondant à la dernière itération de la boucle lors de la sortie
réduction opérateur de réduction : liste séparée par des virgules des variables de réduction correspondantes
horaire static, dynamic, guided, auto ou runtime avec une taille de bloc optionnelle après une virgule pour les 3 premiers
réduire Nombre de boucles parfaitement imbriquées à réduire et à paralléliser ensemble
commandé Indique que certaines parties de la boucle devront être conservées dans l’ordre (ces parties seront spécifiquement identifiées avec des clauses “ordonnées” à l’intérieur du corps de la boucle)
maintenant Supprimer la barrière implicite existant par défaut à la fin de la construction de la boucle

La signification de la clause “schedule” est la suivante :

  • static[,chunk] : distribue statiquement (ce qui signifie que la distribution est effectuée avant d’entrer dans la boucle) les itérations de la boucle par lots de la taille de chunk de manière circulaire. Si chunk n’est pas spécifié, alors les morceaux sont aussi égaux que possible et chaque thread en reçoit au plus un.
  • dynamic[,chunk] : Distribuez les itérations de boucle parmi les threads par lots de taille chunk avec une politique du premier arrivé, premier servi, jusqu’à ce qu’il ne reste plus de lot. S’il n’est pas spécifié, chunk est défini sur 1
  • guided[,chunk] : comme dynamic mais avec des lots dont les tailles deviennent de plus en plus petites, jusqu’à 1
  • auto : laissez le compilateur et/ou la bibliothèque d’exécution décider ce qui convient le mieux
  • runtime : Diffère la décision à l’exécution au moyen de la variable d’environnement OMP_SCHEDULE. Si au moment de l’exécution la variable d’environnement n’est pas définie, la planification par défaut sera utilisée

La valeur par défaut pour schedule est définition d’implémentation. Dans de nombreux environnements, il est “statique”, mais peut également être “dynamique” ou très bien “auto”. Par conséquent, veillez à ce que votre implémentation ne s’appuie pas implicitement sur elle sans la définir explicitement.

Dans les exemples ci-dessus, nous avons utilisé la forme fusionnée “parallel for” ou “parallel do”. Cependant, la construction de boucle peut être utilisée sans la fusionner avec la directive parallel, sous la forme d’une directive autonome #pragma omp for [...] ou !$omp do [...] dans un région “parallèle”.

Pour la version Fortran uniquement, la ou les variables d’index de boucle de la ou des boucles parallélisées est (sont) toujours “privée” par défaut. Il n’est donc pas nécessaire de les déclarer explicitement “privés” (bien que cela ne soit pas une erreur).
Pour la version C et C++, les index de boucle sont comme n’importe quelle autre variable. Par conséquent, si leur portée s’étend en dehors de la ou des boucles parallélisées (c’est-à-dire si elles ne sont pas déclarées comme for ( int i = ...) mais plutôt comme `int i; … for ( i = … )’ alors ils doivent être déclarés ‘privés’.

Exemple typique en C

#include <stdio.h>
#include <math.h>
#include <omp.h>

#define N 1000000

int main() {
    double sum = 0;

    double tbegin = omp_get_wtime();
    #pragma omp parallel for reduction( +: sum )
    for ( int i = 0; i < N; i++ ) {
        sum += cos( i );
    }
    double wtime = omp_get_wtime() - tbegin;

    printf( "Computing %d cosines and summing them with %d threads took %fs\n",
            N, omp_get_max_threads(), wtime );

    return sum;
}

Dans cet exemple, nous calculons simplement 1 million de cosinus et additionnons leurs valeurs en parallèle. Nous chronométrons également l’exécution pour voir si la parallélisation a un effet sur les performances. Enfin, puisque nous mesurons le temps, nous devons nous assurer que le compilateur n’optimisera pas le travail que nous avons fait, nous faisons donc semblant d’utiliser le résultat en le renvoyant simplement.

Même exemple en Fortran

program typical_loop
    use omp_lib
    implicit none
    integer, parameter :: N = 1000000, kd = kind( 1.d0 )
    real( kind = kd ) :: sum, tbegin, wtime
    integer :: i

    sum = 0

    tbegin = omp_get_wtime()
    !$omp parallel do reduction( +: sum )
    do i = 1, N
        sum = sum + cos( 1.d0 * i )
    end do
    !$omp end parallel do
    wtime = omp_get_wtime() - tbegin

    print "( 'Computing ', i7, ' cosines and summing them with ', i2, &
        & ' threads took ', f6.4,'s' )", N, omp_get_max_threads(), wtime

    if ( sum > N ) then
        print *, "we only pretend using sum"
    end if
end program typical_loop

Ici encore, nous calculons et accumulons 1 million de cosinus. Nous chronométrons la boucle et pour éviter une optimisation indésirable du compilateur, nous faisons semblant d’utiliser le résultat.

Compilation et exécution des exemples

Sur une machine Linux à 8 cœurs utilisant GCC version 4.4, les codes C peuvent être compilés et exécutés de la manière suivante :

$ gcc -std=c99 -O3 -fopenmp loop.c -o loopc -lm
$ OMP_NUM_THREADS=1 ./loopc
Computing 1000000 cosines and summing them with 1 threads took 0.095832s
$ OMP_NUM_THREADS=2 ./loopc
Computing 1000000 cosines and summing them with 2 threads took 0.047637s
$ OMP_NUM_THREADS=4 ./loopc
Computing 1000000 cosines and summing them with 4 threads took 0.024498s
$ OMP_NUM_THREADS=8 ./loopc
Computing 1000000 cosines and summing them with 8 threads took 0.011785s

Pour la version Fortran, cela donne :

$ gfortran -O3 -fopenmp loop.f90 -o loopf
$ OMP_NUM_THREADS=1 ./loopf
Computing 1000000 cosines and summing them with  1 threads took 0.0915s
$ OMP_NUM_THREADS=2 ./loopf
Computing 1000000 cosines and summing them with  2 threads took 0.0472s
$ OMP_NUM_THREADS=4 ./loopf
Computing 1000000 cosines and summing them with  4 threads took 0.0236s
$ OMP_NUM_THREADS=8 ./loopf
Computing 1000000 cosines and summing them with  8 threads took 0.0118s

Ajout de deux vecteurs utilisant OpenMP parallèle pour la construction

void parallelAddition (unsigned N, const double *A, const double *B, double *C)
{
    unsigned i;

    #pragma omp parallel for shared (A,B,C,N) private(i) schedule(static)
    for (i = 0; i < N; ++i)
    {
        C[i] = A[i] + B[i];
    }
}

Cet exemple ajoute deux vecteurs (A et B dans C) en créant une équipe de threads (spécifiés par la variable d’environnement OMP_NUM_THREADS, par exemple) et en attribuant à chaque thread un morceau de travail (dans cet exemple, attribué statiquement via l’expression schedule(static)).

Voir la section des remarques concernant l’option “private(i)”.