Оптимизация SSE SIMD для цикла

У меня есть код в цикле

for(int i = 0; i < n; i++)
{
  u[i] = c * u[i] + s * b[i];
}

Итак, u и b - векторы одинаковой длины, а c и s - скаляры. Подходит ли этот код для векторизации для использования с SSE для ускорения?

ОБНОВЛЕНИЕ

Я изучил векторизацию (оказывается, это не так уж сложно, если использовать встроенные функции) и реализовал свой цикл в SSE. Однако при установке флага SSE2 в компиляторе VC ++ я получаю примерно такую ​​же производительность, как и с моим собственным кодом SSE. С другой стороны, компилятор Intel был намного быстрее, чем мой код SSE или компилятор VC ++.

Вот код, который я написал для справки

double *u = (double*) _aligned_malloc(n * sizeof(double), 16);
for(int i = 0; i < n; i++)
{
   u[i] = 0;
}

int j = 0;
__m128d *uSSE = (__m128d*) u;
__m128d cStore = _mm_set1_pd(c);
__m128d sStore = _mm_set1_pd(s);
for (j = 0; j <= i - 2; j+=2)
{
  __m128d uStore = _mm_set_pd(u[j+1], u[j]);

  __m128d cu = _mm_mul_pd(cStore, uStore);
  __m128d so = _mm_mul_pd(sStore, omegaStore);

  uSSE[j/2] = _mm_add_pd(cu, so);
}
for(; j <= i; ++j)
{
  u[j] = c * u[j] + s * omegaCache[j];
}

person Projectile Fish    schedule 27.05.2010    source источник


Ответы (5)


Да, это отличный кандидат на векторизацию. Но перед этим убедитесь, что вы профилировали свой код, чтобы убедиться, что его действительно стоит оптимизировать. Тем не менее, векторизация будет выглядеть примерно так:

int i;
for(i = 0; i < n - 3; i += 4)
{
  load elements u[i,i+1,i+2,i+3]
  load elements b[i,i+1,i+2,i+3]
  vector multiply u * c
  vector multiply s * b
  add partial results
  store back to u[i,i+1,i+2,i+3]
}

// Finish up the uneven edge cases (or skip if you know n is a multiple of 4)
for( ; i < n; i++)
  u[i] = c * u[i] + s * b[i];

Для еще большей производительности вы можете рассмотреть возможность предварительной выборки дополнительных элементов массива и / или развертывания цикла и использования конвейерной обработки программного обеспечения, чтобы чередовать вычисления в одном цикле с обращениями к памяти из другой итерации.

person Adam Rosenfield    schedule 27.05.2010
comment
Определенно нашел этот код узким местом. Вопрос, чтобы убедиться, что мое обучение и внедрение векторизации - это не пустая трата усилий - компиляторы обычно не векторизуют такой код автоматически, верно? - person Projectile Fish; 27.05.2010
comment
@Projectile, если вы сообщите компилятору о псевдониме, как правило, так и будет. По моему собственному опыту, очень необычно генерировать лучший код, чем компилятор, без особых усилий. - person Anycorn; 27.05.2010

_mm_set_pd не векторизован. Если понимать буквально, он считывает два двойных числа с помощью скалярных операций, затем объединяет два скалярных двойника и копирует их в регистр SSE. Вместо этого используйте _mm_load_pd.

person rwong    schedule 30.06.2010

возможно, да, но вы должны помочь компилятору некоторыми подсказками. __restrict__, помещенный в указатели, сообщает компилятору, что между двумя указателями нет псевдонима. если вы знаете выравнивание ваших векторов, сообщите об этом компилятору (Visual C ++ может иметь некоторую возможность).

Я сам не знаком с Visual C ++, но слышал, что он не годится для векторизации. Вместо этого рассмотрите возможность использования компилятора Intel. Intel позволяет довольно детально управлять созданной сборкой: http://www.intel.com/software/products/compilers/docs/clin/main_cls/cref_cls/common/cppref_pragma_vector.htm

person Anycorn    schedule 27.05.2010
comment
кто знает процессор Intel лучше, чем они сами? :) - person YeenFei; 27.05.2010

Да, это отличный кандидат для векторизации, при условии, что нет перекрытия массивов U и B. Но код ограничен доступом к памяти (загрузка / сохранение). Векторизация помогает сократить количество циклов на цикл, но выполнение инструкций будет остановлено из-за промаха кеша в массивах U и B. Компилятор Intel C / C ++ генерирует следующий код с флагами по умолчанию для процессора Xeon x5500. Компилятор разворачивает цикл на 8 и использует инструкции SIMD ADD (addpd) и MULTIPLY (mulpd), используя регистры xmm [0-16] SIMD. В каждом цикле процессор может выдавать 2 инструкции SIMD, что дает 4-сторонний скалярный ILP, если у вас есть данные в регистрах.

Здесь U, B, C и S - двойная точность (8 байтов).

    ..B1.14:                        # Preds ..B1.12 ..B1.10
    movaps    %xmm1, %xmm3                                  #5.1
    unpcklpd  %xmm3, %xmm3                                  #5.1
    movaps    %xmm0, %xmm2                                  #6.12
    unpcklpd  %xmm2, %xmm2                                  #6.12
      # LOE rax rcx rbx rbp rsi rdi r8 r12 r13 r14 r15 xmm0 xmm1 xmm2 xmm3
    ..B1.15:     # Preds ..B1.15 ..B1.14
    movsd     (%rsi,%rcx,8), %xmm4                          #6.21
    movhpd    8(%rsi,%rcx,8), %xmm4                         #6.21
    mulpd     %xmm2, %xmm4                                  #6.21
    movaps    (%rdi,%rcx,8), %xmm5                          #6.12
    mulpd     %xmm3, %xmm5                                  #6.12
    addpd     %xmm4, %xmm5                                  #6.21
    movaps    16(%rdi,%rcx,8), %xmm7                        #6.12
    movaps    32(%rdi,%rcx,8), %xmm9                        #6.12
    movaps    48(%rdi,%rcx,8), %xmm11                       #6.12
    movaps    %xmm5, (%rdi,%rcx,8)                          #6.3
    mulpd     %xmm3, %xmm7                                  #6.12
    mulpd     %xmm3, %xmm9                                  #6.12
    mulpd     %xmm3, %xmm11                                 #6.12
    movsd     16(%rsi,%rcx,8), %xmm6                        #6.21
    movhpd    24(%rsi,%rcx,8), %xmm6                        #6.21
    mulpd     %xmm2, %xmm6                                  #6.21
    addpd     %xmm6, %xmm7                                  #6.21
    movaps    %xmm7, 16(%rdi,%rcx,8)                        #6.3
    movsd     32(%rsi,%rcx,8), %xmm8                        #6.21
    movhpd    40(%rsi,%rcx,8), %xmm8                        #6.21
    mulpd     %xmm2, %xmm8                                  #6.21
    addpd     %xmm8, %xmm9                                  #6.21
    movaps    %xmm9, 32(%rdi,%rcx,8)                        #6.3
    movsd     48(%rsi,%rcx,8), %xmm10                       #6.21
    movhpd    56(%rsi,%rcx,8), %xmm10                       #6.21
    mulpd     %xmm2, %xmm10                                 #6.21
    addpd     %xmm10, %xmm11                                #6.21
    movaps    %xmm11, 48(%rdi,%rcx,8)                       #6.3
    addq      $8, %rcx                                      #5.1
    cmpq      %r8, %rcx                                     #5.1
    jl        ..B1.15       # Prob 99%                      #5.1
person Pari Rajaram    schedule 20.12.2011

это зависит от того, как вы разместили u и b в памяти. если оба блока памяти находятся далеко друг от друга, SSE не будет сильно увеличиваться в этом сценарии.

Предлагается, чтобы массивы u и b были AOE (массив структур) вместо SOA (структура массива), потому что вы можете загрузить их оба в регистр в одной инструкции.

person YeenFei    schedule 27.05.2010
comment
Я не согласен с тем, что использование AOS здесь будет лучше, чем SOA. Вы по-прежнему делаете 2 загрузки для каждого магазина, а с AOS теперь вам нужно возвращать только 2 из каждых 4 единиц. С помощью SOA вы можете загрузить 4 модуля из u, 4 из b, а затем записать 4 обратно в u без необходимости выполнять перетасовку или маскирование. - person Adam Rosenfield; 27.05.2010
comment
Не согласен с ЙенФей по обоим пунктам. SOA обычно лучше вертикального SIMD. Расстояние памяти не является важным фактором из-за кэширования - даже если AOS разрешает использование меньшего количества строк кэша (например, из-за чрезмерного выравнивания заполнения, которое по своей сути маловероятно для хорошего кандидата для SIMDization), все равно было бы лучше реструктурировать эти строки кэша должны быть SOA. - person mabraham; 14.11.2012