Как рассчитать одно-векторное точечное произведение с использованием встроенных функций SSE в C

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

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

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

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


person Sam    schedule 08.11.2010    source источник


Ответы (4)


Если вы выполняете скалярное произведение более длинных векторов, используйте умножение и регулярный _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 значительно медленнее. (См. таблицы инструкций Agner Fog)


В качестве запасного варианта для старых процессоров вы можете использовать этот алгоритм для создания скалярного произведения векторов 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);
person caf    schedule 08.11.2010
comment
В качестве примечания я хотел бы отметить, что вычисление Dot-произведения с использованием встроенной функции dp происходит медленнее, чем при втором способе. - person Serguei Fedorov; 30.10.2013
comment
@SergueiFedorov, который полностью зависит от вашего оборудования, нет глобального случая, чтобы он был медленнее. - person RamblingMad; 25.04.2014
comment
Я думаю, что есть лучшие способы для горизонтального суммирования, чем использование _mm_hadd_ps. См. stackoverflow.com/a/35270026/195787. - person Royi; 21.03.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.

person Royi    schedule 21.03.2017
comment
Отличная находка, хадд-инструкции расширяются до нескольких мопов. - person caf; 21.03.2017

Я написал это и скомпилировал с 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 могут указывать на один из входов.

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

person Ben Jackson    schedule 08.11.2010
comment
Я думаю, он имел в виду другое. Как 2 массива по 4 элемента в каждом. Здесь вы делаете что-то другое. Что-то вроде скалярного произведения массива векторов. - person Royi; 21.03.2017

здесь есть статья Intel, которая касается реализаций точечных продуктов. .

person DennyRolling    schedule 08.11.2010