Наиболее эффективный способ хранения четырехточечных продуктов в непрерывном массиве в C с использованием встроенных функций SSE - PullRequest
11 голосов
/ 13 ноября 2010

Я оптимизирую некоторый код для микроархитектуры 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 ]

6 голосов
/ 13 ноября 2010

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


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

3 голосов
/ 17 декабря 2010

Также можно использовать хад SSE3.В некоторых тривиальных тестах это оказалось быстрее, чем при использовании _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];
}
1 голос
/ 22 декабря 2012

Я понимаю, что этот вопрос старый, но зачем вообще использовать _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));
1 голос
/ 13 ноября 2010

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

Профессиональный способ сделать это - транспонированный подход Селиона.Макрос _MM_TRANSPOSE4_PS MSVC сделает транспонирование за вас.

...