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?
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.
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
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