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


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

это часто используется в raytracing, чтобы проверить 4 луча сразу против плоскости (например, при пересечении KD-дерева). Если у вас нет контроля над входными данными, хотя, накладные расходы на транспонирование могут не стоить того. Код также будет выполняться на машинах pre-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 партиями.

профессиональный способ сделать это-транспонировать подход селиона. Индекса 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 точек, и это (быстрая) битовая операция. Наконец:--6-->

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