Ответ 1
Это первый раз, когда я отвечаю на свой вопрос, но это кажется уместным. Основано на hirschhornsalz ответ на сумму префикса на 16 байт simd-prefix-sum-on-intel-cpu Я придумал решение для использования SIMD на первом проходе для 4, 8 и 16 32-битных слов.
Общая теория гласит следующее. Для последовательного сканирования слов n
требуется n
добавления (n-1 для сканирования n слов и еще одно добавление, перенесенное из предыдущего набора сканированных слов). Однако использование SIMD n слов может быть отсканировано в добавлениях log 2 (n) и равном числе сдвигов плюс еще одно добавление и широковещание для переноса из предыдущего SIMD-сканирования. Поэтому при некотором значении n
метод SIMD победит.
Посмотрите на 32-битные слова с SSE, AVX и AVX-512:
4 32-bit words (SSE): 2 shifts, 3 adds, 1 broadcast sequential: 4 adds
8 32-bit words (AVX): 3 shifts, 4 adds, 1 broadcast sequential: 8 adds
16 32 bit-words (AVX-512): 4 shifts, 5 adds, 1 broadcast sequential: 16 adds
Исходя из этого, SIMD не будет полезен для сканирования 32-разрядных слов до AVX-512. Это также предполагает, что сдвиги и широковещание могут выполняться только с одной инструкцией. Это справедливо для SSE, но не для AVX и, возможно, даже для AVX2.
В любом случае я собрал некоторый рабочий и проверенный код, который использует префиксную сумму, используя SSE.
inline __m128 scan_SSE(__m128 x) {
x = _mm_add_ps(x, _mm_castsi128_ps(_mm_slli_si128(_mm_castps_si128(x), 4)));
x = _mm_add_ps(x, _mm_castsi128_ps(_mm_slli_si128(_mm_castps_si128(x), 8)));
return x;
}
void prefix_sum_SSE(float *a, float *s, const int n) {
__m128 offset = _mm_setzero_ps();
for (int i = 0; i < n; i+=4) {
__m128 x = _mm_load_ps(&a[i]);
__m128 out = scan_SSE(x);
out = _mm_add_ps(out, offset);
_mm_store_ps(&s[i], out);
offset = _mm_shuffle_ps(out, out, _MM_SHUFFLE(3, 3, 3, 3));
}
Обратите внимание, что функция scan_SSE
имеет два дополнения (_mm_add_ps) и две смены (_mm_slli_si128). Броски используются только для того, чтобы сделать компилятор счастливым и не преобразовываться в инструкции. Затем внутри основного цикла над массивом в prefix_sum_SSE
используется другое добавление и один случайный перетасовка. Всего 6 операций по сравнению с 4 дополнениями с последовательной суммой.
Вот работающее решение для AVX:
inline __m256 scan_AVX(__m256 x) {
__m256 t0, t1;
//shift1_AVX + add
t0 = _mm256_permute_ps(x, _MM_SHUFFLE(2, 1, 0, 3));
t1 = _mm256_permute2f128_ps(t0, t0, 41);
x = _mm256_add_ps(x, _mm256_blend_ps(t0, t1, 0x11));
//shift2_AVX + add
t0 = _mm256_permute_ps(x, _MM_SHUFFLE(1, 0, 3, 2));
t1 = _mm256_permute2f128_ps(t0, t0, 41);
x = _mm256_add_ps(x, _mm256_blend_ps(t0, t1, 0x33));
//shift3_AVX + add
x = _mm256_add_ps(x,_mm256_permute2f128_ps(x, x, 41));
return x;
}
void prefix_sum_AVX(float *a, float *s, const int n) {
__m256 offset = _mm256_setzero_ps();
for (int i = 0; i < n; i += 8) {
__m256 x = _mm256_loadu_ps(&a[i]);
__m256 out = scan_AVX(x);
out = _mm256_add_ps(out, offset);
_mm256_storeu_ps(&s[i], out);
//broadcast last element
__m256 t0 = _mm256_permute2f128_ps(out, out, 0x11);
offset = _mm256_permute_ps(t0, 0xff);
}
}
Для трех сдвигов требуется 7 встроенных функций. Трансляция требует 2 встроенных функций. Таким образом, с 4 дополнениями, что 13 интрисики. Для AVX2 для сдвигов требуется всего 5 встроенных функций, поэтому всего 11 intrinsics total. Для последовательной суммы требуется только 8 дополнений. Поэтому, вероятно, ни AVX, ни AVX2 не будут полезны для первого прохода.
Edit:
Итак, я, наконец, сравнил это, и результаты неожиданны. Код SSE и AVX примерно в два раза быстрее, чем следующий последовательный код:
void scan(float a[], float s[], int n) {
float sum = 0;
for (int i = 0; i<n; i++) {
sum += a[i];
s[i] = sum;
}
}
Я предполагаю, что это связано с паралеллизмом уровня инструкций.
Так что я отвечаю на свой вопрос. Мне удалось использовать SIMD для pass1 в общем случае. Когда я совмещаю это с OpenMP на моей 4-жильной мостовой системе плюща, общая скорость составляет около семи для float 512k.