Умножение SSE 2 64-битных целых чисел

Как умножить два 64-битных целых числа на два других 64-битных целых числа? Я не нашел ни одной инструкции, которая может это сделать.


person Ines Karmani    schedule 25.07.2013    source источник
comment
Что означают два 64-битных целых числа в этом контексте? Вы имеете в виду пару 64-битных целых чисел (а-ля комплексные числа) или одно 128-битное целое число, представленное как пара 64-битных целых чисел?   -  person Eric Brown    schedule 25.07.2013
comment
Я имею в виду одно битовое целое число m128i, представленное как пара 64-битных целых чисел.   -  person Ines Karmani    schedule 25.07.2013
comment
Возможный дубликат этого тогда вопрос.   -  person Eric Brown    schedule 25.07.2013
comment
Связано: Самый быстрый способ умножить массив int64_t? для AVX2 или SSE4.1 с анализом производительности по сравнению с 64-битным скалярным кодом ( если у вас еще нет данных в SIMD-векторах.)   -  person Peter Cordes    schedule 15.01.2019


Ответы (3)


Поздний ответ, но это лучшая версия того, что опубликовал Барабас.

Если вы когда-либо использовали векторные расширения GCC или Clang, они используют эту процедуру.

Здесь используется тот же метод, что и в умножении на длину и умножение на сетку.

    65
  * 73
  ----
    15 //   (5 * 3)
   180 //   (6 * 3) * 10
   350 //   (5 * 7) * 10
+ 4200 // + (6 * 7) * 100
------
  4745

Однако вместо того, чтобы делать каждую единицу из 10, он использует каждую единицу из 32 бит и пропускает последнее умножение, потому что оно всегда будет сдвигаться за 64-й бит, точно так же, как вы не умножали бы 6 * 7, если бы вы усечение значений больше 99.

#include <emmintrin.h>

/*
 * Grid/long multiply two 64-bit SSE lanes.
 * Works for both signed and unsigned.
 *   ----------------.--------------.----------------.
 *  |                |   b >> 32    | a & 0xFFFFFFFF |
 *  |----------------|--------------|----------------|  
 *  | d >> 32        |   b*d << 64  |    a*d << 32   |
 *  |----------------|--------------|----------------|
 *  | c & 0xFFFFFFFF |   b*c << 32  |       a*c      |
 *  '----------------'--------------'----------------'
 *  Add all of them together to get the product.
 *
 *  Because we truncate the value to 64 bits, b*d << 64 will be zero,
 *  so we can leave it out.
 *
 *  We also can add a*d and b*c first and then shift because of the
 *  distributive property: (a << 32) + (b << 32) == (a + b) << 32.
 */

__m128i Multiply64Bit(__m128i ab, __m128i cd)
{
    /* ac = (ab & 0xFFFFFFFF) * (cd & 0xFFFFFFFF); */
    __m128i ac = _mm_mul_epu32(ab, cd);

    /* b = ab >> 32; */
    __m128i b = _mm_srli_epi64(ab, 32);

    /* bc = b * (cd & 0xFFFFFFFF); */
    __m128i bc = _mm_mul_epu32(b, cd);

    /* d = cd >> 32; */
    __m128i d = _mm_srli_epi64(cd, 32);

    /* ad = (ab & 0xFFFFFFFF) * d; */
    __m128i ad = _mm_mul_epu32(ab, d);

    /* high = bc + ad; */
    __m128i high = _mm_add_epi64(bc, ad);

    /* high <<= 32; */
    high = _mm_slli_epi64(high, 32);

    /* return ac + high; */
    return _mm_add_epi64(high, ac);
}

Проводник компилятора Примечание. Версия векторного расширения GCC также приведена ниже для сравнения.

person EasyasPi    schedule 15.01.2019
comment
С -march=skylake-avx512 мы получаем AVX512DQ vpmulqq :) AVX512, наконец, представил 64-битное целочисленное умножение размера элемента. - person Peter Cordes; 15.01.2019
comment
И, кстати, без AVX2, вероятно, не стоит использовать SIMD для 64x64 => 64-битное умножение, если только у вас уже нет данных в векторах. (Одна скалярная imul r64, r/m64 uop на 64-битное целое число — это очень хорошо. Самый быстрый способ умножить массив int64_t?). В моем ответе используется mullo_epi32 (SSE4.1 или AVX2) для одновременного получения обоих продуктов с низким и высоким уровнем, хотя pmulld действительно занимает 2 мкп на процессорах Intel. - person Peter Cordes; 15.01.2019
comment
Истинный. Я хочу упомянуть, что метод, используемый для Neon, также делает то же самое, он выполняет vrev64 (32-битный обмен словами), умножение 4x32, vpaddl (попарное сложение), сдвиг влево, а затем длительное умножение с накоплением. Если бы в SSE было попарное добавление, это, вероятно, было бы быстрее, но, учитывая, что NEON_2_SSE масштабирует эту инструкцию, я предполагаю, что это не так. - person EasyasPi; 16.01.2019
comment
SSSE3 имеет phaddd, но он декодирует 2 перетасовки, которые передают paddd uop по вертикали; его быстрее не использовать. Я не просматривал детали моего связанного ответа, но в нем упоминается использование psrlq / paddq / pand (всего 3 операции) вместо phadd + pshufd (3 операции в случайном порядке + ADD). Больше инструкций, но меньше мопов и гораздо меньше узких мест при случайном переносе. О, vpaddl расширяет элементы. PHADDD имеет 2 входа и 1 выход, так что это не полная замена. - person Peter Cordes; 16.01.2019

Я знаю, что это старый вопрос, но я действительно искал именно это. Поскольку для этого до сих пор нет инструкции, я сам реализовал 64-битное умножение с помощью pmuldq, как упомянул Пол Р. Вот что я придумал:

// requires g++ -msse4.1 ...

#include <emmintrin.h>
#include <smmintrin.h>

__m128i Multiply64Bit(__m128i a, __m128i b)
{
    auto ax0_ax1_ay0_ay1 = a;
    auto bx0_bx1_by0_by1 = b;

    // i means ignored

    auto ax1_i_ay1_i = _mm_shuffle_epi32(ax0_ax1_ay0_ay1, _MM_SHUFFLE(3, 3, 1, 1));
    auto bx1_i_by1_i = _mm_shuffle_epi32(bx0_bx1_by0_by1, _MM_SHUFFLE(3, 3, 1, 1));

    auto ax0bx0_ay0by0 = _mm_mul_epi32(ax0_ax1_ay0_ay1, bx0_bx1_by0_by1);
    auto ax0bx1_ay0by1 = _mm_mul_epi32(ax0_ax1_ay0_ay1, bx1_i_by1_i);
    auto ax1bx0_ay1by0 = _mm_mul_epi32(ax1_i_ay1_i, bx0_bx1_by0_by1);

    auto ax0bx1_ay0by1_32 = _mm_slli_epi64(ax0bx1_ay0by1, 32);
    auto ax1bx0_ay1by0_32 = _mm_slli_epi64(ax1bx0_ay1by0, 32);

    return _mm_add_epi64(ax0bx0_ay0by0, _mm_add_epi64(ax0bx1_ay0by1_32, ax1bx0_ay1by0_32));
}

Godbolt на SSE Multiply64Bit.

person Bas    schedule 22.05.2017
comment
Проводили ли вы какое-либо сравнение кода с использованием для этого регистров общего назначения? Мне были бы интересны результаты, так как я делаю массу умножений 64 на 64 бита. - person jeteon; 09.08.2017
comment
Я только что провел сравнительный анализ, он все еще быстрее, чем скалярное умножение (скомпилированное с помощью cl/O2). Около 831600000 умножений в среднем. 0,37 секунды на моем довольно мощном i7 5820k. Между тем те же скалярные умножения заняли 1,71 в среднем. так что это примерно в 4 раза быстрее, что немного странно. Я думаю, cl действительно хорош в оптимизации суперскалярных инструкций. - person JukesOnYou; 24.10.2017
comment
_mm_mul_epi32 — это инструкция SSE4.1. _mm_mul_epu32 — это инструкция SSE2. _mm_mul_epu32 производит намного лучший код, но требует беззнаковых типов. - person jww; 09.01.2019

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

person Paul R    schedule 25.07.2013
comment
На мой взгляд, не было ли добавлено pmuldqq или что-то еще в SSE4? - person Gunther Piez; 26.07.2013
comment
В SSE4 есть pmuldq, который представляет собой 32x32 => 64-битное умножение, поэтому вы можете использовать его в качестве строительного блока для 64x64-битного умножения. - person Paul R; 26.07.2013
comment
Знаете ли вы лучший скалярный алгоритм для этого (при условии, что у вас есть только 32-битное оборудование)? Это то, что я бы сделал. Я бы разделил каждое число на его верхнюю и нижнюю 32-битную часть, а затем сделал бы (ab) = (al+ah)*(blbh) = t1 + t2 + t3 + t4, где t1= albl, t2=albh, t3=ahbl t4=ahbh. Каждый термин будет 64-битным числом. Тогда t2 и t3 придется снова разделить на младшую и старшую, и окончательное число будет (ab)l = t1 + t2l + t3l, (ab)h = t4 + t2h + t3h + c, где c — любой перенос из (a*b)l. Это 4 мульта и 7 аддов. Это где-то на SO? - person Z boson; 18.02.2015
comment
Я никогда не реализовывал это сам, но это должно быть что-то вроде метода, который вы предлагаете. Я не могу представить, что это будет очень эффективно, поэтому, вероятно, имеет смысл только в том случае, если у вас есть другие 64-битные SIMD-операции, с которыми вы хотите их комбинировать. - person Paul R; 18.02.2015
comment
В Sandy Bridge умножение общего назначения и векторное умножение выдаются на разные порты, поэтому вы можете получить умножения SSE бесплатно, если вы выполняете более одного набора умножений. Однако добавление и перетасовка будут проблемой. Если вы делаете что-то, что не требует большого порта 5, они также могут выйти бесплатно. - person jeteon; 09.08.2017