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