Как рассчитать произведение векторной точки с помощью встроенных функций SSE в C - PullRequest
10 голосов
/ 08 ноября 2010

Я пытаюсь умножить два вектора вместе, где каждый элемент одного вектора умножается на элемент в том же индексе в другом векторе.Затем я хочу суммировать все элементы результирующего вектора, чтобы получить одно число.Например, расчет будет выглядеть так для векторов {1,2,3,4} и {5,6,7,8}:

1 * 5 + 2 * 6 + 3 * 7 +4 * 8

По сути, я беру скалярное произведение двух векторов.Я знаю, что для этого есть команда SSE, но у этой команды нет встроенной функции, связанной с ней.На данный момент я не хочу писать встроенную сборку в моем C-коде, поэтому я хочу использовать только встроенные функции.Это похоже на обычный расчет, поэтому я сам удивлен, что не смог найти ответ в Google.

Примечание: я оптимизирую под конкретную микроархитектуру, которая поддерживает до SSE 4.2.

Спасибо за вашу помощь.

Ответы [ 4 ]

18 голосов
/ 08 ноября 2010

Если вы создаете скалярное произведение длинных векторов, используйте умножение и регулярное значение _mm_add_ps (или FMA) во внутреннем цикле. Сохраните горизонтальную сумму до конца.


Но если вы делаете точечное произведение только одной пары SIMD-векторов:

GCC (по крайней мере версия 4.3) включает <smmintrin.h> со встроенными характеристиками уровня SSE4.1, включая продукты с одинарной и двойной точностью:

_mm_dp_ps (__m128 __X, __m128 __Y, const int __M);
_mm_dp_pd (__m128d __X, __m128d __Y, const int __M);

В основных процессорах Intel (не Atom / Silvermont) они выполняются несколько быстрее, чем при ручном выполнении нескольких инструкций.

Но на AMD (включая Ryzen) dpps значительно медленнее. (См. Таблицы инструкций Агнера Фога )


В качестве запасного варианта для более старых процессоров вы можете использовать этот алгоритм для создания точечного произведения векторов a и b:

__m128 r1 = _mm_mul_ps(a, b);

и затем горизонтальная сумма r1 с использованием Самый быстрый способ сделать горизонтальную векторную сумму с плавающей запятой на x86 (см. Там прокомментированную версию и почему она быстрее.)

__m128 shuf   = _mm_shuffle_ps(r1, r1, _MM_SHUFFLE(2, 3, 0, 1));
__m128 sums   = _mm_add_ps(r1, shuf);
shuf          = _mm_movehl_ps(shuf, sums);
sums          = _mm_add_ss(sums, shuf);
float result =  _mm_cvtss_f32(sums);

Медленная альтернатива стоит 2 тасов за hadd, что легко ограничивает пропускную способность тасования, особенно на процессорах Intel.

r2 = _mm_hadd_ps(r1, r1);
r3 = _mm_hadd_ps(r2, r2);
_mm_store_ss(&result, r3);
5 голосов
/ 21 марта 2017

Я бы сказал, что самый быстрый метод SSE будет:

static inline float CalcDotProductSse(__m128 x, __m128 y) {
    __m128 mulRes, shufReg, sumsReg;
    mulRes = _mm_mul_ps(x, y);

    // Calculates the sum of SSE Register - https://stackoverflow.com/a/35270026/195787
    shufReg = _mm_movehdup_ps(mulRes);        // Broadcast elements 3,1 to 2,0
    sumsReg = _mm_add_ps(mulRes, shufReg);
    shufReg = _mm_movehl_ps(shufReg, sumsReg); // High Half -> Low Half
    sumsReg = _mm_add_ss(sumsReg, shufReg);
    return  _mm_cvtss_f32(sumsReg); // Result in the lower part of the SSE Register
}

Я следовал - Самый быстрый способ сделать горизонтальную векторную сумму с плавающей запятой на x86 .

3 голосов
/ 08 ноября 2010

Есть статья Intel здесь , которая касается реализации точечного продукта.

3 голосов
/ 08 ноября 2010

Я написал это и скомпилировал это с gcc -O3 -S -ftree-vectorize -ftree-vectorizer-verbose=2 sse.c

void f(int * __restrict__ a, int * __restrict__ b, int * __restrict__ c, int * __restrict__ d,
       int * __restrict__ e, int * __restrict__ f, int * __restrict__ g, int * __restrict__ h,
       int * __restrict__ o)
{
    int i;

    for (i = 0; i < 8; ++i)
        o[i] = a[i]*e[i] + b[i]*f[i] + c[i]*g[i] + d[i]*h[i];
}

И GCC 4.3.0 автоматически векторизовал его:

sse.c:5: note: LOOP VECTORIZED.
sse.c:2: note: vectorized 1 loops in function.

Однако, он сделал бы это только если яиспользовал цикл с достаточным количеством итераций - в противном случае подробный вывод прояснил бы, что векторизация невыгодна или цикл слишком мал.Без ключевых слов __restrict__ он должен генерировать отдельные не векторизованные версии, чтобы иметь дело со случаями, когда выходные данные o могут указывать на один из входных данных.

Я вставил бы инструкции в качестве примера, нопоскольку часть векторизации развернула цикл, он не очень читабелен.

...