Почему мой процессор не имеет встроенной поддержки BigInt?
насколько я понял, BigInts обычно реализуются на большинстве языков программирования в виде массивов, содержащих цифры, где, например. при добавлении двух из них, каждая цифра добавляется одна за другой, как мы знаем из школы, например:
246
816
* *
----
1062
где * отмечает, что было переполнение. Я узнал об этом в школе, и все функции добавления BigInt, которые я реализовал, похожи на приведенный выше пример.
Итак, мы все знаем, что наши процессоры могут только изначально управление ints от 0 до 2^32
/ 2^64
.
это означает, что большинство скриптовых языков, чтобы быть высокоуровневыми и предлагать арифметику с большими целыми числами, должны реализовывать/использовать библиотеки BigInt, которые работают с целыми числами как массивы, как указано выше. Но, конечно, это означает, что они будут намного медленнее, чем процессор.
Итак, я спросил себя:
- почему мой процессор не имеет встроенной функции BigInt?
Она будет работать как и любая другая библиотека BigInt, только (намного) быстрее и на более низком уровне: процессор извлекает одну цифру из кэша/ОЗУ, добавляет ее и снова записывает результат.
мне кажется, что это прекрасная идея, так почему же нет ничего подобного?
8 ответов
есть просто слишком много проблем, которые требуют, чтобы процессор имел дело с тонной вещей, которые не являются его работой.
предположим, что у процессора была эта функция. Мы можем разработать систему, в которой мы знаем, сколько байтов используется данным BigInt-просто используйте тот же принцип, что и большинство строковых библиотек, и запишите длину.
но что произойдет, если результат операции BigInt превысит объем зарезервированного пространства?
есть два опции:
- он обернется внутри пространства, которое у него есть или
- он будет использовать больше памяти.
дело в том, что если бы это было 1), то это бесполезно - вам нужно было бы знать, сколько места требуется заранее, и это часть причины, по которой вы хотите использовать BigInt - так что вы не ограничены этими вещами.
Если он сделал 2), то он должен будет каким-то образом выделить эту память. Распределение памяти не выполняется одинаково в ОС, но даже если бы это было так, ему все равно пришлось бы обновлять все указатели на старое значение. Откуда ему знать, что такое указатели на значение и что такое просто целочисленные значения, содержащие то же значение, что и адрес памяти?
Двоичный Код Decimal является формой Строковой математики. Процессоры Intel x86 имеют опкоды для прямые АРТМЕТИЧЕСКИЕ операции БХД.
предположим, что результат умножения требуется 3 раза пространство (память) для хранения - где процессор будет хранить этот результат ? Как пользователи этого результата, включая все указатели на него, знают, что его размер внезапно изменился - и изменение размера может потребоваться для его перемещения в память, потому что расширение текущего местоположения столкнется с другой переменной.
Это создаст много взаимодействия между процессором, управлением памятью ОС и компилятором это было бы трудно сделать как общим, так и эффективным.
управление памятью типов приложений-это не то, что должен делать процессор.
Как я думаю, основная идея не включать поддержку bigint в современных процессорах-это желание уменьшить ISA и оставить как можно меньше инструкций, которые извлекаются, декодируются и выполняются на полной скорости. Кстати, в процессорах семейства x86 есть набор инструкций, которые делают написание большой библиотеки int делом одного дня. Другая причина, я думаю, это цена. Гораздо эффективнее сохранить некоторый космос на вафле падая резервные деятельности, которые могут быть легко реализовано на более высоком уровне.
кажется, Intel добавляет (или добавил как @ time этого после 2015) новые инструкции для поддержки большой целочисленной арифметики.
новые инструкции вводятся на архитектуре Intel® Процессоры для быстрой реализации большой целочисленной арифметики. Большая целочисленная арифметика широко используется в библиотеках с несколькими точностями для высокопроизводительных технических вычислений, а также для открытого ключа криптографии (например, RSA). В этой статье мы опишем критический операции, требуемые в большой целочисленной арифметике, и их эффективность реализации с использованием новых инструкций.
есть так много инструкций и функциональных возможностей, 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
: добавить с carry /sbb
: убавить с одолжить -
mul
: полное умножение, производя верхнюю и нижнюю половины результата: например, 64b * 64b = > 128b -
div
: дивиденд в два раза шире других операндов, например, 128b / 64b => 64b деление.
конечно, 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 на самом деле имеет определенные внутренние компоненты для adc
(и adcx
/ 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