[英]Compute (a*b)%n FAST for 64-bit unsigned arguments in C(++) on x86-64 platforms?
我正在尋找一種快速方法來有效地計算( a
⋅ b
)模n
(在數學意義上)對於uint64_t
類型的a
, b
, n
。 我可以接受諸如n!=0
,甚至a<n && b<n
先決條件。
請注意,C 表達式(a*b)%n
不會剪切它,因為乘積被截斷為 64 位。 我正在尋找(uint64_t)(((uint128_t)a*b)%n)
除了我沒有uint128_t
(我知道,在 Visual C++ 中)。
我正在使用 Visual C++(最好)或 GCC/clang 內在方法,以充分利用 x86-64 平台上可用的底層硬件; 或者如果對於便攜式inline
函數無法做到這一點。
好的,這個怎么樣(未測試)
modmul:
; rcx = a
; rdx = b
; r8 = n
mov rax, rdx
mul rcx
div r8
mov rax, rdx
ret
前提是a * b / n <= ~0ULL
,否則會出現除法錯誤。 這是一個比a < n && m < n
稍微不嚴格的條件,其中一個可以大於n
,只要另一個足夠小。
不幸的是,它必須單獨組裝和鏈接,因為 MSVC 不支持 64 位目標的內聯 asm。
它也仍然很慢,真正的問題是 64 位div
,它可能需要近一百個周期(例如,在 Nehalem 上最多需要 90 個周期)。
您可以使用 shift/add/subtract 以老式的方式來完成。 下面的代碼假設a
< n
和
n
< 2 63 (所以事情不會溢出):
uint64_t mulmod(uint64_t a, uint64_t b, uint64_t n) {
uint64_t rv = 0;
while (b) {
if (b&1)
if ((rv += a) >= n) rv -= n;
if ((a += a) >= n) a -= n;
b >>= 1; }
return rv;
}
你可以使用while (a && b)
如果它是可能的,而不是循環短路事情a
將是一個因素n
。 如果a
不是n
的因子,則會稍微慢一些(更多的比較和可能正確預測的分支)。
如果你真的,絕對需要最后一點(允許n
高達 2 64 -1),你可以使用:
uint64_t mulmod(uint64_t a, uint64_t b, uint64_t n) {
uint64_t rv = 0;
while (b) {
if (b&1) {
rv += a;
if (rv < a || rv >= n) rv -= n; }
uint64_t t = a;
a += a;
if (a < t || a >= n) a -= n;
b >>= 1; }
return rv;
}
或者,只需使用 GCC 內在函數來訪問底層 x64 指令:
inline uint64_t mulmod(uint64_t a, uint64_t b, uint64_t n) {
uint64_t rv;
asm ("mul %3" : "=d"(rv), "=a"(a) : "1"(a), "r"(b));
asm ("div %4" : "=d"(rv), "=a"(a) : "0"(rv), "1"(a), "r"(n));
return rv;
}
然而,64 位 div 指令確實很慢,因此循環實際上可能更快。 你需要配置文件才能確定。
7 年后,我得到了一個在 Visual Studio 2019 中工作的解決方案
#include <stdint.h>
#include <intrin.h>
#pragma intrinsic(_umul128)
#pragma intrinsic(_udiv128)
// compute (a*b)%n with 128-bit intermediary result
// assumes n>0 and a*b < n * 2**64 (always the case when a<=n || b<=n )
inline uint64_t mulmod(uint64_t a, uint64_t b, uint64_t n) {
uint64_t r, s = _umul128(a, b, &r);
(void)_udiv128(r, s, n, &r);
return r;
}
// compute (a*b)%n with 128-bit intermediary result
// assumes n>0, works including if a*b >= n * 2**64
inline uint64_t mulmod1(uint64_t a, uint64_t b, uint64_t n) {
uint64_t r, s = _umul128(a % n, b, &r);
(void)_udiv128(r, s, n, &r);
return r;
}
此內在函數名為__mul128
。
typedef unsigned long long BIG;
// handles only the "hard" case when high bit of n is set
BIG shl_mod( BIG v, BIG n, int by )
{
if (v > n) v -= n;
while (by--) {
if (v > (n-v))
v -= n-v;
else
v <<= 1;
}
return v;
}
現在你可以使用shl_mod(B, n, 64)
沒有內聯匯編有點糟糕。 不管怎樣,函數調用的開銷其實是非常小的。 參數在易失性寄存器中傳遞,不需要清理。
我沒有匯編器,而且 x64 目標不支持 __asm,所以我別無選擇,只能自己從操作碼“組裝”我的函數。
顯然這取決於 . 我使用 mpir (gmp) 作為參考來顯示函數產生正確的結果。
#include "stdafx.h"
// mulmod64(a, b, m) == (a * b) % m
typedef uint64_t(__cdecl *mulmod64_fnptr_t)(uint64_t a, uint64_t b, uint64_t m);
uint8_t mulmod64_opcodes[] = {
0x48, 0x89, 0xC8, // mov rax, rcx
0x48, 0xF7, 0xE2, // mul rdx
0x4C, 0x89, 0xC1, // mov rcx, r8
0x48, 0xF7, 0xF1, // div rcx
0x48, 0x89, 0xD0, // mov rax,rdx
0xC3 // ret
};
mulmod64_fnptr_t mulmod64_fnptr;
void init() {
DWORD dwOldProtect;
VirtualProtect(
&mulmod64_opcodes,
sizeof(mulmod64_opcodes),
PAGE_EXECUTE_READWRITE,
&dwOldProtect);
// NOTE: reinterpret byte array as a function pointer
mulmod64_fnptr = (mulmod64_fnptr_t)(void*)mulmod64_opcodes;
}
int main() {
init();
uint64_t a64 = 2139018971924123ull;
uint64_t b64 = 1239485798578921ull;
uint64_t m64 = 8975489368910167ull;
// reference code
mpz_t a, b, c, m, r;
mpz_inits(a, b, c, m, r, NULL);
mpz_set_ui(a, a64);
mpz_set_ui(b, b64);
mpz_set_ui(m, m64);
mpz_mul(c, a, b);
mpz_mod(r, c, m);
gmp_printf("(%Zd * %Zd) mod %Zd = %Zd\n", a, b, m, r);
// using mulmod64
uint64_t r64 = mulmod64_fnptr(a64, b64, m64);
printf("(%llu * %llu) mod %llu = %llu\n", a64, b64, m64, r64);
return 0;
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.