Почему мой процессор не имеет встроенной поддержки BigInt?

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

 246
 816
 * *
----
1062

где * отмечает, что было переполнение. Я узнал об этом в школе, и все функции добавления BigInt, которые я реализовал, похожи на приведенный выше пример.

Итак, мы все знаем, что наши процессоры могут только изначально управление ints от 0 до 2^32 / 2^64.

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

Итак, я спросил себя:

  • почему мой процессор не имеет встроенной функции BigInt?

Она будет работать как и любая другая библиотека BigInt, только (намного) быстрее и на более низком уровне: процессор извлекает одну цифру из кэша/ОЗУ, добавляет ее и снова записывает результат.

мне кажется, что это прекрасная идея, так почему же нет ничего подобного?

8 ответов


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

предположим, что у процессора была эта функция. Мы можем разработать систему, в которой мы знаем, сколько байтов используется данным BigInt-просто используйте тот же принцип, что и большинство строковых библиотек, и запишите длину.

но что произойдет, если результат операции BigInt превысит объем зарезервированного пространства?

есть два опции:

  1. он обернется внутри пространства, которое у него есть или
  2. он будет использовать больше памяти.

дело в том, что если бы это было 1), то это бесполезно - вам нужно было бы знать, сколько места требуется заранее, и это часть причины, по которой вы хотите использовать BigInt - так что вы не ограничены этими вещами.

Если он сделал 2), то он должен будет каким-то образом выделить эту память. Распределение памяти не выполняется одинаково в ОС, но даже если бы это было так, ему все равно пришлось бы обновлять все указатели на старое значение. Откуда ему знать, что такое указатели на значение и что такое просто целочисленные значения, содержащие то же значение, что и адрес памяти?


Двоичный Код Decimal является формой Строковой математики. Процессоры Intel x86 имеют опкоды для прямые АРТМЕТИЧЕСКИЕ операции БХД.


предположим, что результат умножения требуется 3 раза пространство (память) для хранения - где процессор будет хранить этот результат ? Как пользователи этого результата, включая все указатели на него, знают, что его размер внезапно изменился - и изменение размера может потребоваться для его перемещения в память, потому что расширение текущего местоположения столкнется с другой переменной.

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

управление памятью типов приложений-это не то, что должен делать процессор.


Как я думаю, основная идея не включать поддержку bigint в современных процессорах-это желание уменьшить ISA и оставить как можно меньше инструкций, которые извлекаются, декодируются и выполняются на полной скорости. Кстати, в процессорах семейства x86 есть набор инструкций, которые делают написание большой библиотеки int делом одного дня. Другая причина, я думаю, это цена. Гораздо эффективнее сохранить некоторый космос на вафле падая резервные деятельности, которые могут быть легко реализовано на более высоком уровне.


кажется, Intel добавляет (или добавил как @ time этого после 2015) новые инструкции для поддержки большой целочисленной арифметики.

новые инструкции вводятся на архитектуре Intel® Процессоры для быстрой реализации большой целочисленной арифметики. Большая целочисленная арифметика широко используется в библиотеках с несколькими точностями для высокопроизводительных технических вычислений, а также для открытого ключа криптографии (например, RSA). В этой статье мы опишем критический операции, требуемые в большой целочисленной арифметике, и их эффективность реализации с использованием новых инструкций.

http://www.intel.com/content/www/us/en/intelligent-systems/intel-technology/ia-large-integer-arithmetic-paper.html


есть так много инструкций и функциональных возможностей, jockeying для области на чипе процессора, что в конце концов те, которые используются чаще/считаются более полезными, будут выталкивать те, которые не являются. Инструкции, необходимые для реализации функциональности BigInt, есть, и математика прямолинейна.


он будет работать как любая другая библиотека BigInt, только (намного) быстрее и на более низком уровне: процессор извлекает одну цифру из кэша/ОЗУ, добавляет ее и снова записывает результат.

почти все процессоры do этот встроенный. Вы должны использовать программный цикл вокруг соответствующих инструкций, но это не делает его медленнее, если цикл эффективен. (Это нетривиально на x86, из-за частичных флагов, см. ниже)

например, если x86 при условии rep adc чтобы сделать src += dst, принимая 2 указателя и длину в качестве входных данных (например,rep movsd в функции memcpy), он все равно будет реализован как цикл в микрокоде.

было бы возможно, чтобы 32-битный процессор x86 имел внутреннюю реализацию rep adc который использовал 64bit добавляет внутренне, так как 32-битные процессоры, вероятно, все еще имеют 64-битный сумматор. Однако 64-битные процессоры, вероятно, не имеют однотактного сумматора задержки 128b. Так что я не ожидал, что наличие специальной инструкции для этого даст ускорение над тем, что вы можете сделать с программным обеспечением, по крайней мере на 64-битном процессоре.

возможно, специальная инструкция wide-add будет полезна на маломощном процессоре с низкой тактовой частотой, где возможен действительно широкий сумматор с задержкой в один цикл.


инструкции x86, которые вы ищете:

конечно, adc работает с двоичными целыми числами, а не с десятичными цифрами. x86 can adc в 8, 16, 32 или 64-битных кусках, в отличие от процессоров RISC, которые обычно только АЦП при полной ширине регистра. (GMP называет каждый кусок "конечностью"). (x86 имеет некоторые инструкции для работы с BCD или ASCII, но эти инструкции были удалены для x86-64.)

imul / idiv подписанный эквиваленты. Добавить работает так же для дополнения signed 2, как и для unsigned, поэтому нет отдельной инструкции; просто посмотрите на соответствующие флаги для обнаружения подписанного и неподписанного переполнения. Но для adc, помните, что только самый значительный кусок имеет знак бит; остальные необходимы без знака.

ADX и BMI / BMI2 добавить некоторые инструкции, такие как mulx: full-multiply без касания флагов, поэтому его можно чередовать с adc цепочка для создания большего параллелизма на уровне инструкций для использования суперскалярных процессоров.

в x86, adc даже доступен с назначением памяти, поэтому он выполняет точно так же, как вы описываете: одна инструкция запускает все чтение / изменение / запись куска BigInteger. См. пример под.


большинство языков высокого уровня (включая C/C++) не выставляют флаг "carry"

обычно нет встроенных надстроек с переносом непосредственно в C. библиотеки BigInteger обычно должны быть написаны в asm для хорошей производительности.

однако Intel на самом деле имеет определенные внутренние компоненты для adcadcx / adox).

unsigned char _addcarry_u64 (unsigned char c_in, unsigned __int64 a, \
                             unsigned __int64 b, unsigned __int64 * out);

таким образом, результат переноса обрабатывается как unsigned char в с. _addcarryx_u64 intrinsic, это до компилятора, чтобы проанализировать цепочки зависимостей и решить, что добавляет делать с adcx и что делать с adox, и как связать их вместе, чтобы реализовать источник C.

IDK какой смысл _addcarryx intrinsics, вместо того, чтобы просто использовать компилятор adcx/adox по существующей _addcarry_u64 внутреннеприсущий, когда параллельные цепи dep которые могут принять преимущество его. Возможно, некоторые компиляторы недостаточно умны для что.


вот пример функции добавления BigInteger в синтаксисе NASM:

;;;;;;;;;;;; UNTESTED ;;;;;;;;;;;;
; C prototype:
; void bigint_add(uint64_t *dst, uint64_t *src, size_t len);
;   len is an element-count, not byte-count

global bigint_add
bigint_add:   ; AMD64 SysV ABI: dst=rdi, src=rsi, len=rdx

                              ; set up for using dst as an index for src
    sub    rsi, rdi           ;  rsi -= dst.  So orig_src = rsi + rdi

    clc                           ;  CF=0 to set up for the first adc
           ; alternative: peel the first iteration and use add instead of adc

.loop:
    mov    rax, [rsi + rdi]   ; load from src
    adc    rax, [rdi]         ;  <================= ADC with dst
    mov    [rdi], rax         ; store back into dst.  This appears to be cheaper than  adc  [rdi], rax  since we're using a non-indexed addressing mode that can micro-fuse

    lea    rdi,  [rdi + 8]    ; pointer-increment without clobbering CF
    dec    rdx                ; preserves CF
    jnz    .loop              ; loop while(--len)

    ret

на более старых моделях, особенно предварительно Sandybridge, adc вызовет частичный флаг при чтении CF после dec пишет другие флаги. цикл с другой инструкцией поможет для старых процессоров, которые останавливаются при слиянии частичного флага, но не стоит на SnB-family.

развертывание цикла также очень важно для adc петли. adc декодирует до нескольких uops на Intel, поэтому накладные расходы цикла являются проблемой, esp, если у вас есть дополнительные накладные расходы цикла от избежания частичных флагов. Если len - небольшая известная константа, полностью развернутый цикл обычно хорош. (например, компиляторы просто используют add/adc сделать uint128_t на x86-64.)

adc С назначением памяти, похоже, не самый эффективный способ, так как трюк с разницей указателей позволяет нам использовать один регистр режим адресации для dst. (Без этого трюка, память-операнды не будут микро-fuse).

по данным таблицы инструкций Agner Fog для Haswell и Skylake,adc r,m - 2 uops (сплавленный домен) с одним на 1 пропускную способность часов, в то время как adc m, r/i - 4 uops (сплавленный домен), с одним на пропускную способность часов 2. По-видимому, это не помогает, что Broadwell/Skylake run adc r,r/i как инструкция одиночн-uop (принимающ преимущество способности иметь uops с 3 входные зависимости, введенные с Haswell для FMA). Я также не на 100% уверен, что результаты Агнера прямо здесь, так как он не понял, что процессоры семейства SnB только микро-предохранители индексируют режимы адресации в декодерах / UOP-кэше, а не в ядре вне порядка.

во всяком случае, этот простой не развернутый цикл составляет 6 uops и должен выполняться на одной итерации за 2 цикла на процессорах семейства Intel SnB. Даже если для слияния с частичным флагом требуется дополнительный uop, это все равно легко меньше, чем 8 fused-домен uops, который может быть выпущен за 2 цикла.

некоторые незначительные развертки могут приблизиться к 1 adc за цикл, так как эта часть составляет всего 4 uops. Однако 2 нагрузки и один магазин за цикл не вполне устойчивы.


Extended-precision умножить и разделить также возможны, воспользовавшись расширением / сужением умножить и разделить инструкции. Это намного сложнее, конечно, из-за природы мультипликационный.


это не очень полезно использовать SSE для add-with carry, или AFAIK любые другие операции BigInteger.

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


BigInt: требуется фундаментальная функция: Умножение целого числа без знака, добавление предыдущего высокого порядка Я написал один в ассемблере Intel 16bit, затем 32 бит... Код C обычно достаточно быстр .. ie для BigInt вы используете библиотеку программного обеспечения. Процессоры (и графические процессоры) не предназначены с целым числом без знака в качестве высшего приоритета.

Если вы хотите написать свой собственный BigInt...

деление выполняется через Knuths Vol 2 (его куча умножить и вычесть, с некоторыми хитрыми add-backs)

добавить с переносом и вычесть проще. и т. д. и т. п.

Я только что опубликовал это в Intel: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx SSE4 есть библиотека BigInt?

процессор i5 2410M я полагаю, не может использовать AVX [AVX только на очень последних процессорах Intel] но можно использовать SSE4.2

есть ли библиотека BigInt для SSE? Думаю, я ищу что-то, что реализует unsigned integer

PMULUDQ (с 128-битным операнды) PMULUDQ __m128i _mm_mul_epu32 (__m128i a, __m128i b)

и не несет.

его ноутбук, поэтому я не могу купить NVIDIA GTX 550, который не так велик на неподписанных Ints, я слышал. xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx