Наиболее эффективный способ хранения 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-ноль-ноль-ноль

Tmp1= ноль-R1-ноль-ноль

Tmp2= ноль-ноль-R2-ноль

Tmp3= ноль-ноль-ноль-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 ненулевое значение в них. Кажется, должен быть более эффективный способ сделать это.

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

4 11

4 ответа:

Для такого кода мне нравится хранить "транспонирование" 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 теперь полностью независимы. Кроме того, я не знаю относительного времени добавления и перетасовки, но это может быть немного быстрее.


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

Также можно использовать 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];
}

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

Лучший способ сделать это-транспонировать подход celion. Макрос MSVC _MM_TRANSPOSE4_PS сделает транспонирование за вас.

Я понимаю, что этот вопрос стар, но зачем вообще использовать _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 также не ждет конечных 2-точечных продуктов, и это (быстрая) битовая операция. Наконец:

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