Чому pow (a, d, n) настільки швидше, ніж ** d% n?


110

Я намагався здійснити тест первинності Міллера-Рабіна і був спантеличений, чому для середніх чисел (~ 7 цифр) потрібно так довго (> 20 секунд). Зрештою, я знайшов наступний рядок коду як джерело проблеми:

x = a**d % n

(де a, dі nвсі вони схожі, але нерівні середні числа, **є оператором експоненції та %є оператором модуля)

Потім я спробував замінити його на таке:

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 набагато швидше ?

Відповіді:


164

Дивіться статтю у Вікіпедії про модульну експоненцію . В основному, коли ви робите 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).
докстринг

6
Залежно від вашої версії Python, це може бути правдою лише за певних умов. IIRC, в 3.x і 2.7, ви можете використовувати лише три аргументи з інтегральними типами (і негативною силою), і ви завжди отримаєте модульну експоненцію з нативним intтипом, але не обов'язково з іншими цілісними типами. Але в старих версіях існували правила щодо підключення до C long, три аргументованої форми було дозволено і floatт. Д. (Сподіваємось, ви не використовуєте 2.1 або більш ранню версію і не використовуєте будь-яких спеціальних інтегральних типів з модулів C, так що жодна про це має значення для вас.)
abarnert

13
З вашої відповіді здається, що компілятору неможливо побачити вираз та оптимізувати його, що не відповідає дійсності. Це просто трапляється , що ніякі поточні компіляторів Python не робити.
danielkza

5
@danielkza: Це правда, я не мав на увазі, що це теоретично неможливо. Можливо, "не дивиться в майбутнє" було б точніше, ніж "не бачу в майбутнє". Зауважте, що оптимізація може бути вкрай складною або взагалі неможливою. Для константних операндів це може бути оптимізовано, але в x ** y % n, xможе бути об'єктом, який реалізує __pow__і, спираючись на випадкове число, повертає один з декількох різних об'єктів, що реалізуються __mod__способами, які також залежать від випадкових чисел тощо.
BrenBarn

2
@danielkza: Крім того, функції не мають однакового домену: .3 ** .4 % .5цілком законно, але якщо компілятор перетворив це на pow(.3, .4, .5)те, воно повинно б створити a TypeError. Компілятор повинен був би бути в змозі знати про це a, dі nгарантовано є значеннями цілісного типу (а може бути, саме конкретно типу int, тому що перетворення не допомагає інакше), і dгарантовано буде негативним. Це те, що JIT міг би зробити, але статичний компілятор для мови з динамічними типами і без умовиводу просто не може.
abarnert

37

BrenBarn відповів на ваше головне питання. Для вашого боку:

чому це майже вдвічі швидше при запуску з Python 2 або 3, ніж PyPy, коли зазвичай PyPy набагато швидше?

Якщо ви читаєте сторінку продуктивності PyPy , це саме та річ, в якій PyPy не годиться - насправді це перший приклад, який вони дають:

Погані приклади включають проведення обчислень з великими довжинами - що виконується за допомогою неоптимізованого коду підтримки.

Теоретично перетворення величезної експоненції, що супроводжується модом, в модульну експоненцію (принаймні після першого проходження) - це перетворення, яке JIT може зробити…

Як зауваження, якщо вам потрібно робити обчислення з величезними цілими числами, ви можете подивитися на сторонні модулі на зразок gmpy, які іноді можуть бути набагато швидшими, ніж початкова реалізація CPython в деяких випадках за межами основного використання, а також має багато додаткової функціональності, яку вам інакше доведеться писати самостійно, ціною менш зручною.


2
довги виправились. спробуйте pypy 2.0 beta 1 (це не буде швидше, ніж CPython, але і не повинно бути повільніше). gmpy не має можливості впоратися з MemoryError :(
fijal

@fijal: Так, а gmpyтакож повільніше замість швидшого в кількох випадках, і робить багато простих речей менш зручними. Це не завжди відповідь, але іноді це так. Тож варто придивитися, якщо ви маєте справу з величезними цілими числами, а рідний тип Python не здається досить швидким.
abarnert

1
і якщо вам не байдуже, чи великі ваші цифри, ваша програма робить
segfault

1
Це фактор, який змусив PyPy довго не використовувати бібліотеку GMP. Для вас це може бути нормально, для розробників Python VM це не нормально. Малалок може вийти з ладу, не використовуючи багато оперативної пам’яті, просто поставте туди дуже велику кількість. Поведінка GMP з цього моменту не визначена, і Python не може цього дозволити.
фіджал

1
@fijal: Я повністю погоджуюся, що його не слід використовувати для впровадження вбудованого типу Python. Це не означає, що його ніколи не слід використовувати ні для чого.
abarnert

11

Є ярлики робити модульне зведення в ступінь: наприклад, ви можете знайти a**(2i) mod nдля кожного iз 1до log(d)і розмножуватися разом (мод n) проміжних результатів вам потрібно. Виділена функція модульної експоненціації, як 3-аргумент, pow()може використовувати такі хитрощі, оскільки вона знає, що ви виконуєте модульну арифметику. Аналізатор Python не може розпізнати це за голим виразом a**d % n, тому він виконає повний розрахунок (що займе набагато більше часу).


3

Шлях x = a**d % nрозрахований - підняти aна dпотужність, а потім по модулю, що з n. По-перше, якщо aвона велика, це створює величезну кількість, яке потім усікається. Однак він, x = pow(a, d, n)швидше за все, оптимізований так, що nвідслідковуються лише останні цифри, які є всіма необхідними для обчислення модуля множення на число.


6
"для обчислення х ** d потрібні d множення - не вірно. Ви можете це зробити у множині O (log d) (дуже широке). Експоненція методом квадратування може використовуватися без модуля. Тут є чіткий розмір мультиплікацій.
Джон Дворак

@JanDvorak Щоправда, я не впевнений, чому я думав, що python не буде використовувати той же алгоритм експоненціації, **як і для pow.
Юуші

5
Не останні "n" цифри .. він просто зберігає обчислення в Z / nZ.
Томас
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.