Практичный BigNum AVX/SSE?

Регистры SSE/AVX могут рассматриваться как целочисленные или с большими числами BigNums. То есть можно пренебречь тем, что существуют полосы вообще. Существует ли простой способ использовать эту точку зрения и использовать эти регистры как BigNums как по отдельности, так и в сочетании? Я спрашиваю, потому что из того, что мало я видел в библиотеках BigNum, они почти повсеместно хранят и делают арифметику на массивах, а не на SSE/AVX-регистрах. Переносимость?

Пример:

Скажем, вы сохраняете содержимое регистра SSE в качестве ключа в std::set, вы можете сравнить это содержимое как BigNum.

Ответы

Ответ 1

Я думаю, что возможно реализовать BigNum с SIMD эффективно, но не так, как вы предлагаете.

Вместо того, чтобы внедрять один BigNum с использованием SIMD-регистра (или с массивом SIMD-регистров), вы должны обрабатывать сразу несколько BigNums.

Рассмотрим 128-битное дополнение. Пусть 128-битные целые числа определяются парой высоких и низких 64-битных значений, и предположим, что мы хотим добавить 128-битное целое число (y_low, y_high) в 128-битное целое число (x_low, x_high). С помощью скалярных 64-битных регистров требуется только две команды

add rax, rdi // x_low  += y_low;
adc rdx, rsi // x_high += y_high + (x_low < y_low);

С SSE/AVX проблема, как объясняют другие, заключается в том, что нет флажков переноса SIMD. Флаг переноса должен быть рассчитан, а затем добавлен. Для этого требуется 64-разрядное сравнение без знака. Единственным реалистичным вариантом для этого с SSE является команда AMD XOP vpcomgtuq

vpaddq      xmm2, xmm0, xmm2 // x_low  += y_low;
vpcomgtuq   xmm0, xmm0, xmm2 // x_low  <  y_low
vpaddq      xmm1, xmm1, xmm3 // x_high += y_high
vpsubq      xmm0, xmm1, xmm0 // x_high += xmm0

Для добавления двух пар 128-разрядных чисел используется четыре команды. Для скалярных 64-битных регистров требуется также четыре команды (два add и два adc).

С AVX2 мы можем добавить сразу четыре пары 128-битных чисел. Но в XOP нет 64-битной 64-разрядной команды без знака. Вместо этого мы можем сделать следующее для a<b:

__m256i sign64 = _mm256_set1_epi64x(0x8000000000000000L);
__m256i aflip = _mm256_xor_si256(a, sign64);
__m256i bflip = _mm256_xor_si256(b, sign64);
__m256i cmp = _mm256_cmpgt_epi64(aflip,bflip);

Регистр sign64 может быть предварительно вычислен, поэтому необходимы только три инструкции. Поэтому добавление четырех пар 128-битных номеров с помощью AVX2 может быть выполнено с помощью шести инструкций

vpaddq
vpaddq
vpxor
vpxor
vpcmpgtq 
vpsubq

тогда как скалярные регистры нуждаются в восьми инструкциях.

AVX512 имеет одну инструкцию для выполнения 64-битного сравнения без знака vpcmpuq. Поэтому должно быть возможно добавить восемь пар 128-битных чисел, используя только четыре команды

vpaddq
vpaddq
vpcmpuq
vpsubq

В скалярном регистре потребуется 16 команд для добавления восьми пар 128-битных чисел.

Вот таблица с кратким изложением количества SIMD-инструкций (называемая nSIMD) и количеством скалярных инструкций (называемых nscalar), необходимых для добавления числа пар (называемых npairs) из 128-битных чисел

              nSIMD      nscalar     npairs
SSE2 + XOP        4           4           2
AVX2              6           8           4
AVX2 + XOP2       4           8           4
AVX-512           4          16           8

Обратите внимание, что XOP2 еще не существует, и я только предполагаю, что он может существовать в какой-то момент.

Обратите также внимание на то, что для эффективного использования массивы BigNum необходимо хранить в массиве структуры массива (AoSoA). Например, используя l, чтобы означать, что младшие 64-битные и h означают высокие 64-бит, массив из 128-битных целых чисел хранится как массив структур, подобных этому

lhlhlhlhlhlhlhlh

вместо этого следует сохранить с помощью AoSoA, подобного этому

SSE2:   llhhllhhllhhllhh
AVX2:   llllhhhhllllhhhh
AVX512: llllllllhhhhhhhh

Ответ 2

Перемещено из комментария выше

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

Для простой задачи добавления тривиальным является использование флага Carry x86 EFLAGS/RFLAGS для распространения добавления, перенесенного с самого низкого "конечности" вверх (для использования GMP) и перебирать произвольное количество конечностей, заложенных в массив.

Напротив, полосы регистров SSE/AVX не имеют флагов переноса, что означает, что переполнение должно быть обнаружено по-другому, включая сравнения, чтобы обнаруживать обход, который более интенсивно вычисляется. Более того, если переполнение обнаруживается в одном конечности, оно должно быть распространено путем уродливого перетасовки "вверх", а затем добавлено, и это добавление может привести к другому переполнению и переносу, до N-1 раз для N -limb bignum. Затем, как только сумма приносит бонусы за 128-битные/256-битные (или более 128 бит x # регистров), вам все равно придется переместить его в массив.

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

Ответ 3

Это возможно, но не практично.

Как я уже сказал в другом ответе, в AVX/SSE нет флага переноса, поэтому невозможно эффективно выполнять сложение и вычитание. И для умножения вам понадобится много перетасовки, чтобы получить расширенный результат умножения в желаемой позиции.

Если вам разрешено работать с новой микроархитектурой Haswell/Broadwell, решение будет MULX в BMI2 и ADOX, ADCX в ADX. Вы можете прочитать о них здесь.