SIMD, подписанный с неподписанным умножением для 64-разрядных * 64-бит до 128 бит

Я создал функцию, которая выполняет 64-битную * 64-битную до 128-битной, используя SIMD. В настоящее время я реализовал его с использованием SSE2 (acutally SSE4.1). Это означает, что он одновременно выполняет два 64b * 64b до 128b. Та же идея может быть распространена на AVX2 или AVX512, предоставляя четыре или восемь 64b * 64 до 128b продуктов одновременно. Я основывал свой алгоритм на http://www.hackersdelight.org/hdcodetxt/muldws.c.txt

Этот алгоритм выполняет одно беззнаковое умножение, одно подписанное умножение и два подписанных * неподписанных умножения. Подписанные * подписанные и неподписанные * неподписанные операции просты в использовании с помощью _mm_mul_epi32 и _mm_mul_epu32. Но смешанные подписанные и неподписанные продукты вызвали у меня проблемы. Рассмотрим, например.

int32_t x = 0x80000000;
uint32_t y = 0x7fffffff;
int64_t z = (int64_t)x*y;

Произведение двойного слова должно быть 0xc000000080000000. Но как вы можете получить это, если предположите, что ваш компилятор знает, как обрабатывать смешанные типы? Вот что я придумал:

int64_t sign = x<0; sign*=-1;        //get the sign and make it all ones
uint32_t t = abs(x);                 //if x<0 take two complement again
uint64_t prod = (uint64_t)t*y;       //unsigned product
int64_t z = (prod ^ sign) - sign;    //take two complement based on the sign

Используя SSE, это можно сделать следующим образом:

__m128i xh;    //(xl2, xh2, xl1, xh1) high is signed, low unsigned
__m128i yl;    //(yh2, yl2, yh2, yl2)
__m128i xs     = _mm_cmpgt_epi32(_mm_setzero_si128(), xh); // get sign
        xs     = _mm_shuffle_epi32(xs, 0xA0);              // extend sign
__m128i t      = _mm_sign_epi32(xh,xh);                    // abs(xh)
__m128i prod   = _mm_mul_epu32(t, yl);                     // unsigned (xh2*yl2,xh1*yl1)
__m128i inv    = _mm_xor_si128(prod,xs);                   // invert bits if negative
__m128i z      = _mm_sub_epi64(inv,xs);                    // add 1 if negative

Это дает правильный результат. Но я должен сделать это дважды (один раз при возведении в квадрат), и теперь это значительная часть моей функции. Есть ли более эффективный способ сделать это с SSE4.2, AVX2 (четыре 128-битных продукта) или даже с AVX512 (восемь 128-битных продуктов)?

Возможно, есть более эффективные способы сделать это, чем с SIMD? Это много расчетов, чтобы получить верхнее слово.

Изменить: на основе комментария @ElderBug похоже, что способ сделать это не с SIMD, а с инструкцией mul. Для чего это стоит, если кто-то хочет увидеть, насколько это сложно, вот полная рабочая функция (я только что заработал, поэтому я ее не оптимизировал, но я не думаю, что это того стоит).

void muldws1_sse(__m128i x, __m128i y, __m128i *lo, __m128i *hi) {
    __m128i lomask = _mm_set1_epi64x(0xffffffff);

    __m128i xh     = _mm_shuffle_epi32(x, 0xB1);    // x0l, x0h, x1l, x1h
    __m128i yh     = _mm_shuffle_epi32(y, 0xB1);    // y0l, y0h, y1l, y1h

    __m128i xs     = _mm_cmpgt_epi32(_mm_setzero_si128(), xh);
    __m128i ys     = _mm_cmpgt_epi32(_mm_setzero_si128(), yh);
            xs     = _mm_shuffle_epi32(xs, 0xA0);
            ys     = _mm_shuffle_epi32(ys, 0xA0);

    __m128i w0     = _mm_mul_epu32(x,  y);          // x0l*y0l, y0l*y0h
    __m128i w3     = _mm_mul_epi32(xh, yh);         // x0h*y0h, x1h*y1h
            xh     = _mm_sign_epi32(xh,xh);
            yh     = _mm_sign_epi32(yh,yh);

    __m128i w1     = _mm_mul_epu32(x,  yh);         // x0l*y0h, x1l*y1h
    __m128i w2     = _mm_mul_epu32(xh, y);          // x0h*y0l, x1h*y0l

    __m128i yinv   = _mm_xor_si128(w1,ys);          // invert bits if negative
            w1     = _mm_sub_epi64(yinv,ys);         // add 1
    __m128i xinv   = _mm_xor_si128(w2,xs);          // invert bits if negative
            w2     = _mm_sub_epi64(xinv,xs);         // add 1

    __m128i w0l    = _mm_and_si128(w0, lomask);
    __m128i w0h    = _mm_srli_epi64(w0, 32);

    __m128i s1     = _mm_add_epi64(w1, w0h);         // xl*yh + w0h;
    __m128i s1l    = _mm_and_si128(s1, lomask);      // lo(wl*yh + w0h);
    __m128i s1h    = _mm_srai_epi64(s1, 32);

    __m128i s2     = _mm_add_epi64(w2, s1l);         //xh*yl + s1l
    __m128i s2l    = _mm_slli_epi64(s2, 32);
    __m128i s2h    = _mm_srai_epi64(s2, 32);           //arithmetic shift right

    __m128i hi1    = _mm_add_epi64(w3, s1h);
            hi1    = _mm_add_epi64(hi1, s2h);

    __m128i lo1    = _mm_add_epi64(w0l, s2l);
    *hi = hi1;
    *lo = lo1;
}

Ухудшается. Существует не _mm_srai_epi64 instrinsic/инструкция до AVX512, поэтому я должен был сделать свой собственный.

static inline __m128i _mm_srai_epi64(__m128i a, int b) {
    __m128i sra = _mm_srai_epi32(a,32);
    __m128i srl = _mm_srli_epi64(a,32);
    __m128i mask = _mm_set_epi32(-1,0,-1,0);
    __m128i out = _mm_blendv_epi8(srl, sra, mask);
}

Моя реализация _mm_srai_epi64 выше неполна. Я думаю, что использовал Agner Fog Vector Class Library. Если вы посмотрите в файле vectori128.h, вы найдете

static inline Vec2q operator >> (Vec2q const & a, int32_t b) {
    // instruction does not exist. Split into 32-bit shifts
    if (b <= 32) {
        __m128i bb   = _mm_cvtsi32_si128(b);               // b
        __m128i sra  = _mm_sra_epi32(a,bb);                // a >> b signed dwords
        __m128i srl  = _mm_srl_epi64(a,bb);                // a >> b unsigned qwords
        __m128i mask = _mm_setr_epi32(0,-1,0,-1);          // mask for signed high part
        return  selectb(mask,sra,srl);
    }
    else {  // b > 32
        __m128i bm32 = _mm_cvtsi32_si128(b-32);            // b - 32
        __m128i sign = _mm_srai_epi32(a,31);               // sign of a
        __m128i sra2 = _mm_sra_epi32(a,bm32);              // a >> (b-32) signed dwords
        __m128i sra3 = _mm_srli_epi64(sra2,32);            // a >> (b-32) >> 32 (second shift unsigned qword)
        __m128i mask = _mm_setr_epi32(0,-1,0,-1);          // mask for high part containing only sign
        return  selectb(mask,sign,sra3);
    }
}

Ответы

Ответ 1

Правильный способ рассмотрения пределов пропускной способности целочисленного умножения с использованием различных инструкций заключается в том, сколько "битов продукта" вы можете вычислить за цикл.

mulx выводит один результат 64x64 → 128 каждый цикл; что 64x64 = 4096 бит продукта за цикл "

Если вы объединяете мультипликатор на SIMD из инструкций, которые умножают 32x32 → 64 бит, вы должны иметь возможность получать четыре результата в каждом цикле в соответствии с mulx (4x32x32 = 4096). Если не было никакой арифметики, кроме множителей, вы бы просто сломались даже на AVX2. К сожалению, как вы уже заметили, существует множество арифметических операций, отличных от множителей, поэтому это полный отказ от исходного оборудования.

Ответ 2

Я нашел решение SIMD, которое намного проще и не нуждается в продуктах signed*unsigned. Я больше не уверен, что SIMD (по крайней мере, с AVX2 и AV512) не может конкурировать с mulx. В некоторых случаях SIMD может конкурировать с mulx. Единственный случай, о котором я знаю, - это умножение больших чисел на FFT.

Трюк состоял в том, чтобы сначала выполнить беззнаковое умножение, а затем исправить. Я узнал, как это сделать из этого ответа 32-bit-signed-multiplication-without-using-64-bit-data-type. Коррекция проста для (hi,lo) = x*y сначала выполнить беззнаковое умножение, а затем исправить hi следующим образом:

hi -= ((x<0) ? y : 0)  + ((y<0) ? x : 0)

Это можно сделать с помощью встроенного SSE4.2 _mm_cmpgt_epi64

void muldws1_sse(__m128i x, __m128i y, __m128i *lo, __m128i *hi) {    
    muldwu1_sse(x,y,lo,hi);    
    //hi -= ((x<0) ? y : 0)  + ((y<0) ? x : 0);
    __m128i xs = _mm_cmpgt_epi64(_mm_setzero_si128(), x);
    __m128i ys = _mm_cmpgt_epi64(_mm_setzero_si128(), y);           
    __m128i t1 = _mm_and_si128(y,xs);
    __m128i t2 = _mm_and_si128(x,ys);
           *hi = _mm_sub_epi64(*hi,t1);
           *hi = _mm_sub_epi64(*hi,t2);
}

Код для беззнакового умножения проще, поскольку для него не нужны смешанные продукты signed*unsigned. Кроме того, поскольку он без знака, ему не нужен арифметический сдвиг вправо, который имеет только инструкцию для AVX512. На самом деле следующей функции требуется только SSE2:

void muldwu1_sse(__m128i x, __m128i y, __m128i *lo, __m128i *hi) {    
    __m128i lomask = _mm_set1_epi64x(0xffffffff);

    __m128i xh     = _mm_shuffle_epi32(x, 0xB1);    // x0l, x0h, x1l, x1h
    __m128i yh     = _mm_shuffle_epi32(y, 0xB1);    // y0l, y0h, y1l, y1h

    __m128i w0     = _mm_mul_epu32(x,  y);          // x0l*y0l, x1l*y1l
    __m128i w1     = _mm_mul_epu32(x,  yh);         // x0l*y0h, x1l*y1h
    __m128i w2     = _mm_mul_epu32(xh, y);          // x0h*y0l, x1h*y0l
    __m128i w3     = _mm_mul_epu32(xh, yh);         // x0h*y0h, x1h*y1h

    __m128i w0l    = _mm_and_si128(w0, lomask);     //(*)
    __m128i w0h    = _mm_srli_epi64(w0, 32);

    __m128i s1     = _mm_add_epi64(w1, w0h);
    __m128i s1l    = _mm_and_si128(s1, lomask);
    __m128i s1h    = _mm_srli_epi64(s1, 32);

    __m128i s2     = _mm_add_epi64(w2, s1l);
    __m128i s2l    = _mm_slli_epi64(s2, 32);        //(*)
    __m128i s2h    = _mm_srli_epi64(s2, 32);

    __m128i hi1    = _mm_add_epi64(w3, s1h);
            hi1    = _mm_add_epi64(hi1, s2h);

    __m128i lo1    = _mm_add_epi64(w0l, s2l);       //(*)
    //__m128i lo1    = _mm_mullo_epi64(x,y);          //alternative

    *hi = hi1;
    *lo = lo1;
}

В этом случае используется

4x mul_epu32
5x add_epi64
2x shuffle_epi32
2x and
2x srli_epi64
1x slli_epi64
****************
16 instructions

AVX512 имеет встроенный _mm_mullo_epi64, который может вычислять lo с одной инструкцией. В этом случае можно использовать альтернативу (прокомментировать строки с комментарием (*) и раскомментировать альтернативную строку):

5x mul_epu32
4x add_epi64
2x shuffle_epi32
1x and
2x srli_epi64
****************
14 instructions

Чтобы изменить код для полной ширины AVX2, замените _mm на _mm256, si128 на si256, а __m128i на __m256i для AVX512 замените их на _mm512, si512 и __m512i.