Comment utiliser openmp et AVX2 simultanément avec une réponse parfaite?

J’ai écrit le programme produit Masortingx-Vector sous OpenMP et AVX2.

Cependant, j’ai eu la mauvaise réponse à cause d’OpenMP. La vraie réponse est que toute la valeur du tableau c deviendrait 100.

Ma réponse était un mélange de 98, 99 et 100.

Le code actuel est ci-dessous.

J’ai compilé Clang avec -fopenmp, -mavx, -mfma.

#include "stdio.h" #include "math.h" #include "stdlib.h" #include "omp.h" #include "x86insortingn.h" void mv(double *a,double *b,double *c, int m, int n, int l) { int k; #pragma omp parallel { __m256d va,vb,vc; int i; #pragma omp for private(i, va, vb, vc) schedule(static) for (k = 0; k < l; k++) { vb = _mm256_broadcast_sd(&b[k]); for (i = 0; i < m; i+=4) { va = _mm256_loadu_pd(&a[m*k+i]); vc = _mm256_loadu_pd(&c[i]); vc = _mm256_fmadd_pd(vc, va, vb); _mm256_storeu_pd( &c[i], vc ); } } } } int main(int argc, char* argv[]) { // set variables int m; double* a; double* b; double* c; int i; m=100; // main program // set vector or matrix a=(double *)malloc(sizeof(double) * m*m); b=(double *)malloc(sizeof(double) * m*1); c=(double *)malloc(sizeof(double) * m*1); //preset for (i=0;i<m;i++) { a[i]=1; b[i]=1; c[i]=0.0; } for (i=m;i<m*m;i++) { a[i]=1; } mv(a, b, c, m, 1, m); for (i=0;i<m;i++) { printf("%e\n", c[i]); } free(a); free(b); free(c); return 0; } 

Je sais que la section critique aiderait. Cependant, la section critique était lente.

Alors, comment puis-je résoudre le problème?

L’opération fondamentale que vous voulez est

 c[i] = a[i,k]*b[k] 

Si vous utilisez un stockage de rangée majeure, cela devient

 c[i] = a[i*l + k]*b[k] 

Si vous utilisez le stockage des commandes en colonnes, cela devient

 c[i] = a[k*m + i]*b[k] 

Pour un ordre de rang majeur, vous pouvez paralléliser comme ceci

 #pragma omp parallel for for(int i=0; i 

Pour l'ordre des colonnes, vous pouvez paralléliser comme ceci

 #pragma omp parallel for(int k=0; k 

Les opérations masortingcielles sont des opérations de niveau 2 qui sont des opérations liées à la bande passante mémoire. Les opérations de niveau 1 et de niveau 2 ne sont pas adaptées, par exemple, au nombre de cœurs. Seules les opérations de niveau 3 (par exemple, la multiplication de masortingce dense) permettent d’ échelonner https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms#Level_3 .

Le problème ne vient pas de vos composants insortingnsèques AVX, examinons le code sans les composants insortingnsèques pendant une minute:

 void mv(double *a,double *b,double *c, int m, int n, int l) { #pragma omp parallel for schedule(static) for (int k = 0; k < l; k++) { double xb = b[k]; for (int i = 0; i < m; i++) { double xa = a[m*k+i]; double xc = c[i]; xc = xc + xa * xb; c[i] = xc; } } } 

Remarque: votre déclaration privée était techniquement correcte et redondante car déclarée à l'intérieur de la boucle parallèle, mais il est beaucoup plus facile de raisonner sur le code si vous déclarez les variables aussi localement que possible.

La condition de concurrence sur votre code est sur c[i] - ce que plusieurs threads tentent de mettre à jour. Maintenant, même si vous pouviez protéger cela avec, disons, une mise à jour atomique, la performance serait horrible: pas seulement à cause de la protection, mais aussi parce que les données de c[i] doivent être constamment déplacées entre des caches de cœurs différents.

Une solution consiste à utiliser une réduction de tableau sur c . Cela crée une copie privée de c pour chaque thread et ils sont fusionnés à la fin:

 void mv(double *a,double *b,double *c, int m, int n, int l) { #pragma omp parallel for schedule(static) reduction(+:c[:m]) for (int k = 0; k < l; k++) { for (int i = 0; i < m; i++) { c[i] += a[m*k+i] * b[k]; } } } 

Cela devrait être raisonnablement efficace tant que deux vecteurs m rentrent dans votre cache, mais que vous risquez de subir beaucoup de surcharge en raison de la charge de gestion des threads. Vous finirez par être limité par la largeur de bande de la mémoire car dans une multiplication masortingcielle-vectorielle, un seul calcul par élément est lu à partir d' a .

Quoi qu'il en soit, vous pouvez bien sûr échanger les boucles i et k et enregistrer la réduction, mais votre configuration d'access à la mémoire sur a disque sera inefficace (à foulée). Vous devez donc bloquer la boucle pour éviter cela.

Maintenant, si vous regardez le résultat d'un compilateur moderne , il générera lui-même du code SIMD. Bien sûr, vous pouvez appliquer vos propres composants insortingnsèques SIMD si vous le souhaitez. Mais assurez-vous de gérer correctement les cas extrêmes si m n’est pas divisible par 4 (vous ne l’aviez pas dans votre version originale).

En fin de compte, si vous voulez vraiment de la performance, utilisez les fonctions d’une bibliothèque BLAS (par exemple, MKL). Si vous voulez jouer avec l'optimisation, il existe de nombreuses opportunités pour aller dans les détails.