SSE SIMD Оптимизация для цикла
-
04-10-2019 - |
Вопрос
У меня есть код в петле
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];
}
Решение
Да, это отличный кандидат на векторизацию. Но, прежде чем сделать это, Убедитесь, что вы профилировали свой код Чтобы быть уверенным, что это на самом деле стоит оптимизировать. Тем не менее, векторизация пошла бы что-то вроде этого:
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];
Для еще более производительности вы можете рассмотреть возможность предварительной выборки дополнительных элементов массива и / или развернуть петлю и использование Программное обеспечение Чтобы переместить вычисление в одну петлю с доступом памяти из другой итерации.
Другие советы
Вероятно, да, но вы должны помочь компилятору с некоторыми советами.__restrict__
Помещенные на указателях говорит компилятор, что нет псевдонимов между двумя указателями. Если вы знаете выравнивание ваших векторов, сообщите, что для компилятора (Visual C ++ может иметь некоторые объекты).
Я не знаком с Visual C ++ сам, но я слышал, что это не полезно для векторизации. Попробуйте использовать компилятор Intel вместо этого. Intel позволяет довольно мелкозернированный контроль над сборкой сборки: http://www.intel.com/software/products/compilers/docs/clin/main_cls/cref_cls/common/cpp_pragma_vector.htm.
_mm_set_pd
не верится. Если принято буквально, он читает два двойных с использованием скалярных операций, затем сочетает в себе два скалярных удваивания и скопируют их в регистр SSE. Использовать _mm_load_pd
вместо.
Да, это отличный кандидат на векторные кандидаты, предполагая, что нет перекрытия A и B Array. Но код связан с доступом памяти (Load / Store). Векторная видимость помогает уменьшить циклы на петлю, но инструкции будут стойвлять из-за пропускания кэша в массиве U и B. Компилятор Intel C / C ++ генерирует следующий код с флагами по умолчанию для процессора Xeon X5500. Компилятор разворачивает петлю на 8 и использует SIMD Add (AddPD) и Multipy (MULPD), используя XMM [0-16] CIMD-регистры. В каждом цикле процессор может выпускать 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
Это зависит от того, как вы разместили U и B в память. Если оба блока памяти далеко друг от друга, SSE не будет увеличиваться в этом сценарии.
Предполагается, что массив U и B представляют собой AoE (массив структуры) вместо SOA (структура массива), поскольку вы можете загрузить их обоих в регистр в одной инструкции.