为什么pow(a,d,n)比a ** d%n快得多?


110

我正在尝试实施Miller-Rabin素数测试,并对为什么中号(〜7位数)要花这么长时间(> 20秒)感到困惑。我最终发现以下代码行是问题的根源:

x = a**d % n

(其中adn都是相似的,但不相等的中号,**是幂运算符,并且%是模运算符)

然后,我尝试将其替换为以下内容:

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

还有一个相关的问题,为什么使用Python 2或3运行时,这种计算几乎比使用PyPy时快两倍,而通常PyPy却要快得多

Answers:


164

请参阅Wikipedia上有关模幂的文章。基本上,当您这样做时a**d % n,实际上必须计算a**d,这可能会很大。但是有些计算方法a**d % n不必自己计算a**d,这就是pow它的作用。该**运营商不能做到这一点,因为它不能“预见未来”知道你要立即采取模数。


14
+1实际上就是文档字符串所隐含的含义>>> print pow.__doc__ pow(x, y[, z]) -> number With two arguments, equivalent to x**y. With three arguments, equivalent to (x**y) % z, but may be more efficient (e.g. for longs).
Hedde van der Heide 2013年

6
根据您的Python版本,这仅在某些情况下可能是正确的。IIRC在3.x和2.7中,只能将三参数形式与整数类型(和非负幂)一起使用,并且始终会使用本机int类型获得模幂,而不必使用其他整数类型。但是在较旧的版本中,存在关于适合C的规则long,允许使用三参数形式float,等等。(希望您没有使用2.1或更早的版本,并且没有使用C模块中的任何自定义整数类型,因此没有这对您很重要。)
abarnert 2013年

13
从您的答案看来,编译器似乎无法查看表达式并对其进行优化,但这是不正确的。这恰好没有电流的Python编译器做。
danielkza

5
@danielkza:是的,我的意思并不是说理论上是不可能的。也许“不展望未来”比“不展望未来”更为准确。但是请注意,优化通常可能非常困难,甚至不可能。对于操作数它可以优化,但在x ** y % nx可能是一个对象实现__pow__,并根据随机数,返回一个实现几种不同的对象之一__mod__的方式,这也依赖于随机数等
BrenBarn

2
@danielkza:而且,这些函数没有相同的域:.3 ** .4 % .5完全合法,但是如果编译器将其转换pow(.3, .4, .5)为该域,则会引发TypeError。编译器必须必须知道,ad,并且n必须保证是整数类型的值(或者可能只是特定于type的值int,因为转换对其他情况无济于事),并且d必须保证是非负数。可以想象,JIT可以做到这一点,但是静态编译器无法处理具有动态类型且没有推断的语言。
abarnert

37

BrenBarn回答了您的主要问题。除了您:

为什么用Python 2或3运行时,它的速度几乎是PyPy的两倍,而通常PyPy要快得多?

如果您阅读了PyPy的性能页面,这正是PyPy不擅长的事情-实际上,他们给出的第一个示例是:

不良的例子包括进行大量的计算-这是由无法优化的支持代码执行的。

从理论上讲,将巨大的幂乘以Mod转换为模块化幂(至少在第一遍之后)是JIT可以实现的一种转换,但不是PyPy的JIT。

附带说明一下,如果您需要使用巨大的整数进行计算,则可能需要查看第三方模块,例如gmpy,在某些情况下,它有时会比CPython的本机实现快得多,在某些主流用途之外,并且也有很多用途。否则,您将不得不编写自己的其他功能,而代价是不太方便。


2
多头固定。尝试pypy 2.0 beta 1(它不会比CPython快,但也不应慢)。gmpy没有办法处理MemoryError :(
FIJAL 2013年

@fijal:是的,而且gmpy在某些情况下也较慢而不是更快,并且使许多简单的事情变得不太方便。这并不总是答案,但有时是答案。因此,如果您要处理巨大的整数并且Python的本机类型似乎不够快,则值得一看。
abarnert 2013年

1
如果您不在乎您的人数是否会
导致

1
这就是PyPy长时间不使用GMP库的原因。对您来说可能没事,对于Python VM开发人员来说则不行。如果不使用大量RAM,则malloc可能会失败,只需在其中放置大量RAM。从那时起,GMP的行为是不确定的,Python不允许这样做。
2013年

1
@fijal:我完全同意不应将其用于实现Python的内置类型。这并不意味着它永远都不应被用于任何用途。
abarnert

11

进行模幂运算有一些捷径:例如,您可以找到从到的a**(2i) mod n每个,并将所需的中间结果相乘(mod )。专用的模幂函数(例如3参数)可以利用这些技巧,因为它知道您正在执行模数运算。Python解析器无法识别给定的裸表达式,因此它将执行完整的计算(这将花费更长的时间)。i1log(d)npow()a**d % n


3

x = a**d % n计算的方法是提高功率,然后用adn。首先,如果a很大,则会创建一个巨大的数字,然后将其截短。但是,x = pow(a, d, n)最有可能进行了优化,以便仅n跟踪最后一位,这是计算以模为模的乘法所需的全部数字。


6
“它需要d个乘法才能计算出x ** d” –不正确。您可以使用O(log d)(非常宽)乘法来完成。平方运算可以在没有模块的情况下使用。被乘数的绝对大小是导致这里领先的原因。
约翰·德沃夏克

@JanDvorak诚然,我不知道为什么我认为蟒蛇不会使用相同幂算法**作为pow
Yuushi

5
不是最后的“ n”个数字..它只是将计算保持在Z / nZ中。
2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.