Pourquoi _mm_stream_ps génère-t-il des erreurs de cache L1 / LL?

J’essaie d’optimiser un algorithme exigeant beaucoup de calculs et suis bloqué par un problème de cache. J’ai un énorme tampon qui est écrit de temps en temps et au hasard et ne lit qu’une fois à la fin de l’application. De toute évidence, l’écriture dans la mémoire tampon produit de nombreuses erreurs de cache et pollue les caches qui sont ensuite nécessaires à nouveau pour le calcul. J’ai essayé d’utiliser des instructions de déplacement non temporelles, mais les erreurs de cache (signalées par valgrind et supscopes par les mesures d’exécution) se produisent toujours. Cependant, afin d’approfondir mes recherches sur les mouvements non temporels, j’ai écrit un petit programme de test, que vous pouvez voir ci-dessous. Accès séquentiel, grand tampon, écrit seulement.

#include  #include  #include  #include  void tim(const char *name, void (*func)()) { struct timespec t1, t2; clock_gettime(CLOCK_REALTIME, &t1); func(); clock_gettime(CLOCK_REALTIME, &t2); printf("%s : %f s.\n", name, (t2.tv_sec - t1.tv_sec) + (float) (t2.tv_nsec - t1.tv_nsec) / 1000000000); } const int CACHE_LINE = 64; const int FACTOR = 1024; float *arr; int length; void func1() { for(int i = 0; i < length; i++) { arr[i] = 5.0f; } } void func2() { for(int i = 0; i < length; i += 4) { arr[i] = 5.0f; arr[i+1] = 5.0f; arr[i+2] = 5.0f; arr[i+3] = 5.0f; } } void func3() { __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); for(int i = 0; i < length; i += 4) { _mm_stream_ps(&arr[i], buf); } } void func4() { __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); for(int i = 0; i < length; i += 16) { _mm_stream_ps(&arr[i], buf); _mm_stream_ps(&arr[4], buf); _mm_stream_ps(&arr[8], buf); _mm_stream_ps(&arr[12], buf); } } int main() { length = CACHE_LINE * FACTOR * FACTOR; arr = malloc(length * sizeof(float)); tim("func1", func1); free(arr); arr = malloc(length * sizeof(float)); tim("func2", func2); free(arr); arr = malloc(length * sizeof(float)); tim("func3", func3); free(arr); arr = malloc(length * sizeof(float)); tim("func4", func4); free(arr); return 0; } 

La fonction 1 est l’approche naïve, la fonction 2 utilise le déroulement de la boucle. La fonction 3 utilise movntps, qui a en fait été insérée dans l’assemblage au moins lorsque j’ai vérifié -O0. Dans la fonction 4, j’ai essayé d’émettre plusieurs instructions movntps à la fois pour aider le processeur à combiner son écriture. J’ai compilé le code avec gcc -g -lrt -std=gnu99 -OX -msse4.1 test.cX est l’un de [0..3]. Les résultats sont .. intéressants à dire au mieux:

 -O0 func1 : 0.407794 s. func2 : 0.320891 s. func3 : 0.161100 s. func4 : 0.401755 s. -O1 func1 : 0.194339 s. func2 : 0.182536 s. func3 : 0.101712 s. func4 : 0.383367 s. -O2 func1 : 0.108488 s. func2 : 0.088826 s. func3 : 0.101377 s. func4 : 0.384106 s. -O3 func1 : 0.078406 s. func2 : 0.084927 s. func3 : 0.102301 s. func4 : 0.383366 s. 

Comme vous pouvez le constater, _mm_stream_ps est un peu plus rapide que les autres lorsque le programme n’est pas optimisé par gcc, mais qu’il échoue considérablement lorsque l’optimisation de gcc est activée. Valgrind rapporte encore de nombreuses erreurs d’écriture en cache.

Les questions sont donc les suivantes: pourquoi ces erreurs de cache (L1 + LL) se produisent-elles toujours, même si j’utilise les instructions de diffusion en continu NTA? Pourquoi surtout func4 est-il si lent?! Quelqu’un peut-il expliquer / spéculer sur ce qui se passe ici?

  1. Probablement, votre référence mesure principalement les performances d’allocation de mémoire, pas seulement les performances d’écriture. Votre système d’exploitation peut allouer des pages de mémoire non pas dans malloc , mais au premier contact, dans les func* . Le système d’exploitation peut également effectuer certains remaniements de mémoire après l’allocation d’une grande quantité de mémoire. Par conséquent, les tests de performance effectués juste après l’allocation de mémoire peuvent ne pas être fiables.
  2. Votre code a un problème d’ alias : le compilateur ne peut pas garantir que le pointeur de votre tableau ne change pas lors du processus de remplissage de ce tableau. Il doit donc toujours charger la valeur arr de la mémoire au lieu d’utiliser un registre. Cela pourrait entraîner une baisse des performances. Le moyen le plus simple d’éviter les alias est de copier arr et length en variables locales et d’utiliser uniquement des variables locales pour remplir le tableau. Il existe de nombreux conseils bien connus pour éviter les variables globales. L’aliasing est l’une des raisons.
  3. _mm_stream_ps fonctionne mieux si le tableau est aligné sur 64 octets. Dans votre code, aucun alignement n’est garanti (en fait, malloc aligne sur 16 octets). Cette optimisation n’est visible que pour les tableaux courts.
  4. C’est une bonne idée d’appeler _mm_mfence après avoir fini d’ _mm_stream_ps . Ceci est nécessaire pour l’exactitude, pas pour la performance.

Func4 ne devrait pas être ceci:

 void func4() { __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); for(int i = 0; i < length; i += 16) { _mm_stream_ps(&arr[i], buf); _mm_stream_ps(&arr[i+4], buf); _mm_stream_ps(&arr[i+8], buf); _mm_stream_ps(&arr[i+12], buf); } }