Estaba tratando de implementar una prueba de primalidad Miller-Rabin y me sorprendió por qué tardaba tanto (> 20 segundos) en números medianos (~ 7 dígitos). Finalmente encontré que la siguiente línea de código es la fuente del problema:
x = a**d % n
(donde a
,, d
y n
son todos similares, pero desiguales, números medianos, **
es el operador de exponenciación y %
es el operador de módulo)
Luego intenté reemplazarlo con lo siguiente:
x = pow(a, d, n)
y en comparación es casi instantáneo.
Para el contexto, aquí está la función original:
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))
Un ejemplo de cálculo cronometrado:
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)})
Salida (ejecutar con PyPy 1.9.0):
2642565
time: 23.785543s
2642565
time: 0.000030s
Salida (ejecutar con Python 3.3.0, 2.7.2 devuelve tiempos muy similares):
2642565
time: 14.426975s
2642565
time: 0.000021s
Y una pregunta relacionada, ¿por qué este cálculo es casi dos veces más rápido cuando se ejecuta con Python 2 o 3 que con PyPy, cuando normalmente PyPy es mucho más rápido ?
fuente
>>> 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
tipo nativo , pero no necesariamente con otros tipos integrales. Pero en versiones anteriores había reglas sobre cómo encajar en una Clong
, se permitía la forma de tres argumentosfloat
, etc. (Con suerte, no está usando 2.1 o anterior, y no está usando ningún tipo integral personalizado de módulos C, por lo que ninguno de esto es importante para usted.)x ** y % n
,x
podría ser un objeto que implementa__pow__
y, en base a un número aleatorio, devuelve uno de varios objetos diferentes de aplicación__mod__
de manera que también dependen de números aleatorios, etc..3 ** .4 % .5
es perfectamente legal, pero si el compilador lo transforma enpow(.3, .4, .5)
eso generaría unTypeError
. El compilador debería poder saber esoa
,d
yn
se garantiza que son valores de un tipo integral (o tal vez solo específicamente de tipoint
, porque la transformación no ayuda de otra manera), yd
se garantiza que no serán negativos. Eso es algo que un JIT podría hacer, pero un compilador estático para un lenguaje con tipos dinámicos y sin inferencia simplemente no puede.BrenBarn respondió a su pregunta principal. Para tu aparte:
Si lee la página de rendimiento de PyPy , este es exactamente el tipo de cosas en las que PyPy no es bueno; de hecho, el primer ejemplo que dan:
Teóricamente, convertir una exponenciación enorme seguida de un mod en una exponenciación modular (al menos después de la primera pasada) es una transformación que un JIT podría hacer ... pero no el JIT de PyPy.
Como nota al margen, si necesita hacer cálculos con números enteros grandes, es posible que desee mirar módulos de terceros como
gmpy
, que a veces puede ser mucho más rápido que la implementación nativa de CPython en algunos casos fuera de los usos principales, y también tiene mucho de funcionalidad adicional que de otro modo tendría que escribir usted mismo, a costa de ser menos conveniente.fuente
gmpy
también es más lento en lugar de más rápido en algunos casos, y hace que muchas cosas simples sean menos convenientes. No siempre es la respuesta, pero a veces lo es. Por lo tanto, vale la pena analizarlo si se trata de números enteros grandes y el tipo nativo de Python no parece lo suficientemente rápido.Hay atajos para hacer una exponenciación modular: por ejemplo, puede encontrar
a**(2i) mod n
para cadai
from1
tolog(d)
y multiplicar (modn
) los resultados intermedios que necesita. Una función de exponenciación modular dedicada como 3 argumentospow()
puede aprovechar estos trucos porque sabe que estás haciendo aritmética modular. El analizador de Python no puede reconocer esto dada la expresión simplea**d % n
, por lo que realizará el cálculo completo (que llevará mucho más tiempo).fuente
La forma en que
x = a**d % n
se calcula es subira
a lad
potencia, luego modulo eso conn
. En primer lugar, sia
es grande, crea un número enorme que luego se trunca. Sin embargo,x = pow(a, d, n)
lo más probable es que esté optimizado para que solon
se rastreen los últimos dígitos, que son todo lo que se requiere para calcular el módulo de multiplicación de un número.fuente
**
parapow
.