GCC SSE оптимизация кода
Этот пост тесно связан с другим, который я опубликовал несколько дней назад. На этот раз я написал простой код, который просто добавляет пару массивов элементов, умножает результат на значения в другом массиве и сохраняет его в четвертом массиве, все переменные с плавающей точкой с двойной точностью набираются.
Я сделал две версии этого кода: одну с инструкциями SSE, используя вызовы, а другую без них, а затем скомпилировал их с уровнем оптимизации gcc и -O0. Я пишу их ниже:
// 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];
}
}
}
При компиляции с помощью -O0 gcc использует регистры XMM/MMX и инструкции SSE, если специально не указаны параметры -mno-sse (и другие). Я проверил код сборки, сгенерированный для второго кода, и заметил, что он использует инструкции movsd, addd и mulsd. Так что он использует инструкции SSE, но только те, которые используют самую низкую часть регистров, если я не ошибаюсь. Код сборки, сгенерированный для первого кода C, использовал, как и ожидалось, инструкции addp и mulpd, хотя был сгенерирован довольно большой код сборки.
Во всяком случае, первый код должен, насколько мне известно, получать большую прибыль от парадигмы SIMD, поскольку на каждой итерации вычисляются два результирующих значения. Тем не менее, второй код выполняет что-то вроде 25 процентов быстрее, чем первый. Я также сделал тест с одинарной точностью и получил аналогичные результаты. В чем причина?
2 ответа
Векторизация в GCC включена в -O3
, Вот почему в -O0
видишь только обычные скалярные инструкции SSE2 (movsd
, addsd
, так далее). Используя GCC 4.6.1 и ваш второй пример:
#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;
}
и составление с gcc -S -O3 -msse2 sse.c
производит для внутреннего цикла следующие инструкции, что довольно хорошо:
.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
Как видите, с включенной векторизацией GCC испускает код для параллельного выполнения двух итераций цикла. Однако его можно улучшить - этот код использует младшие 128 битов регистров SSE, но он может использовать полные 256-битные регистры YMM, включив AVX-кодирование инструкций SSE (если доступно на машине). Итак, компиляция той же программы с gcc -S -O3 -msse2 -mavx sse.c
дает для внутреннего цикла:
.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
Обратите внимание, что v
перед каждой инструкцией и тем, что инструкции используют 256-битные регистры YMM, четыре итерации исходного цикла выполняются параллельно.
Я хотел бы расширить ответ Чилла и обратить ваше внимание на тот факт, что GCC, по-видимому, не в состоянии сделать то же самое умное использование инструкций AVX при итерации в обратном направлении.
Просто замените внутренний цикл в примере кода chill на:
for (i = N-1; i >= 0; --i)
r[i] = (a[i] + b[i]) * c[i];
GCC (4.8.4) с опциями -S -O3 -mavx
производит:
.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