Why is pow(a, d, n) so much faster than a**d % n?

我当时正在尝试实现一个 Miller-Rabin primality test,我很困惑为什么对于中等大小的数字(大约7位数字)要花这么长的时间(> 20秒)。我最终发现下面这行代码是问题的根源:

x = a**d % n

(where a, d, and n are all similar, but unequal, midsize numbers, ** is the exponentiation operator, and % is the modulo operator)

然后我试着用下面的代替它:

x = pow(a, d, n)

相比之下,它几乎是瞬间的。

关于上下文,以下是最初的功能:

from random import randint


def primalityTest(n, k):
if n < 2:
return False
if n % 2 == 0:
return False
s = 0
d = n - 1
while d % 2 == 0:
s += 1
d >>= 1
for i in range(k):
rand = randint(2, n - 2)
x = rand**d % n         # offending line
if x == 1 or x == n - 1:
continue
for r in range(s):
toReturn = True
x = pow(x, 2, n)
if x == 1:
return False
if x == n - 1:
toReturn = False
break
if toReturn:
return False
return True


print(primalityTest(2700643,1))

一个定时计算的例子:

from timeit import timeit


a = 2505626
d = 1520321
n = 2700643


def testA():
print(a**d % n)


def testB():
print(pow(a, d, n))


print("time: %(time)fs" % {"time":timeit("testA()", setup="from __main__ import testA", number=1)})
print("time: %(time)fs" % {"time":timeit("testB()", setup="from __main__ import testB", number=1)})

输出(使用 PyPy 1.9.0运行) :

2642565
time: 23.785543s
2642565
time: 0.000030s

输出(使用 Python 3.3.0运行,2.7.2返回非常相似的时间) :

2642565
time: 14.426975s
2642565
time: 0.000021s

还有一个相关的问题,为什么当使用 Python2或3运行时,这个计算的速度几乎是 PyPy 的两倍,而 PyPy 通常是 快多了

18172 次浏览

参见关于 模幂运算的维基百科文章。基本上,当你做 a**d % n,你实际上必须计算 a**d,这可能是相当大的。但是有一些方法可以计算 a**d % n而不必计算 a**d本身,这就是 pow所做的。**操作符不能这样做,因为它不能“预见未来”,知道你将立即采取模。

做模幂运算有一些捷径: 例如,你可以找到从 1log(d)的每个 ia**(2i) mod n,然后把你需要的中间结果相乘(mod n)。一个专用的模幂函数,比如3参数的 pow(),可以利用这些技巧,因为它知道你在做同余关系。Python 解析器无法识别这一点,因为只有表达式 a**d % n,所以它将执行完整的计算(这将花费更长的时间)。

The way x = a**d % n is calculated is to raise a to the d power, then modulo that with n. Firstly, if a is large, this creates a huge number which is then truncated. However, x = pow(a, d, n) is most likely optimized so that only the last n digits are tracked, which are all that are required for calculating multiplication modulo a number.

布伦巴恩回答了你的主要问题:

why is it almost twice as fast when run with Python 2 or 3 than PyPy, when usually PyPy is much faster?

如果你读过 PyPy 的 性能页,你会发现这正是 PyPy 所不擅长的ーー事实上,他们给出的第一个例子就是:

不好的例子包括使用大长度进行计算-这是由不可优化的支持代码执行的。

从理论上讲,将一个巨大的指数运算后跟一个 mod 运算转换成模数运算(至少在第一次运算之后)是一个 JIT 可能能够实现的转换... ... 但 PyPy 的 JIT 不能。

顺便说一句,如果你需要使用大整数进行计算,你可能想看看像 gmpy这样的第三方模块,它有时比 CPython 的本机实现在某些情况下在主流用法之外快得多,而且还有很多额外的功能,你不得不自己编写,代价是不方便。