Векторизация вычисления количества точек с использованием SSE4

Я пытаюсь улучшить этот код с помощью продукта SSE4 dot, но мне сложно найти решение. Эта функция получает параметры qi и tj, которые содержат массивы с плавающей запятой по 80 ячеек каждый, а затем вычисляют точечный продукт. Возвращаемое значение представляет собой вектор с четырьмя точечными продуктами. Поэтому я пытаюсь вычислить четыре точечных произведения из двадцати значений параллельно.

Вы знаете, как улучшить этот код?

inline __m128 ScalarProd20Vec(__m128* qi, __m128* tj)
{
    __m128 res=_mm_add_ps(_mm_mul_ps(tj[0],qi[0]),_mm_mul_ps(tj[1],qi[1]));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[2],qi[2]),_mm_mul_ps(tj[3],qi[3])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[4],qi[4]),_mm_mul_ps(tj[5],qi[5])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[6],qi[6]),_mm_mul_ps(tj[7],qi[7])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[8],qi[8]),_mm_mul_ps(tj[9],qi[9])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[10],qi[10]),_mm_mul_ps(tj[11],qi[11])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[12],qi[12]),_mm_mul_ps(tj[13],qi[13])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[14],qi[14]),_mm_mul_ps(tj[15],qi[15])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[16],qi[16]),_mm_mul_ps(tj[17],qi[17])));
    res=_mm_add_ps(res,_mm_add_ps(_mm_mul_ps(tj[18],qi[18]),_mm_mul_ps(tj[19],qi[19])));
    return res;
}

Ответ 1

Из сотен примеров SSE, которые я видел на SO, ваш код является одним из немногих, которые уже в очень хорошей форме с самого начала. Вам не нужна инструкция dot-product SSE4. (Вы можете сделать лучше!)

Однако есть одна вещь, которую вы можете попробовать: (я говорю, попробуйте, потому что я еще не приурочен).

В настоящее время у вас есть цепочка зависимостей данных на res. В настоящее время векторное добавление составляет 3-4 цикла на большинстве машин. Таким образом, ваш код будет занимать не менее 30 циклов, чем вы:

(10 additions on critical path) * (3 cycles addps latency) = 30 cycles

Что вы можете сделать, это node -расщепить переменную res следующим образом:

__m128 res0 = _mm_add_ps(_mm_mul_ps(tj[ 0],qi[ 0]),_mm_mul_ps(tj[ 1],qi[ 1]));
__m128 res1 = _mm_add_ps(_mm_mul_ps(tj[ 2],qi[ 2]),_mm_mul_ps(tj[ 3],qi[ 3]));

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[ 4],qi[ 4]),_mm_mul_ps(tj[ 5],qi[ 5]))); 
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[ 6],qi[ 6]),_mm_mul_ps(tj[ 7],qi[ 7])));

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[ 8],qi[ 8]),_mm_mul_ps(tj[ 9],qi[ 9])));
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[10],qi[10]),_mm_mul_ps(tj[11],qi[11])));

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[12],qi[12]),_mm_mul_ps(tj[13],qi[13])));
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[14],qi[14]),_mm_mul_ps(tj[15],qi[15])));

res0 = _mm_add_ps(res0,_mm_add_ps(_mm_mul_ps(tj[16],qi[16]),_mm_mul_ps(tj[17],qi[17])));
res1 = _mm_add_ps(res1,_mm_add_ps(_mm_mul_ps(tj[18],qi[18]),_mm_mul_ps(tj[19],qi[19])));

return _mm_add_ps(res0,res1);

Это почти сокращает ваш критический путь пополам. Обратите внимание, что из-за неассоциативности с плавающей запятой эта оптимизация является незаконной для компиляторов.


Здесь представлена ​​альтернативная версия, использующая 4-way node -splitting и инструкции AMD FMA4. Если вы не можете использовать добавленные с плавным умножением, не стесняйтесь их разделить. Это может быть лучше, чем первая версия выше.

__m128 res0 = _mm_mul_ps(tj[ 0],qi[ 0]);
__m128 res1 = _mm_mul_ps(tj[ 1],qi[ 1]);
__m128 res2 = _mm_mul_ps(tj[ 2],qi[ 2]);
__m128 res3 = _mm_mul_ps(tj[ 3],qi[ 3]);

res0 = _mm_macc_ps(tj[ 4],qi[ 4],res0);
res1 = _mm_macc_ps(tj[ 5],qi[ 5],res1);
res2 = _mm_macc_ps(tj[ 6],qi[ 6],res2);
res3 = _mm_macc_ps(tj[ 7],qi[ 7],res3);

res0 = _mm_macc_ps(tj[ 8],qi[ 8],res0);
res1 = _mm_macc_ps(tj[ 9],qi[ 9],res1);
res2 = _mm_macc_ps(tj[10],qi[10],res2);
res3 = _mm_macc_ps(tj[11],qi[11],res3);

res0 = _mm_macc_ps(tj[12],qi[12],res0);
res1 = _mm_macc_ps(tj[13],qi[13],res1);
res2 = _mm_macc_ps(tj[14],qi[14],res2);
res3 = _mm_macc_ps(tj[15],qi[15],res3);

res0 = _mm_macc_ps(tj[16],qi[16],res0);
res1 = _mm_macc_ps(tj[17],qi[17],res1);
res2 = _mm_macc_ps(tj[18],qi[18],res2);
res3 = _mm_macc_ps(tj[19],qi[19],res3);

res0 = _mm_add_ps(res0,res1);
res2 = _mm_add_ps(res2,res3);

return _mm_add_ps(res0,res2);

Ответ 2

Во-первых, самая важная оптимизация, которую вы можете сделать, - убедиться, что ваш компилятор включил все свои настройки оптимизации.


Компиляторы довольно умны, поэтому, если записать его как цикл, он, скорее всего, разворачивает его:

__128 res = _mm_setzero();
for (int i = 0; i < 10; i++) {
  res = _mm_add_ps(res, _mm_add_ps(_mm_mul_ps(tj[2*i], qi[2*i]), _mm_mul_ps(tj[2*i+1], qi[2*i+1])));
}
return res;

(С GCC вам нужно пройти -funroll-loops, а затем он разворачивает его, чтобы выполнить 5 итераций за раз.)

Вы также можете определить макрос и развернуть его вручную, если версия цикла медленнее, например:

__128 res = _mm_setzero();

#define STEP(i) res = _mm_add_ps(res, _mm_add_ps(_mm_mul_ps(tj[2*i], qi[2*i]), _mm_mul_ps(tj[2*i+1], qi[2*i+1])))

STEP(0); STEP(1); STEP(2); STEP(3); STEP(4);
STEP(5); STEP(6); STEP(7); STEP(8); STEP(9);

#undef STEP

return res;

Вы можете даже запустить цикл от 0 до 20 (или сделать то же самое с версией макроса), то есть:

__128 res = _mm_setzero();
for (int i = 0; i < 20; i++) {
  res = _mm_add_ps(res, _mm_mul_ps(tj[i], qi[i]));
}
return res;

(С GCC и -funroll-loops это разворачивается, чтобы выполнить 10 итераций за раз, то же самое, что и цикл "два-в-время" выше.)

Ответ 3

Ваши данные не располагаются в памяти в подходящем формате для специализированных инструкций продукта SSE4 dot (dpps). Эти инструкции ожидают, что размеры одного вектора будут смежными, например:

| dim0 | dim1 | dim2 | ... | dim19 |

тогда как ваши данные, как представляется, чередуются между собой:

| v0-dim0 | v1-dim0 | v2-dim0 | v3-dim0 | v0-dim1 | ...

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