Question Optimisation du code SCC GCC


Ce post est étroitement lié à un autre que j'ai posté il y a quelques jours. Cette fois-ci, j'ai écrit un code simple qui ajoute simplement une paire de tableaux d'éléments, multiplie le résultat par les valeurs d'un autre tableau et le stocke dans un quatrième tableau, toutes les variables étant composées en double précision.

J'ai fait deux versions de ce code: l'une avec les instructions SSE, utilisant des appels vers et l'autre sans elles. Je les ai ensuite compilées avec le niveau d'optimisation gcc et -O0. Je les écris ci-dessous:

// SSE VERSION

#define N 10000
#define NTIMES 100000
#include <time.h>
#include <stdio.h>
#include <xmmintrin.h>
#include <pmmintrin.h>

double a[N] __attribute__((aligned(16)));
double b[N] __attribute__((aligned(16)));
double c[N] __attribute__((aligned(16)));
double r[N] __attribute__((aligned(16)));

int main(void){
  int i, times;
  for( times = 0; times < NTIMES; times++ ){
     for( i = 0; i <N; i+= 2){ 
        __m128d mm_a = _mm_load_pd( &a[i] );  
        _mm_prefetch( &a[i+4], _MM_HINT_T0 );
        __m128d mm_b = _mm_load_pd( &b[i] );  
        _mm_prefetch( &b[i+4] , _MM_HINT_T0 );
        __m128d mm_c = _mm_load_pd( &c[i] );
        _mm_prefetch( &c[i+4] , _MM_HINT_T0 );
        __m128d mm_r;
        mm_r = _mm_add_pd( mm_a, mm_b );
        mm_a = _mm_mul_pd( mm_r , mm_c );
        _mm_store_pd( &r[i], mm_a );
      }   
   }
 }

//NO SSE VERSION
//same definitions as before
int main(void){
  int i, times;
   for( times = 0; times < NTIMES; times++ ){
     for( i = 0; i < N; i++ ){
      r[i] = (a[i]+b[i])*c[i];
    }   
  }
}

Lorsque vous les compilez avec -O0, gcc utilise les registres XMM / MMX et les instructions SSE, si elles ne sont pas spécifiquement définies avec les options -mno-sse (et autres). J'ai inspecté le code d'assemblage généré pour le deuxième code et j'ai remarqué qu'il utilisait movsd, ajoute et mulsd instructions. Donc, il utilise les instructions SSE, mais seulement celles qui utilisent la partie la plus basse des registres, si je ne me trompe pas. Le code assembleur généré pour le premier code C utilise, comme prévu, le addp et mulpd instructions, bien qu'un code d'assemblage assez important ait été généré.

Quoi qu'il en soit, le premier code devrait, à ma connaissance, être plus rentable que le paradigme SIMD, puisque chaque itération calcule deux valeurs de résultat. Pourtant, le deuxième code effectue quelque chose comme 25% plus rapide que le premier. J'ai également fait un test avec des valeurs de précision uniques et obtenu des résultats similaires. Quelle est la raison de cela?


14
2017-10-27 16:38


origine


Réponses:


La vectorisation dans GCC est activée à -O3. C'est pourquoi chez -O0, vous ne voyez que les instructions scalaires SSE2 ordinaires (movsd, addsd, etc). En utilisant GCC 4.6.1 et votre deuxième exemple:

#define N 10000
#define NTIMES 100000

double a[N] __attribute__ ((aligned (16)));
double b[N] __attribute__ ((aligned (16)));
double c[N] __attribute__ ((aligned (16)));
double r[N] __attribute__ ((aligned (16)));

int
main (void)
{
  int i, times;
  for (times = 0; times < NTIMES; times++)
    {
      for (i = 0; i < N; ++i)
        r[i] = (a[i] + b[i]) * c[i];
    }

  return 0;
}

et compiler avec gcc -S -O3 -msse2 sse.c produit pour la boucle interne les instructions suivantes, ce qui est plutôt bien:

.L3:
    movapd  a(%eax), %xmm0
    addpd   b(%eax), %xmm0
    mulpd   c(%eax), %xmm0
    movapd  %xmm0, r(%eax)
    addl    $16, %eax
    cmpl    $80000, %eax
    jne .L3

Comme vous pouvez le voir, avec la vectorisation activée, GCC émet du code pour effectuer deux itérations en boucle en parallèle. Cela peut être amélioré, cependant - ce code utilise les 128 bits inférieurs des registres SSE, mais il peut utiliser tous les registres YMM 256 bits, en activant le codage AVX des instructions SSE (si elles sont disponibles sur la machine). Donc, compiler le même programme avec gcc -S -O3 -msse2 -mavx sse.c donne pour la boucle interne:

.L3:
    vmovapd a(%eax), %ymm0
    vaddpd  b(%eax), %ymm0, %ymm0
    vmulpd  c(%eax), %ymm0, %ymm0
    vmovapd %ymm0, r(%eax)
    addl    $32, %eax
    cmpl    $80000, %eax
    jne .L3

Notez que v devant chaque instruction et que les instructions utilisent les registres YMM 256 bits, quatre les itérations de la boucle d'origine sont exécutées en parallèle.


14
2017-11-10 13:18



Je voudrais prolonger réponse de froid et attirer votre attention sur le fait que GCC ne semble pas être en mesure de faire la même utilisation intelligente des instructions AVX lors d’une itération en arrière.

Il suffit de remplacer la boucle interne dans l'exemple de code de chill par:

for (i = N-1; i >= 0; --i)
    r[i] = (a[i] + b[i]) * c[i];

GCC (4.8.4) avec options -S -O3 -mavx produit:

.L5:
    vmovsd  a+79992(%rax), %xmm0
    subq    $8, %rax
    vaddsd  b+80000(%rax), %xmm0, %xmm0
    vmulsd  c+80000(%rax), %xmm0, %xmm0
    vmovsd  %xmm0, r+80000(%rax)
    cmpq    $-80000, %rax
    jne     .L5

2
2018-04-21 14:42