Наиболее эффективный способ хранения 4-точечных продуктов в смежном массиве в C с использованием SSE-свойств

Я оптимизирую код для микроархитектуры Intel x86 Nehalem, используя встроенные функции SSE.

Часть моей программы вычисляет 4 точечных продукта и добавляет каждый результат к предыдущим значениям в смежном фрагменте массива. Более конкретно,

tmp0 = _mm_dp_ps(A_0m, B_0m, 0xF1);
tmp1 = _mm_dp_ps(A_1m, B_0m, 0xF2);
tmp2 = _mm_dp_ps(A_2m, B_0m, 0xF4);
tmp3 = _mm_dp_ps(A_3m, B_0m, 0xF8);

tmp0 = _mm_add_ps(tmp0, tmp1);
tmp0 = _mm_add_ps(tmp0, tmp2);
tmp0 = _mm_add_ps(tmp0, tmp3);
tmp0 = _mm_add_ps(tmp0, C_0n);

_mm_storeu_ps(C_2, tmp0);

Обратите внимание, что я об этом говорю, используя 4 временных регистра xmm для хранения результата каждого точечного продукта. В каждом регистре xmm результат помещается в уникальные 32 бита относительно других временных регистров xmm, так что конечный результат выглядит следующим образом:

tmp0 = R0-zero-zero-zero

tmp1 = zero-R1-zero-zero

tmp2 = zero-zero-R2-zero

tmp3 = zero-zero-zero-R3

Я объединяю значения, содержащиеся в каждой переменной tmp, в одну переменную xmm, суммируя их со следующими инструкциями:

tmp0 = _mm_add_ps(tmp0, tmp1);
tmp0 = _mm_add_ps(tmp0, tmp2);
tmp0 = _mm_add_ps(tmp0, tmp3);

Наконец, я добавляю регистр, содержащий все 4 результата точечных произведений, в смежную часть массива, так что индексы массива увеличиваются на точечный продукт, так же (C_0n - это 4 значения, которые в настоящее время находятся в массиве, который равен для обновления, C_2 - адрес, указывающий на эти 4 значения):

tmp0 = _mm_add_ps(tmp0, C_0n);
_mm_storeu_ps(C_2, tmp0);

Я хочу знать, существует ли менее эффективный и эффективный способ получения результатов точечных продуктов и добавления их в смежный кусок массива. Таким образом, я делаю 3 дополнения между регистрами, в которых есть только 1 ненулевое значение. Кажется, должен быть более эффективный способ сделать это.

Я ценю всю помощь. Спасибо.

Ответ 1

Для такого кода мне нравится хранить "транспонирование" A и B, так что {A_0m.x, A_1m.x, A_2m.x, A_3m.x} сохраняются в одном векторе и т.д. Тогда вы можете делать точечный продукт, просто умножая и добавляя, и когда вы закончите, у вас есть все 4 точечных продукта в одном векторе без перетасовки.

Это часто используется при трассировке лучей, чтобы сразу протестировать 4 луча против плоскости (например, при обходе kd-дерева). Однако, если у вас нет контроля над входными данными, накладные расходы на выполнение транспонирования могут не стоить того. Код также будет работать на машинах с предварительным SSE4, хотя это может и не быть проблемой.


Небольшая заметка об эффективности существующего кода: вместо этого

tmp0 = _mm_add_ps(tmp0, tmp1);
tmp0 = _mm_add_ps(tmp0, tmp2);
tmp0 = _mm_add_ps(tmp0, tmp3);
tmp0 = _mm_add_ps(tmp0, C_0n);

Это может быть немного лучше сделать:

tmp0 = _mm_add_ps(tmp0, tmp1);  // 0 + 1 -> 0
tmp2 = _mm_add_ps(tmp2, tmp3);  // 2 + 3 -> 2
tmp0 = _mm_add_ps(tmp0, tmp2);  // 0 + 2 -> 0
tmp0 = _mm_add_ps(tmp0, C_0n);

Поскольку первые два mm_add_ps теперь полностью независимы. Кроме того, я не знаю относительные моменты добавления и перетасовки, но это может быть немного быстрее.


Надеюсь, что это поможет.

Ответ 2

Также можно использовать SSE3 hadd. Это оказалось быстрее, чем использование _dot_ps, в некоторых тривиальных тестах. Это возвращает 4 точечных продукта, которые могут быть добавлены.

static inline __m128 dot_p(const __m128 x, const __m128 y[4])
{
   __m128 z[4];

   z[0] = x * y[0];
   z[1] = x * y[1];
   z[2] = x * y[2];
   z[3] = x * y[3];
   z[0] = _mm_hadd_ps(z[0], z[1]);
   z[2] = _mm_hadd_ps(z[2], z[3]);
   z[0] = _mm_hadd_ps(z[0], z[2]);

   return z[0];
}

Ответ 3

Вы можете попытаться оставить результат точечного продукта в младшем слове и использовать скалярное хранилище op _mm_store_ss, чтобы сохранить этот поплавок из каждого регистра m128 в соответствующее местоположение массива. Буфер хранилища Nehalem должен накапливать последовательные записи в одной строке и сбрасывать их в L1 партиями.

Простой способ сделать это - подход celion transpose. MSVC _ MM_TRANSPOSE4_PS макрос сделает транспонирование для вас.

Ответ 4

Я понимаю, что этот вопрос старый, но зачем вообще использовать _mm_add_ps? Замените его на:

tmp0 = _mm_or_ps(tmp0, tmp1);
tmp2 = _mm_or_ps(tmp2, tmp3);
tmp0 = _mm_or_ps(tmp0, tmp2);

Вероятно, вы можете скрыть некоторую задержку _mm_dp_ps. Первый _mm_or_ps не ждет окончательных двухточечных продуктов, и это (быстрая) бит-мудрая операция. Наконец:

_mm_storeu_ps(C_2, _mm_add_ps(tmp0, C_0));