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

lya*_*per 110 python performance pypy

我试图实施Miller-Rabin素性测试,并且很困惑为什么中等数字(~7位数)需要这么长时间(> 20秒).我最终发现以下代码行是问题的根源:

x = a**d % n
Run Code Online (Sandbox Code Playgroud)

(where a,dn都是相似的,但是不相等的,中等数字,**是取幂运算符,并且%是模运算符)

然后我尝试用以下内容替换它:

x = pow(a, d, n)
Run Code Online (Sandbox Code Playgroud)

相比之下它几乎是瞬间完成的.

对于上下文,这是原始函数:

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))
Run Code Online (Sandbox Code Playgroud)

示例定时计算:

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)})
Run Code Online (Sandbox Code Playgroud)

输出(使用PyPy 1.9.0运行):

2642565
time: 23.785543s
2642565
time: 0.000030s
Run Code Online (Sandbox Code Playgroud)

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

2642565
time: 14.426975s
2642565
time: 0.000021s
Run Code Online (Sandbox Code Playgroud)

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

Bre*_*arn 163

请参阅维基百科有关模幂运算的文章.基本上,当你这样做时a**d % n,你实际上必须计算a**d,这可能是非常大的.但是有一些计算方法,a**d % n而不必计算a**d自己,这就是做什么的pow.该**运营商不能做到这一点,因为它不能"预见未来"知道你要立即采取模数.

  • +1这实际上是文档字符串所暗示的`>>> print pow .__ doc__ pow(x,y [,z]) - > number有两个参数,相当于x**y.使用三个参数,相当于(x**y)%z,但可能更有效(例如,对于longs).` (14认同)
  • 从你的回答看来,编译器似乎不可能看到表达式并对其进行优化,但事实并非如此.*只是发生*没有当前的Python编译器这样做. (13认同)
  • 根据您的Python版本,这可能仅在某些条件下才是真实的.IIRC,在3.x和2.7中,你只能使用具有整数类型(和非负幂)的三参数形式,并且你将始终使用本机`int`类型进行模幂运算,但不一定使用其他积分类型.但是在旧版本中有关于适合C`long`的规则,三个参数形式允许使用`float`等等.(希望你没有使用2.1或更早版本,并且没有使用任何自定义整数类型来自C模块,所以这些都不重要.) (6认同)
  • @danielkza:这是真的,我并不是故意暗示它在理论上是不可能的.也许"不展望未来"会比"看不到未来"更准确.但请注意,一般来说,优化可能非常困难甚至是不可能的.对于*constant*操作数,它可以被优化,但是在`x**y%n`中,`x`可以是实现`__pow__`的对象,并且基于随机数,返回实现`__mod__的几个不同对象之一`在某些方面也取决于随机数等 (5认同)
  • @danielkza:而且,这些函数没有相同的域:`.3 ** .4%.5`完全合法,但是如果编译器将其转换为`pow(.3,.4,.5)`那会引发`TypeError`。编译器必须能够知道保证a,d和n是整数类型的值(或者可能只是专门为int类型的值,因为否则转换无济于事。 ),并且保证d为非负数。可以想象,这是JIT可以做到的,但是对于具有动态类型并且没有推断的语言来说,静态编译器是行不通的。 (2认同)

aba*_*ert 37

BrenBarn回答了你的主要问题.对你而言:

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

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

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

从理论上讲,将一个巨大的取幂,然后将mod转换为模幂运算(至少在第一次通过之后)是JIT可能做出的转换......但不是PyPy的JIT.

作为旁注,如果您需要使用大整数进行计算,您可能需要查看第三方模块gmpy,在某些情况下,在主流使用之外,有时可能比CPython的本机实现快得多,而且还有很多您不得不自己编写的附加功能,但代价是不方便.

  • 渴望得到解决.尝试pypy 2.0 beta 1(它不会比CPython快,但也不应该慢).gmpy没有办法处理MemoryError :( (2认同)

ato*_*inf 11

有快捷方式做模幂:例如,你可以找到a**(2i) mod n每一个i1log(d)繁衍起来(MOD n),你需要的中间结果.像3参数pow()这样的专用模幂运算函数可以利用这些技巧,因为它知道你正在进行模运算.给定裸表达式时a**d % n,Python解析器无法识别它,因此它将执行完整计算(这将花费更长时间).