Почему мой процессор не имеет встроенной поддержки BigInt? - PullRequest
10 голосов
/ 13 апреля 2010

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

 246
 816
 * *
----
1062

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

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

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

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

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

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

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

Ответы [ 8 ]

9 голосов
/ 13 апреля 2010

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

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

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

Есть два варианта:

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

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

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

8 голосов
/ 13 апреля 2010

Двоичный код с двоичным кодом является формой строковой математики.Процессоры Intel x86 имеют коды операций для прямых BCD-операций .

3 голосов
/ 17 июня 2016

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

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

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

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

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


Нужные инструкции x86:

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

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

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

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


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

Обычно нет встроенных дополнений с переносомнепосредственно в библиотеках 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 в C. Для встроенного _addcarryx_u64, компилятор должен проанализировать цепочки зависимостей и решить, что добавить к adcx и что делать с adox, и как связать их вместе для реализации источника C.

IDK в чем смысл встроенных _addcarryx, вместо того, чтобы компилятор использовал 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 .

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

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

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

В любом случае, этот простой цикл «не развернут на всех» равен 6 мопам и должен выполняться с одной итерацией на 2 цикла на процессорах семейства Intel SnB. Даже если для слияния с частичным флагом требуется дополнительная моп, это все равно намного меньше, чем 8 мопов с объединенными доменами, которые могут быть выполнены за 2 цикла.

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


Возможны также умножение и деление с расширенной точностью, используя команды умножения и деления с увеличением / сужением. Конечно, все гораздо сложнее из-за характера умножения.


не очень полезно использовать SSE для переноса надстройки или AFAIK для любых других операций BigInteger.

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

3 голосов
/ 13 апреля 2010

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

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

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

1 голос
/ 25 августа 2015

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

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

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

1 голос
/ 13 апреля 2010

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

0 голосов
/ 13 октября 2011

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

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

Деление осуществляется с помощью Knuths Vol 2 (это кучка умножения и вычитания, с некоторыми хитрыми надбавками)

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

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

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

Существует ли библиотека BigInt для SSE? Я думаю, что я ищу что-то, что реализует целое число без знака

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

и делает переносы.

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

0 голосов
/ 21 февраля 2011

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

...