Практичный BigNum AVX / SSE возможен?

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

Пример:

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


person user1095108    schedule 13.01.2015    source источник
comment
Конечно, это возможно, просто безумно неудобно, неэффективно и медленно. Когда вы выполняете сложение с массивами конечностей (32/64-битные слова), легко использовать флаг переноса x86 для распространения бита переноса. Дорожки регистров SSE не имеют флаги переноса, что означает, что переполнение должно быть обнаружено другим способом (более интенсивным с точки зрения вычислений), и даже если вы обнаружили переполнение, у вас есть проблема сложной SSE / AVX перетасовывает, чтобы поднять носители, и вы должны проделать это N-1 раз для N-конечностей bignums. Тогда что произойдет, если вам нужно расширить bignum за пределы 128/256 бит ...?   -  person Iwillnotexist Idonotexist    schedule 13.01.2015
comment
Вы объединяете 2 или более регистров вместе, используя векторные расширения gcc / clang / icc . Вы можете написать ответ, почему считаете это непрактичным. Дело в том, что я думаю, что gcc плохо отображает массивы в регистры SIMD, но он легко отображает регистры SIMD в обратном направлении и без каких-либо проблем.   -  person user1095108    schedule 13.01.2015
comment
То, с чем вы связались, не является способом динамического расширения bignum во время выполнения. Вы связались с методом объявления векторов ограниченного и фиксированного размера (с полосами) во время компиляции. У вас все еще есть те же проблемы, что я перечислил выше, и вы по-прежнему не можете легко обнаружить и распространить перенос от нижних конечностей к верхним с помощью этих расширений.   -  person Iwillnotexist Idonotexist    schedule 13.01.2015
comment
Хорошо, +1, напишите, пожалуйста, ответ, и я приму.   -  person user1095108    schedule 13.01.2015
comment
stackoverflow.com/questions/12200698/   -  person phuclv    schedule 13.01.2015
comment
@Zboson: Могут ли длинные целочисленные подпрограммы выиграть от SSE? - да, если вы измените формат хранилища, оставив некоторые запасные биты, чтобы можно было отложить нормализация / распространение переноса.   -  person Peter Cordes    schedule 19.03.2021


Ответы (3)


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

Вместо реализации одного BigNum с использованием регистра SIMD (или с массивом регистров SIMD) вы должны обрабатывать несколько BigNum одновременно.

Рассмотрим 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-битных чисел. Но нет 256-битной 64-битной беззнаковой инструкции от XOP. Вместо этого мы можем сделать следующее для 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
person Z boson    schedule 16.01.2015
comment
Как я и думал. По мере того, как регистры SIMD становятся все больше, в них будет все больше места для BigNums (и всего остального) и все меньше инструкций, необходимых для выполнения арифметических операций с ними. Кроме того, регистровая память - это самая быстрая память, доступная программисту. Не могли бы вы немного пояснить свою таблицу? - person user1095108; 16.01.2015
comment
Хорошо видно (+1): делаем нарезку. Но: 1) 128-битные целые числа являются расширенным (большим) целочисленным типом фиксированной точности, не обязательно типом произвольной точности. 2) Есть дополнительные накладные расходы и удача, которые здесь не учитываются. Как часто у кого-то n бигнумы одного размера и одинакового количества конечностей и вы хотите применить одни и те же операции к каждой из них? Как эффективно загружать их в регистры? Количество требуемых загрузок / (де) чередований / вставок / извлечений / сохранений означает, что точка безубыточности выше, чем предлагает ваша таблица. VGATHER можно, но AVX2 встречается редко, поскольку 3) XOP выходит за рамки последних предложений AMD. - person Iwillnotexist Idonotexist; 16.01.2015
comment
@IwillnotexistIdonotexist, загрузка может выполняться эффективно, если 128-битные целые числа хранятся в форме AoSoA (например, с AVX2 lolololohihihihi). Что вы имеете в виду, это XOP за пределами последнего предложения AMD. Это должен был быть вопрос? Ваш ответ в целом лучше. В основном я пытался найти случай, когда SIMD может быть полезен для типа точности большого целого числа. Если точность не фиксирована, можно использовать максимальную точность для каждого из них в блоке ширины SIMD. - person Z boson; 16.01.2015
comment
@ user1095108, я немного уточнил таблицу, но я не уверен, что вы хотели. Пожалуйста, дайте Iwillnotexist Idonotexist принятый ответ. Его ответ более общий, чем мой. Мой ответ больше касается особых случаев, которые могут существовать. - person Z boson; 16.01.2015
comment
@Zboson Нет; Я поддержал ваш (лучший) ответ, потому что мне стыдно, что мне раньше не приходил в голову подход к нарезке; Тем более что сам раньше пользовался. Это действенный подход в пределах своих возможностей. W.r.t. XOP и AVX2: я просто указал на то, что они возникают достаточно редко, и большинство людей не может их использовать; Я редко вижу кодовые пути с версией AMD / Intel, чаще всего с версией общего C / SSE2 и иногда AVX. - person Iwillnotexist Idonotexist; 16.01.2015
comment
@IwillnotexistIdonotexist, Я изучил 64b * 64b на 128b умножение с SIMD. Я не думаю, что это возможно превзойти mul, так что вы, вероятно, правы, что нет эффективного метода SIMD bignum даже с AVX512. - person Z boson; 02.03.2015
comment
Вам будет трудно поверить в это, но для записи я нашел способ (горизонтально) векторизовать bignum add. IOW, сложите два __m512i вместе, как если бы они были 512-битными целыми числами. Это можно сделать менее чем за 10 инструкций с достаточно коротким критическим путем, чтобы ограничить пропускную способность. IOW, он, вероятно, превзойдет цепочку из 8 инструкций добавления с переносом. К сожалению, для его работы требуется AVX512-DQ. Идея основана на сумматоре Когге-Стоун. Но пока я все еще прорабатываю детали (и доказательства), и я хочу сначала протестировать его на y-cruncher с реальным оборудованием. - person Mysticial; 22.01.2017
comment
@Mysticial, у вас есть доступ к оборудованию AVX512? - person Z boson; 23.01.2017
comment
@Zboson Нет. И я, скорее всего, не буду до Скайлейка Перли. Кстати, я нашел способ добавить Kogge-Stone к AVX512F и AVX2 - хотя и менее эффективно. И на Haswell, и на Skylake версия AVX2 примерно на 10% отстает от цепочки adc при добавлении пары 64000-битных целых чисел. (из кеша L1) Skylake имеет меньшую adc задержку, но также имеет более высокую пропускную способность int-SIMD. Так что эффекты нейтрализуются. - person Mysticial; 23.01.2017
comment
Я также попытался реализовать горизонтальное умножение с __m256i AVX2, используя ту же идею. Но прежде чем я закончил, я мог сказать, что он будет примерно в 4 раза медленнее, чем MULX + ADCX / ADOX. Итак, я остановился на этом. Это не будет победой даже с AVX512. - person Mysticial; 23.01.2017
comment
@Mysticial, как вы рассчитываете производительность AVX512 без железа. Вы просто смотрите значения задержки и пропускной способности или как-то имитируете это? Я знаю, что вы имитируете встроенные функции AVX512. Делали игрушечный эмулятор AVX512? - person Z boson; 24.01.2017
comment
@Mysticial, не могли бы вы дать ответ своим __m512i решением горизонтального добавления, описывающим метод Когге-Стоуна? Думаю, это будет интересно другим. А как насчет умножения? Вы добавляете одно число 512b, но как тогда делать мульт? Я думаю, это то, что вы имеете в виду, говоря выше. Тогда полезно ли это добавление 512b? - person Z boson; 24.01.2017
comment
как вы рассчитываете производительность AVX512 без оборудования - я не знаю, но инструкции дешевые. Для добавления вектора с вводом / выводом: версии AVX2 требуется 13 инструкций + LUT. Критический путь - 3 цикла. Это связано с пропускной способностью и всего на 10% медленнее, чем цепочка из 4 adcs + 4 загрузок + 4 хранилищ. Версия AVX512-DQ требует 9 инструкций, и все они дешевы. Критический путь - 3 инструкции маски. Каковы шансы, что он побьет 8 adcs + 8 загрузок + 8 магазинов? - person Mysticial; 24.01.2017
comment
Я менее уверен в KNL, так как он имеет длительные задержки обхода и меньшие возможности OOE. Для умножения это просто сложение нескольких строк с частичным продуктом. Так что проблема сводится к дополнениям. Проблема в том, что умножение 32 x 32 - ›64 SIMD требует слишком большого количества инструкций, чтобы конкурировать с MULX. - person Mysticial; 24.01.2017
comment
Если вам интересно, я написал блог о подходе, здесь. В конце концов я смог (неофициально) доказать, что это правильно. Но чем больше я исследую это, тем менее полезным он становится. - person Mysticial; 30.01.2017
comment
@Mysticial, я раньше не читал ваш блог (по крайней мере, описание алгоритмов). Это отличный ресурс, спасибо! - person Z boson; 30.01.2017
comment
Эта страница новая. Значит, вы не могли это прочитать. Но остальная часть сайта существует уже много лет. - person Mysticial; 31.01.2017

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

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

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

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

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

person Iwillnotexist Idonotexist    schedule 13.01.2015

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

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

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

person phuclv    schedule 13.01.2015
comment
не могли бы вы ответить на мой другой вопрос, связанный с вашим ответом. - person user1095108; 13.01.2015