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
Другие вопросы по тегам