известно, что все модули меньше 2 ^ 64, поэтому все операнды реализуются 64-разрядными целыми числами без знака.
Однако a * b
- это 128-битное, что усложняет история. div
требует 128-битного дивиденда и a * b / n < n
, поэтому он не может переполнить деление (что подразумевает вход за пределы диапазона), поэтому записать в сборке x64 тривиально:
; compute (A * B) % N
; A: rax
; B: rdx
; N: rcx
; result: rdx
mul rdx
div rcx
И в C вышесказанное невозможно написать, за исключением некоторых специальных вещей, таких как __uint128_t
или _mul128
и _div128
. Как бы вы ни отображали этот код, эта форма div
является самой медленной из возможных форм , ищите «: DIV r64 128 / 64b (full)», например, Дамп синхронизации инструкций Haswell . Почти сто циклов, на процессорах до IceLake это в основном хуже, чем все остальное, что вы можете сделать, за исключением самостоятельного внедрения побитового разделения. Ледяное озеро отличается и, наконец, имеет приличное целочисленное деление, в 18 циклах (добавьте +4 для начального mul
для общего модуля) это все еще не быстрое , но по крайней мере это не так на порядок выше нормы и, возможно, стоит задуматься (включая покупку нового оборудования), потому что:
с множеством различных модулей
Это может сломать все, в зависимости от того, как много есть много Умножение Монтгомери требует нахождения некоторого забавного модульного обратного, по модулю степени два, чтобы вы могли по крайней мере использовать подъем Хензеля вместо расширенного евклидова, но это все еще стоит дюжину умножений плюс некоторые дополнительные вещи, все последовательные. Точно так же, уменьшение Барретта требует нахождения некоторой забавной обратной аппроксимации с фиксированной точкой, которая является причудливым способом сказать, что требуется предварительное деление. При слишком большом количестве различных модулей уловки бесполезны.