Optymalizacja kodu GCC SSE

Ten post jest ściśle związany z innym, który napisałem kilka dni temu . Tym razem napisałem prosty kod, który po prostu dodaje parę tablic elementów, mnoży wynik przez wartości w innej tablicy i przechowuje go w czwartej tablicy, wszystkie zmienne zmiennoprzecinkowe z podwójną precyzją wpisane.

Zrobiłem dwie wersje tego kodu: jedną z instrukcjami SSE, używając wywołań do, a drugą bez nich skompilowałem je z poziomem optymalizacji gcc i-O0. Piszę je poniżej:

// 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];
    }   
  }
}

Podczas kompilacji z-O0, gcc używa rejestrów XMM / MMX i struktur SSE, jeśli nie podano opcji-mno-sse (i innych). Sprawdzałem kod assembly wygenerowany dla drugiego kodu i zauważyłem, że korzysta on z movsd, instrukcje addsd i mulsd . Korzysta więc z instrukcji SSE, ale tylko z tych, które używają najniższej części rejestrów, jeśli się nie mylę. Kod assembly wygenerowany dla pierwszego C kod wykorzystywał, zgodnie z oczekiwaniami, instrukcje addp i mulpd, chociaż wygenerowano dość większy kod złożenia.

W każdym razie, pierwszy kod powinien uzyskać lepszy zysk, o ile wiem, paradygmatu SIMD, ponieważ każda iteracja oblicza dwie wartości wyniku. Mimo to drugi kod wykonuje coś takiego jak 25% szybciej niż pierwszy. Zrobiłem również test z pojedynczymi wartościami dokładności i uzyskałem podobne wyniki. Jaki jest tego powód?

Author: Community, 2011-10-27

2 answers

Wektoryzacja w GCC jest włączona w -O3. Dlatego w -O0 widzisz tylko zwykłe skalarne instrukcje SSE2(movsd, addsd, itp.). Używając GCC 4.6.1 i Twojego drugiego przykładu:

#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;
}

I kompilowanie z gcc -S -O3 -msse2 sse.c tworzy dla wewnętrznej pętli następujące instrukcje, co jest całkiem dobre:

.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

Jak widać, z włączoną wektoryzacją GCC emituje kod do wykonywania dwóch iteracji pętli równolegle. Można go jednak poprawić - kod ten wykorzystuje niższe 128 bitów rejestrów SSE, ale może korzystać z pełnych 256-bitowych rejestrów YMM, umożliwiając kodowanie instrukcji SSE AVX (jeśli są dostępne na maszynie). Tak więc kompilacja tego samego programu z gcc -S -O3 -msse2 -mavx sse.c daje dla pętli wewnętrznej:

.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

Zauważ, że v przed każdą instrukcją i że instrukcje używają 256-bitowych rejestrów YMM, cztery iteracje oryginalnej pętli są wykonywane równolegle.

 14
Author: chill,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-23 14:13:57

Chciałbym rozszerzyć odpowiedź Chilla i zwrócić uwagę na fakt, że GCC wydaje się nie być w stanie zrobić tego samego inteligentnego użycia instrukcji AVX podczas iteracji wstecz.

Po prostu zamień wewnętrzną pętlę w przykładowym kodzie chill ' a na:

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

GCC (4.8.4) z opcjami -S -O3 -mavx daje:

.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
Author: Luca Citi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:46:47