这个问题源于我几乎写在这个问题下面的评论,其中 Zack 正在计算大量模数的阶乘(为了这个问题,我们将假设它是素数)。Zack 使用传统的阶乘计算,在每次乘法时取余数。
我几乎评论说要考虑的替代方案是Montgomery multiplication,但仔细想想,我只看到这种技术用于加速同一个被乘数的多次乘法(特别是加快n mod p 的计算)。
我的问题是:蒙哥马利乘法可以用来加速n的计算!大 n 和 p 的 mod p?
这个问题源于我几乎写在这个问题下面的评论,其中 Zack 正在计算大量模数的阶乘(为了这个问题,我们将假设它是素数)。Zack 使用传统的阶乘计算,在每次乘法时取余数。
我几乎评论说要考虑的替代方案是Montgomery multiplication,但仔细想想,我只看到这种技术用于加速同一个被乘数的多次乘法(特别是加快n mod p 的计算)。
我的问题是:蒙哥马利乘法可以用来加速n的计算!大 n 和 p 的 mod p?
天真地,不;您需要将乘积的 n 项中的每一项转换为“蒙哥马利空间”,因此您有 n 个完全约简 mod m,与“通常”算法相同。
然而,阶乘不仅仅是 n 项的任意乘积;它更有条理。特别是,如果你已经有了“蒙哥马化” kr mod m
,那么你可以用一个非常便宜的减价来获得(k+1)r mod m
。
所以这是完全可行的,虽然我以前没见过。我继续写了一个快速而肮脏的实现(非常未经测试,我根本不会相信它):
// returns m^-1 mod 2**64 via clever 2-adic arithmetic (http://arxiv.org/pdf/1209.6626.pdf)
uint64_t inverse(uint64_t m) {
assert(m % 2 == 1);
uint64_t minv = 2 - m;
uint64_t m_1 = m - 1;
for (int i=1; i<6; i+=1) { m_1 *= m_1; minv *= (1 + m_1); }
return minv;
}
uint64_t montgomery_reduce(__uint128_t x, uint64_t minv, uint64_t m) {
return x + (__uint128_t)((uint64_t)x*-minv)*m >> 64;
}
uint64_t montgomery_multiply(uint64_t x, uint64_t y, uint64_t minv, uint64_t m) {
return montgomery_reduce(full_product(x, y), minv, m);
}
uint64_t montgomery_factorial(uint64_t x, uint64_t m) {
assert(x < m && m % 2 == 1);
uint64_t minv = inverse(m); // m^-1 mod 2**64
uint64_t r_mod_m = -m % m; // 2**64 mod m
uint64_t mont_term = r_mod_m;
uint64_t mont_result = r_mod_m;
for (uint64_t k=2; k<=x; k++) {
// Compute the montgomerized product term: kr mod m = (k-1)r + r mod m.
mont_term += r_mod_m;
if (mont_term >= m) mont_term -= m;
// Update the result by multiplying in the new term.
mont_result = montgomery_multiply(mont_result, mont_term, minv, m);
}
// Final reduction
return montgomery_reduce(mont_result, minv, m);
}
并将其与通常的实现进行基准测试:
__uint128_t full_product(uint64_t x, uint64_t y) {
return (__uint128_t)x*y;
}
uint64_t naive_factorial(uint64_t x, uint64_t m) {
assert(x < m);
uint64_t result = x ? x : 1;
while (x --> 2) result = full_product(result,x) % m;
return result;
}
并针对使用一些内联汇编的通常实现来解决轻微的低效率问题:
uint64_t x86_asm_factorial(uint64_t x, uint64_t m) {
assert(x < m);
uint64_t result = x ? x : 1;
while (x --> 2) {
__asm__("mov %[result], %%rax; mul %[x]; div %[m]"
: [result] "+d" (result) : [x] "r" (x), [m] "r" (m) : "%rax", "flags");
}
return result;
}
对于相当大的 x,在我的 Haswell 笔记本电脑上的结果如下:
implementation speedup
---------------------------
naive 1.00x
x86_asm 1.76x
montgomery 5.68x
所以这看起来确实是一场不错的胜利。蒙哥马利实现的代码生成相当不错,但也可以通过手写汇编进一步改进。
对于“适度”的 x 和 m,这是一种有趣的方法。一旦 x 变大,在 x 中具有次线性复杂度的各种方法必然会胜出;阶乘具有如此多的结构,这种方法没有利用。