Как вы делаете знаковое 32-битное расширяющее умножение на SSE2?

Этот вопрос возник при рассмотрении предложения WebAssembly SIMD для расширенного умножения.

Для поддержки старого оборудования нам необходимо поддерживать SSE2, а единственная операция векторного умножения для 32-битных целых чисел — pmuludq. (Подписанный pmuldq был добавлен только в SSE4.1)

(нерасширение относительно просто; перемешайте, чтобы подать 2x pmuludq, и возьмите младшие половины 4 результатов, чтобы эмулировать SSE4.1 pmulld).


person Dan Weber    schedule 09.10.2020    source источник
comment
Обратите внимание, что в SSE4.1 добавлено pmuldq расширяющее знаковое умножение. Так что он существует, но вы правы, что он не является частью SSE2 и, следовательно, не является базовым для x86-64:/   -  person Peter Cordes    schedule 09.10.2020


Ответы (2)


mulhs(a, b) = mulhu(a, b) - (a < 0 ? b : 0) - (b < 0 ? a : 0)

Используя это, два произведения двойной ширины со знаком могут быть вычислены следующим образом:

__m128i mul_epi32(__m128i a, __m128i b) {
    a = _mm_shuffle_epi32(a, _MM_SHUFFLE(3, 1, 1, 0));
    b = _mm_shuffle_epi32(b, _MM_SHUFFLE(3, 1, 1, 0));
    __m128i unsignedProduct = _mm_mul_epu32(a, b);
    __m128i threshold = _mm_set_epi32(INT_MIN, 0, INT_MIN, 0);
    __m128i signA = _mm_cmplt_epi32(a, threshold);
    __m128i signB = _mm_cmplt_epi32(b, threshold);
    __m128i x = _mm_shuffle_epi32(_mm_and_si128(signA, b), _MM_SHUFFLE(2, 3, 0, 1));
    __m128i y = _mm_shuffle_epi32(_mm_and_si128(signB, a), _MM_SHUFFLE(2, 3, 0, 1));
    return _mm_sub_epi32(_mm_sub_epi32(unsignedProduct, x), y);
}

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

person harold    schedule 09.10.2020
comment
Я думаю, что у нас есть требования, чтобы он выводился в том же порядке, что и оригинал. pmuludq делает то же самое, даже на линии, о которой вы говорите. Перетасовки в моей версии существуют для того, чтобы приспособиться к требованиям стандарта. Если вы добавите перетасовку для себя, будет ли она работать лучше или по-другому? - person Dan Weber; 09.10.2020
comment
@DanWeber по-прежнему убирает две инструкции, но я ничего не сравнивал - person harold; 09.10.2020
comment
Если вы посмотрите на ассемблерную версию Clang (она на стреле бога), ей удалось сбрить 1 операцию перетасовки с моей, хотя я не совсем уверен, что понимаю, что она сделала. - person Dan Weber; 09.10.2020
comment
threshold можно сгенерировать на лету двумя инструкциями: pcmpeqd xmm0, xmm0 (все единицы) / psllq xmm0, 63, чтобы оставить только установленный бит знака в каждой половине qword. Компиляторы по-прежнему будут просто загружать константу, но если вы используете JIT и хотите избежать загрузки, вы можете сделать это, @DanWeber. Хорошо, если вы хотите выйти из цикла, в противном случае, вероятно, загрузите константу вместо того, чтобы тратить больше инструкций векторного ALU для ее регенерации каждый раз. - person Peter Cordes; 09.10.2020
comment
Можете ли вы с пользой получить маску a<0 с psrad арифметическим сдвигом вправо? - person Peter Cordes; 09.10.2020
comment
@PeterCordes Сначала я пытался использовать psrad, но потом мне понадобился дополнительный pand, чтобы обнулить нечетные полосы, так что это не показалось хорошей сделкой. - person harold; 09.10.2020
comment
@PeterCordes здесь нет psubq, только верхняя половина продукта нуждается в исправлении (но это также означает, что нижнюю половину нужно оставить в покое, что является усложняющим фактором) - person harold; 09.10.2020
comment
Ах да, конечно, нижние половинки уже правильные, вот как работает умножение. Теперь я понимаю, что mulhs — это продукт с высокой половиной, и это выражение просто показывает исправления с высокой половиной. Мозг пукает с моей стороны, теперь я понимаю, почему psubq не нужен. - person Peter Cordes; 09.10.2020
comment
@PeterCordes может ли оптимизация с использованием psubq в потоке сравнения работать и здесь? - person Dan Weber; 09.12.2020

Большое спасибо @GeDaMo на ##asm за помощь в разработке этого решения.

Божья стрела

C/C++:

#include <xmmintrin.h>
#include <stdint.h>
#include <tmmintrin.h>
#include <smmintrin.h>
#include <cstdio>

typedef int32_t int32x4_t __attribute__((vector_size(16))) __attribute__((aligned(16)));
typedef int64_t int64x2_t __attribute__((vector_size(16))) __attribute__((aligned(16)));

int64x2_t multiply32_low_s(int32x4_t a, int32x4_t b) {
    auto aSigns = a >> 31;
    auto bSigns = b >> 31;
    auto aInt = a ^ aSigns;
    aInt -= aSigns;
    auto bInt = b ^ bSigns;
    bInt -= bSigns;
    const auto shuffleMask = _MM_SHUFFLE(1,1,0,0);
    auto absProd = _mm_mul_epu32(_mm_shuffle_epi32((__m128i)aInt, shuffleMask), _mm_shuffle_epi32((__m128i)bInt, shuffleMask));
    auto aSignsInt = _mm_shuffle_epi32((__m128i)aSigns, shuffleMask);
    auto bSignsInt = _mm_shuffle_epi32((__m128i)bSigns,shuffleMask);
    auto prodSigns = aSignsInt ^ bSignsInt;
    absProd ^= prodSigns;
    absProd -= prodSigns;
    return (int64x2_t)absProd;
}

int64x2_t multiply32_high_s(int32x4_t a, int32x4_t b) {
    auto aSigns = a >> 31;
    auto bSigns = b >> 31;
    auto aInt = a ^ aSigns;
    aInt -= aSigns;
    auto bInt = b ^ bSigns;
    bInt -= bSigns;
    const auto shuffleMask = _MM_SHUFFLE(3,3,2,2);
    auto absProd = _mm_mul_epu32(_mm_shuffle_epi32((__m128i)aInt, shuffleMask), _mm_shuffle_epi32((__m128i)bInt, shuffleMask));
    auto aSignsInt = _mm_shuffle_epi32((__m128i)aSigns, shuffleMask);
    auto bSignsInt = _mm_shuffle_epi32((__m128i)bSigns,shuffleMask);
    auto prodSigns = aSignsInt ^ bSignsInt;
    absProd ^= prodSigns;
    absProd -= prodSigns;
    return (int64x2_t)absProd;
}


int main(int argc, char* argv[]) {
    int32x4_t a{-5,500,-5000,50000};
    int32x4_t b{10,-100,-5000,500000000};
    auto c = multiply32_low_s(a,b);
    auto d = multiply32_high_s(a,b);
    printf("%ld %ld\n", c[0],c[1]);
    printf("%ld %ld\n", d[0],d[1]);
}

сборка

multiply32_low_s(int __vector(4), int __vector(4)):
 movdqa xmm3,xmm0
 movdqa xmm2,xmm1
 psrad  xmm3,0x1f
 psrad  xmm2,0x1f
 pxor   xmm0,xmm3
 pxor   xmm1,xmm2
 psubd  xmm1,xmm2
 psubd  xmm0,xmm3
 pshufd xmm2,xmm2,0x50
 pshufd xmm1,xmm1,0x50
 pshufd xmm0,xmm0,0x50
 pshufd xmm3,xmm3,0x50
 pmuludq xmm0,xmm1
 pxor   xmm2,xmm3
 pxor   xmm0,xmm2
 psubq  xmm0,xmm2
 ret    
 nop    WORD PTR [rax+rax*1+0x0]
multiply32_high_s(int __vector(4), int __vector(4)):
 movdqa xmm3,xmm0
 movdqa xmm2,xmm1
 psrad  xmm3,0x1f
 psrad  xmm2,0x1f
 pxor   xmm0,xmm3
 pxor   xmm1,xmm2
 psubd  xmm1,xmm2
 psubd  xmm0,xmm3
 pshufd xmm2,xmm2,0xfa
 pshufd xmm1,xmm1,0xfa
 pshufd xmm0,xmm0,0xfa
 pshufd xmm3,xmm3,0xfa
 pmuludq xmm0,xmm1
 pxor   xmm2,xmm3
 pxor   xmm0,xmm2
 psubq  xmm0,xmm2
 ret    
 nop    WORD PTR [rax+rax*1+0x0]
person Dan Weber    schedule 09.10.2020
comment
Это вообще стоит делать? Скалярный запасной вариант выглядит относительно многообещающе. - person harold; 09.10.2020
comment
Это отличный вопрос. Я хотел бы услышать отзывы о нем. - person Dan Weber; 09.10.2020
comment
@harold: Вероятно, единственная причина для такой большой работы с векторами (даже с вашей оптимизированной версией) заключается в том, что действительно полезно иметь результат в векторах, а ввод начинать с векторных регистров. то есть как шаг обработки между операциями SIMD, а не сам по себе. Может быть, даже не тогда; godbolt.org/z/MWY8hq показывает 16 инструкций, а процессоры слишком старые для SSE4.1. t есть mov-устранение xor-устранение. K10 и Core2 имеют как минимум 128-битные векторные ALU; более ранние процессоры этого не делают, что делает эти векторные вставки еще более дорогими. - person Peter Cordes; 09.10.2020
comment
Так что да, movq/shuffle/movq или store/load для передачи некоторого скаляра imul r64,r64 (или, может быть, imul r32 на процессорах с медленным 64-битным целочисленным умножением), вероятно, хороши, даже если нам придется съесть стойло переадресации хранилища, чтобы вернуться к векторам. . (4x скалярные хранилища + векторная загрузка могут быть более производительными, чем 4x movd + 3x punpck, если бы мы использовали 32-битный imul, чтобы результат был разделен на 4 регистра.) - person Peter Cordes; 09.10.2020
comment
@PeterCordes Мне серьезно интересно, почему мы поддерживаем sse2 для wasm. Я предполагаю, что v8 нуждается в сборке для скалярных вариантов, когда они применимы. - person Dan Weber; 10.10.2020
comment
SSE2 является базовым для x86-64, поэтому способность обрабатывать только это означает, что вы можете работать на любой системе x86-64, не принимая решения о том, какие процессоры слишком стары. Для некоторых вещей SSE2 отлично подходит, например. вертикальный SIMD с числами с плавающей запятой или двойным числом, или целыми числами в виде байтов или слов. Если у wasm нет способа для приложений определить, что тип SIMD, который они хотят сделать, будет медленнее, чем скалярный, это проблематично, но полный отказ от SIMD может быть более серьезной проблемой для других целей. (Собственный код x86 может проверять это и выбирать для каждой функции, устанавливая указатели функций.) - person Peter Cordes; 10.10.2020
comment
Требование SSSE3 распространяется на Core 2 первого поколения, например 2007 года. Поэтому я вижу некоторый аргумент в пользу требования просто SSE4.1 по крайней мере. Старым процессорам будет сложно работать с современными веб-браузерами. - person Peter Cordes; 10.10.2020
comment
Я привел аналогичный аргумент, говоря, что Nehalem было почти 12. Мы динамически обнаруживаем наборы инструкций, и я думаю, что дошел до сути, почему вы видели, что раздражающие инструкции смешиваются с не раздражающими. Компилятор V8 сопоставляет встроенные функции в зависимости от того, определен ли набор инструкций более высокого порядка. Например, если бы кто-то написал что-то и сказал, что это будет работать с ssse3 и выше, и он никогда не писал вариант avx, это было бы не раздражающим. - person Dan Weber; 10.10.2020