Я пытался реализовать тест на простоту Миллера-Рабина и был озадачен, почему это занимает так много времени (> 20 секунд) для чисел среднего размера (~ 7 цифр). В конце концов я обнаружил, что источником проблемы является следующая строка кода:
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 намного быстрее ?
источник
>>> 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).
int
типом, но не обязательно с другими целочисленными типами. Но в более старых версиях существовали правила встраивания в Clong
, разрешалась форма с тремя аргументамиfloat
и т. Д. (Надеюсь, вы не используете 2.1 или более раннюю версию и не используете какие-либо пользовательские интегральные типы из модулей C, поэтому ни один это важно для вас.)x ** y % n
,x
может быть объектом , который реализует__pow__
и, основываясь на случайном числе, возвращает один из нескольких различных объектов , реализующих__mod__
таким образом , что также зависит от случайных чисел и т.д..3 ** .4 % .5
это совершенно законно, но если компилятор преобразует это вpow(.3, .4, .5)
это, вызовет файлTypeError
. Компилятор должен знать, чтоa
,d
иn
гарантированно будут значениями целочисленного типа (или, может быть, просто определенного типаint
, потому что в противном случае преобразование не поможет), иd
гарантированно будет неотрицательным. Это то, что JIT предположительно может сделать, но статический компилятор для языка с динамическими типами и без вывода просто не может.БренБарн ответил на ваш главный вопрос. Для вас:
Если вы читаете страницу производительности PyPy , это именно то, в чем PyPy не силен - по сути, самый первый пример, который они приводят:
Теоретически превращение огромного возведения в степень, за которым следует мод, в модульное возведение в степень (по крайней мере, после первого прохода) - это преобразование, которое JIT могла бы выполнить ... но не JIT PyPy.
В качестве побочного примечания, если вам нужно выполнять вычисления с огромными целыми числами, вы можете посмотреть на сторонние модули, например
gmpy
, которые иногда могут быть намного быстрее, чем собственная реализация CPython в некоторых случаях за пределами основного использования, а также имеет много дополнительных функций, которые в противном случае вам пришлось бы писать самостоятельно, но это было бы менее удобно.источник
gmpy
в некоторых случаях он работает медленнее, чем быстрее, и делает многие простые вещи менее удобными. Это не всегда ответ, но иногда это так. Так что на это стоит обратить внимание, если вы имеете дело с огромными целыми числами, а собственный тип Python кажется недостаточно быстрым.Есть ярлыки для выполнения модульного возведения в степень: например, вы можете найти
a**(2i) mod n
для каждогоi
от1
доlog(d)
и умножить вместе (modn
) нужные вам промежуточные результаты. Специальная функция модульного возведения в степень, такая как 3-аргумент,pow()
может использовать такие уловки, потому что знает, что вы выполняете модульную арифметику. Парсер Python не может распознать это по голому выражениюa**d % n
, поэтому он выполнит полное вычисление (что займет гораздо больше времени).источник
x = a**d % n
Рассчитывается способ возвестиa
вd
степень, а затем по модулюn
. Во-первых, еслиa
он большой, это создает огромное число, которое затем обрезается. Однако,x = pow(a, d, n)
скорее всего, оптимизирован так, чтоn
отслеживаются только последние цифры, а это все, что требуется для вычисления умножения по модулю числа.источник
**
что и дляpow
.