Tại sao pow (a, d, n) nhanh hơn rất nhiều so với a ** d% n?


110

Tôi đang cố gắng thực hiện kiểm tra tính nguyên thủy Miller-Rabin và không hiểu tại sao lại mất quá nhiều thời gian (> 20 giây) cho các số cỡ trung bình (~ 7 chữ số). Cuối cùng tôi đã tìm thấy dòng mã sau là nguồn gốc của vấn đề:

x = a**d % n

(trong đó a, dntất cả đều giống nhau, nhưng không bằng nhau, các số cỡ trung bình, **là toán tử lũy thừa và %là toán tử mô đun)

Sau đó, tôi đã thử thay thế nó bằng những thứ sau:

x = pow(a, d, n)

và so sánh thì nó gần như tức thời.

Đối với ngữ cảnh, đây là hàm gốc:

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))

Một ví dụ về tính thời gian:

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)})

Đầu ra (chạy với PyPy 1.9.0):

2642565
time: 23.785543s
2642565
time: 0.000030s

Đầu ra (chạy với Python 3.3.0, 2.7.2 trả về thời gian rất giống nhau):

2642565
time: 14.426975s
2642565
time: 0.000021s

Và một câu hỏi liên quan, tại sao phép tính này nhanh hơn gần như gấp đôi khi chạy với Python 2 hoặc 3 so với PyPy, trong khi thường thì PyPy nhanh hơn nhiều ?

Câu trả lời:


164

Xem bài viết trên Wikipedia về lũy thừa mô-đun . Về cơ bản, khi bạn làm a**d % n, bạn thực sự phải tính toán a**d, có thể là khá lớn. Nhưng có những cách tính toán a**d % nmà không cần phải a**dtự tính toán , và đó là những gì powcó. Nhà **điều hành không thể làm điều này bởi vì nó không thể "nhìn thấy tương lai" để biết rằng bạn sẽ ngay lập tức áp dụng mô-đun.


14
1 đó là thực sự những gì docstring ngụ ý>>> 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

6
Tùy thuộc vào phiên bản Python của bạn, điều này có thể chỉ đúng trong một số điều kiện nhất định. IIRC, trong 3.x và 2.7, bạn chỉ có thể sử dụng dạng ba đối số với kiểu tích phân (và lũy thừa không âm), và bạn sẽ luôn nhận được lũy thừa mô-đun với intkiểu nguyên , nhưng không nhất thiết với các kiểu tích phân khác. Nhưng trong các phiên bản cũ hơn, có các quy tắc về việc phù hợp với C long, biểu mẫu ba đối số được cho phép float, v.v. (Hy vọng rằng bạn không sử dụng 2.1 hoặc phiên bản cũ hơn và không sử dụng bất kỳ kiểu tích phân tùy chỉnh nào từ mô-đun C, vì vậy không có điều này quan trọng đối với bạn).
abarnert

13
Từ câu trả lời của bạn, có vẻ như trình biên dịch không thể nhìn thấy biểu thức và tối ưu hóa nó, điều này không đúng. Nó chỉ xảy ra rằng không có trình biên dịch Python hiện tại nào làm điều đó.
danielkza

5
@danielkza: Đúng vậy, tôi không có ý ám chỉ điều đó về mặt lý thuyết là không thể. Có lẽ "không nhìn vào tương lai" sẽ chính xác hơn là "không thể nhìn vào tương lai". Tuy nhiên, lưu ý rằng nói chung việc tối ưu hóa có thể cực kỳ khó khăn hoặc thậm chí là không thể. Đối với hằng số toán hạng nó có thể được tối ưu hóa, nhưng trong x ** y % n, xcó thể là một đối tượng mà cụ __pow__và dựa trên một số ngẫu nhiên, trả về một trong những đối tượng khác nhau thực hiện __mod__theo những cách mà còn phụ thuộc vào số ngẫu nhiên, vv
BrenBarn

2
@danielkza: Ngoài ra, các hàm không có cùng miền: .3 ** .4 % .5là hoàn toàn hợp pháp, nhưng nếu trình biên dịch chuyển đổi nó thành pow(.3, .4, .5)điều đó sẽ tăng a TypeError. Trình biên dịch sẽ phải biết điều đó a, dnđược đảm bảo là các giá trị của kiểu tích phân (hoặc có thể chỉ là kiểu cụ thể int, bởi vì việc chuyển đổi không giúp được gì khác) và dđược đảm bảo là không âm. Đó là điều mà một JIT có thể làm được, nhưng một trình biên dịch tĩnh cho một ngôn ngữ có kiểu động và không có suy luận thì không thể.
abarnert

37

BrenBarn đã trả lời câu hỏi chính của bạn. Dành cho bạn:

tại sao khi chạy với Python 2 hoặc 3 lại nhanh hơn gần như gấp đôi so với PyPy, trong khi thường thì PyPy nhanh hơn nhiều?

Nếu bạn đọc trang hiệu suất của PyPy , đây chính xác là thứ PyPy không giỏi — trên thực tế, ví dụ đầu tiên mà họ đưa ra:

Các ví dụ xấu bao gồm thực hiện các phép tính với độ dài lớn - được thực hiện bởi mã hỗ trợ không thể tối ưu hóa.

Về mặt lý thuyết, biến một phép lũy thừa khổng lồ theo sau một mô-đun thành một phép lũy thừa mô-đun (ít nhất là sau lần vượt qua đầu tiên) là một phép biến đổi mà JIT có thể thực hiện… nhưng không phải JIT của PyPy.

Lưu ý thêm, nếu bạn cần thực hiện các phép tính với các số nguyên lớn, bạn có thể muốn xem xét các mô-đun của bên thứ ba gmpy, đôi khi có thể nhanh hơn nhiều so với triển khai gốc của CPython trong một số trường hợp ngoài mục đích sử dụng chính và cũng có rất nhiều chức năng bổ sung mà nếu không bạn phải tự viết, với chi phí là kém tiện lợi hơn.


2
lâu đã được sửa. hãy thử pypy 2.0 beta 1 (nó sẽ không nhanh hơn CPython, nhưng cũng không được chậm hơn). gmpy không có cách nào để xử lý MemoryError :(
fijal

@fijal: Vâng, và gmpycũng chậm hơn thay vì nhanh hơn trong một vài trường hợp, và khiến nhiều thứ đơn giản trở nên kém tiện lợi hơn. Nó không phải lúc nào cũng là câu trả lời - nhưng đôi khi nó là như vậy. Vì vậy, nó đáng xem xét nếu bạn đang xử lý các số nguyên lớn và kiểu gốc của Python có vẻ không đủ nhanh.
abarnert

1
và nếu bạn không quan tâm liệu số lượng của bạn lớn có làm cho chương trình của bạn không hoạt động hay không
fijal

1
Đó là yếu tố khiến PyPy không sử dụng thư viện GMP trong suốt thời gian dài. Nó có thể ổn đối với bạn, nó không ổn đối với các nhà phát triển máy ảo Python. Malloc có thể bị lỗi nếu không sử dụng nhiều RAM, chỉ cần đặt một số lượng rất lớn ở đó. Hành vi của GMP từ thời điểm đó trở đi là không xác định và Python không thể cho phép điều này.
fijal

1
@fijal: Tôi hoàn toàn đồng ý rằng nó không nên được sử dụng để triển khai kiểu tích hợp sẵn của Python. Điều đó không có nghĩa là nó không bao giờ nên được sử dụng cho bất cứ điều gì.
abarnert

11

Có những phím tắt để thực hiện phép tính lũy thừa mô-đun: ví dụ, bạn có thể tìm a**(2i) mod ncho mọi itừ 1đến log(d)và nhân với nhau (mod n) các kết quả trung gian mà bạn cần. Một hàm lũy thừa mô-đun chuyên dụng như 3-đối số pow()có thể tận dụng các thủ thuật như vậy vì nó biết bạn đang thực hiện phép tính mô-đun. Trình phân tích cú pháp Python không thể nhận ra điều này với biểu thức trần a**d % n, vì vậy nó sẽ thực hiện tính toán đầy đủ (sẽ mất nhiều thời gian hơn).


3

Cách x = a**d % nđược tính là nâng alên dsức mạnh, sau đó modulo đó với n. Thứ nhất, nếu alớn, điều này tạo ra một con số khổng lồ sau đó bị cắt bớt. Tuy nhiên, x = pow(a, d, n)rất có thể được tối ưu hóa để chỉ các nchữ số cuối cùng được theo dõi, là tất cả những gì được yêu cầu để tính toán mô đun nhân một số.


6
"nó yêu cầu d phép nhân để tính x ** d" - không đúng. Bạn có thể làm điều đó trong phép nhân O (log d) (rất rộng). Có thể sử dụng lũy ​​thừa theo bình phương mà không cần mô-đun. Kích thước tuyệt đối của các bội số và là yếu tố dẫn đầu ở đây.
John Dvorak

@JanDvorak Đúng, tôi không chắc tại sao tôi nghĩ python sẽ không sử dụng cùng một thuật toán lũy thừa đối **với pow.
Yuushi

5
Không phải chữ số "n" cuối cùng .. nó chỉ giữ các phép tính trong Z / nZ.
Thomas
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.