La manière correcte de sumr deux tableaux avec SSE2 SIMD en C ++

Commençons par inclure les éléments suivants:

#include  #include  using namespace std; 

Supposons maintenant que l’on ait les trois std:vector :

 N = 1048576; vector a(N); vector b(N); vector c(N); default_random_engine randomGenerator(time(0)); uniform_real_dissortingbution diceroll(0.0f, 1.0f); for(int i-0; i<N; i++) { a[i] = diceroll(randomGenerator); b[i] = diceroll(randomGenerator); } 

Supposons maintenant qu’il faut sumr a et b élément par élément et stocker le résultat dans c , lequel, sous forme scalaire, ressemble à ceci:

 for(int i=0; i<N; i++) { c[i] = a[i] + b[i]; } 

Quelle serait la version vectorisée SSE2 du code ci-dessus, en gardant à l’esprit que les entrées sont a et b telles que définies ci-dessus (c’est-à-dire en tant que collection de float ) et que la sortie est c (également une collection de float )?

Après de nombreuses recherches, j’ai pu trouver ce qui suit:

 for(int i=0; i<N; i+=4) { float a_toload[4] = { a[i], a[i + 1], a[i + 2], a[i + 3] }; float b_toload[4] = { b[i], b[i + 1], b[i + 2], b[i + 3] }; __m128 loaded_a = _mm_loadu_ps(a_toload); __m128 loaded_b = _mm_loadu_ps(b_toload); float result[4] = { 0, 0, 0, 0 }; _mm_storeu_ps(result, _mm_add_ps(loaded_a , loaded_b)); c[i] = result[0]; c[i + 1] = result[1]; c[i + 2] = result[2]; c[i + 3] = result[3]; } 

Cependant, cela semble être vraiment lourd et certainement certainement assez inefficace: la version SIMD ci-dessus est en réalité trois fois plus lente que la version scalaire initiale (mesurée, bien sûr, avec des optimisations, en mode de publication de Microsoft VS15, et après 1 million d’itérations , pas seulement 12).

Votre boucle for pourrait être simplifiée à

 const int aligendN = N - N % 4; for (int i = 0; i < alignedN; i+=4) { _mm_storeu_ps(&c[i], _mm_add_ps(_mm_loadu_ps(&a[i]), _mm_loadu_ps(&b[i]))); } for (int i = alignedN; i < N; ++i) { c[i] = a[i] + b[i]; } 

Quelques explications supplémentaires:
1, une petite boucle manipulant les derniers flottants est commune et lorsque N%4 != 0 ou N est inconnu à la compilation, il est obligatoire.
2, je remarque que vous choisissez la version non alignée load / store, la pénalité est faible par rapport à la version alignée. J'ai trouvé ce lien à stackoverflow: la charge insortingnsèque SSE non alignée est-elle plus lente que la charge insortingnsèque alignée sur les processeurs Intel x64_64?

Vous n’avez pas besoin des tableaux intermédiaires pour charger les registres SSE. Il suffit de charger directement à partir de vos tableaux.

 auto loaded_a = _mm_loadu_ps(&a[i]); auto loaded_b = _mm_loadu_ps(&b[i]); _mm_storeu_ps(&c[i], _mm_add_ps(loaded_a, loaded_b)); 

Vous pouvez également omettre les deux variables loaded et les intégrer à l’ajout, bien que le compilateur doive le faire pour vous.

Vous devez faire attention à cela, car cela ne fonctionnera pas correctement si les tailles de vecteurs ne sont pas un multiple de 4 (vous aurez access après la fin du tableau, ce qui entraînera un comportement indéfini et une écriture après la fin du tableau pourrait être dommageable).