J'essayais d'implémenter un test de primalité de Miller-Rabin , et je ne savais pas pourquoi cela prenait autant de temps (> 20 secondes) pour les nombres moyens (~ 7 chiffres). J'ai finalement trouvé la ligne de code suivante à l'origine du problème:
x = a**d % n
(où a
, d
et n
sont tous des nombres moyens, mais inégaux, **
est l'opérateur d'exponentiation, et %
est l'opérateur modulo)
J'ai ensuite essayé de le remplacer par ce qui suit:
x = pow(a, d, n)
et par comparaison, il est presque instantané.
Pour le contexte, voici la fonction d'origine:
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 exemple de calcul chronométré:
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)})
Sortie (exécutée avec PyPy 1.9.0):
2642565
time: 23.785543s
2642565
time: 0.000030s
Sortie (exécutée avec Python 3.3.0, 2.7.2 renvoie des temps très similaires):
2642565
time: 14.426975s
2642565
time: 0.000021s
Et une question connexe, pourquoi ce calcul est-il presque deux fois plus rapide lorsqu'il est exécuté avec Python 2 ou 3 qu'avec PyPy, alors que généralement PyPy est beaucoup plus rapide ?
Voir l'article Wikipedia sur exponentiation modulaire . Fondamentalement, lorsque vous faites a**d % n
, vous devez en fait calculer a**d
, qui pourrait être assez volumineux. Mais il existe des moyens de calculer a**d % n
sans avoir à calculer a**d
lui-même, et c'est ce que fait pow
. Le **
L'opérateur ne peut pas faire cela car il ne peut pas "voir dans le futur" pour savoir que vous allez prendre immédiatement le module.
BrenBarn a répondu à votre question principale. Pour votre aparté:
pourquoi est-il presque deux fois plus rapide lorsqu'il est exécuté avec Python 2 ou 3 que PyPy, alors que PyPy est généralement beaucoup plus rapide?
Si vous lisez PyPy page de performance , c'est exactement le genre de choses que PyPy n'est pas bon - en fait, le tout premier exemple qu'ils donnent:
Les mauvais exemples incluent faire des calculs avec des longs longs - qui sont effectués par du code de support non optimisable.
Théoriquement, transformer une énorme exponentiation suivie d'un mod en une exponentiation modulaire (au moins après la première passe) est une transformation qu'un JIT pourrait être en mesure de faire… mais pas le JIT de PyPy.
En guise de remarque, si vous avez besoin de faire des calculs avec des entiers énormes, vous voudrez peut-être regarder des modules tiers comme gmpy
, qui peuvent parfois être beaucoup plus rapides que l'implémentation native de CPython dans certains cas, en dehors des utilisations courantes, et dispose également de nombreuses fonctionnalités supplémentaires que vous auriez autrement à écrire vous-même, au prix d'être moins pratiques.
Il existe des raccourcis pour faire une exponentiation modulaire: par exemple, vous pouvez trouver a**(2i) mod n
pour chaque i
de 1
À log(d)
et multiplier ensemble (mod n
) les résultats intermédiaires dont vous avez besoin. Une fonction d'exponentiation modulaire dédiée telle que pow()
à 3 arguments peut tirer parti de ces astuces car elle sait que vous faites de l'arithmétique modulaire. L'analyseur Python ne peut pas le reconnaître étant donné l'expression nue a**d % n
, Il effectuera donc le calcul complet (ce qui prendra beaucoup plus de temps).
La façon dont x = a**d % n
Est calculée consiste à augmenter a
à la puissance d
, puis à moduler cela avec n
. Premièrement, si a
est grand, cela crée un nombre énorme qui est ensuite tronqué. Cependant, x = pow(a, d, n)
est très probablement optimisé afin que seuls les derniers n
chiffres soient suivis, ce qui est tout ce qui est nécessaire pour calculer la multiplication modulo d'un nombre.