2016-06-01 2 views
1

J'ai observé un comportement inattendu (pour moi!) D'un code openmp que j'écris. La structure de code est le suivant:Problèmes de performances de #pragma omp atomic avec OMP_NUM_THREADS = 1

#pragma omp parallel for 
for(int i=0;i<N;i++){ 
// lots of calculations that produce 3 integers i1,i2,i3 and 3 doubles d1,d2,d3 
#pragma omp atomic 
J1[i1] += d1; 
#pragma omp atomic 
J2[i2] += d2; 
#pragma omp atomic 
J3[i3] += d3; 
} 

j'ai compilé trois versions différentes de ce code:

1) avec openmp (-fopenmp)

2) sans openmp

3) avec openmp, mais sans les 3 opérations atomiques (juste comme un test, puisque les opérations atomiques sont nécessaires)

Lorsque j'exécute la version 1) avec la variable d'environnement O MP_NUM_THREADS = 1, j'observe un ralentissement significatif par rapport à la version 2); alors que la version 3) tourne aussi vite que la version 2).

Je voudrais connaître la raison de ce comportement (pourquoi les opérations atomiques ralentissent-elles le code même s'il est monotrou?!) Et s'il est possible de compiler/réécrire le code de telle sorte que la version 1) fonctionne aussi vite que la version 2).

Je joins à la fin de la question un exemple de travail qui montre le comportement mentionné ci-dessus. Je compilé 1) avec:

g++ -fopenmp -o toy_code toy_code.cpp -std=c++11 -O3 

2) avec:

g++ -o toy_code_NO_OMP toy_code.cpp -std=c++11 -O3 

et 3) avec:

g++ -fopenmp -o toy_code_NO_ATOMIC toy_code_NO_ATOMIC.cpp -std=c++11 -O3 

La version du compilateur est une version gcc 5.3.1 20160519 (Debian 5.3.1-20). Le temps d'exécution des 3 versions est:

1) 1 min 24 sec

2) 51 sec

3) 51 sec

Merci à l'avance pour tout conseil!

// toy_code.cpp 
#include <stdio.h> 
#include <iostream> 
#include <stdlib.h> 
#include <cmath> 
#include <omp.h> 
#define Np 1000000 
#define N 1000 

int main(){ 
     double* Xp, *Yp, *J,*Jb; 
     Xp = new double[Np]; 
     Yp = new double[Np]; 
     J = new double [N*N]; 
     Jb = new double [N*N]; 

     for(int i=0;i<N*N;i++){ 
      J[i]=0.0; 
      Jb[i]=0.0; 
     } 

     for(int i=0;i<Np;i++){ 
      Xp[i] = rand()*1.0/RAND_MAX - 0.5; 
      Yp[i] = rand()*1.0/RAND_MAX - 0.5; 
     } 

     for(int n=0; n<2000; n++){ 
     #pragma omp parallel for 
     for(int p=0;p<Np;p++){ 
      double rx = (Xp[p]+0.5)*(N-1); 
      double ry = (Yp[p]+0.5)*(N-1); 
      int xindex = (int)floor(rx+0.5); 
      int yindex = (int)floor(ry+0.5); 
      int k; 
      k=xindex*N+yindex; 

      #pragma omp atomic 
      J[k]+=1; 
      #pragma omp atomic 
      Jb[k]+=1; 
     } 
     } 

     delete[] Xp; 
     delete[] Yp; 
     delete[] J; 
     delete[] Jb; 

return 0; 
} 
+0

avec openmp activé, les pragmas sont étendus au code GOMP qui induit probablement un surcoût par rapport au code séquentiel –

Répondre

0

Si vous activez OpenMP, gcc doit générer un code différent qui fonctionne pour un certain nombre de fils qui est connu que lors de l'exécution.

Dans ce cas particulier, regardez la sortie de gcc -S (légèrement raccourcie par des étiquettes).

Sans OpenMP:

.loc 1 38 0 discriminator 2 # Line 38 is J[k]+=1; 
movsd 8(%rsp), %xmm1 
cvttsd2si %xmm0, %edx 
cvttsd2si %xmm1, %eax 
movsd .LC3(%rip), %xmm0 
imull $1000, %eax, %eax 
addl %edx, %eax 
cltq 
salq $3, %rax 
leaq 0(%r13,%rax), %rdx 
.loc 1 40 0 discriminator 2 # Line 40 is Jb[k]+=1; 
addq %r12, %rax 
.loc 1 29 0 discriminator 2 
cmpq $8000000, %r15 
.loc 1 38 0 discriminator 2 
addsd (%rdx), %xmm0 
movsd %xmm0, (%rdx) 
.loc 1 40 0 discriminator 2 
movsd .LC3(%rip), %xmm0 
addsd (%rax), %xmm0 
movsd %xmm0, (%rax) 

La boucle est Déroulé fait ce assez compliqué.

Avec -fopenmp:

movsd (%rsp), %xmm2 
cvttsd2si %xmm0, %eax 
cvttsd2si %xmm2, %ecx 
imull $1000, %ecx, %ecx 
addl %eax, %ecx 
movslq %ecx, %rcx 
salq $3, %rcx 
movq %rcx, %rsi 
addq 16(%rbp), %rsi 
movq (%rsi), %rdx 
movsd 8(%rsp), %xmm1 
jmp .L4 
movq %rax, %rdx 
movq %rdx, (%rsp) 
movq %rdx, %rax 
movsd (%rsp), %xmm3 
addsd %xmm1, %xmm3 
movq %xmm3, %rdi 
lock cmpxchgq %rdi, (%rsi) 
cmpq %rax, %rdx 
jne .L9 
.loc 1 40 0 
addq 24(%rbp), %rcx 
movq (%rcx), %rdx 
jmp .L5 
.p2align 4,,10 
.p2align 3 
movq %rax, %rdx 
movq %rdx, (%rsp) 
movq %rdx, %rax 
movsd (%rsp), %xmm4 
addsd %xmm1, %xmm4 
movq %xmm4, %rsi 
lock cmpxchgq %rsi, (%rcx) 
cmpq %rax, %rdx 
jne .L10 
addq $8, %r12 
cmpq %r12, %rbx 
jne .L6 

Je ne vais pas essayer d'expliquer ou de comprendre tous les détails de ce qui se passe ici, mais c'est pas nécessaire pour le message: Le compilateur doit utiliser des instructions atomiques qui sont probablement plus coûteux, en particulier lock cmpxchgq.En plus de ce problème fondamental, OpenMP peut jouer avec l'optimiseur de n'importe quelle manière imaginable, par ex. interférer avec le déroulement. J'ai également vu un curieux cas où le compilateur intel génère réellement un code série plus efficace pour une boucle OpenMP.

P.S. Considérez-vous chanceux - cela pourrait être bien pire. Si le compilateur ne peut pas mapper l'instruction atomique à une instruction matérielle, il doit utiliser des verrous qui seraient encore plus lents.

+0

Pour éviter le problème, j'ai ajouté une instruction if pour vérifier le nombre de threads de sorte que dans le cas d'un thread unique, sans aucune directive pragma. Je vous remercie. – sparappaband