Perché pow (a, d, n) è molto più veloce di a ** d% n?


110

Stavo cercando di implementare un test di primalità di Miller-Rabin e sono rimasto perplesso sul motivo per cui ci volesse così tanto tempo (> 20 secondi) per i numeri di medie dimensioni (~ 7 cifre). Alla fine ho scoperto che la seguente riga di codice era l'origine del problema:

x = a**d % n

(dove a, de nsono tutti numeri medi simili, ma disuguali, **è l'operatore di esponenziazione ed %è l'operatore modulo)

Ho quindi provato a sostituirlo con il seguente:

x = pow(a, d, n)

e in confronto è quasi istantaneo.

Per contesto, ecco la funzione originale:

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 esempio di calcolo a tempo:

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

Risultato (eseguito con PyPy 1.9.0):

2642565
time: 23.785543s
2642565
time: 0.000030s

Output (eseguito con Python 3.3.0, 2.7.2 restituisce tempi molto simili):

2642565
time: 14.426975s
2642565
time: 0.000021s

E una domanda correlata, perché questo calcolo è quasi due volte più veloce se eseguito con Python 2 o 3 rispetto a PyPy, quando di solito PyPy è molto più veloce ?

Risposte:


164

Vedi l'articolo di Wikipedia sull'esponenziazione modulare . Fondamentalmente, quando lo fai a**d % n, devi effettivamente calcolare a**d, che potrebbe essere abbastanza grande. Ma ci sono modi per calcolare a**d % nsenza dover calcolare a**dse stesso, e questo è ciò che powfa. L' **operatore non può farlo perché non può "vedere nel futuro" per sapere che stai per prendere immediatamente il modulo.


14
+1 è in realtà ciò che implica la docstring>>> 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
A seconda della versione di Python, questo potrebbe essere vero solo in determinate condizioni. IIRC, in 3.xe 2.7, puoi usare solo la forma a tre argomenti con tipi integrali (e potenza non negativa), e otterrai sempre un esponenziale modulare con il inttipo nativo , ma non necessariamente con altri tipi integrali. Ma nelle versioni precedenti c'erano regole sull'adattamento in un C long, la forma a tre argomenti era consentita float, ecc. (Si spera che tu non stia usando 2.1 o precedenti e non stia usando alcun tipo integrale personalizzato dai moduli C, quindi nessuno di questo è importante per te.)
abarnert

13
Dalla tua risposta sembra che sia impossibile per un compilatore vedere l'espressione e ottimizzarla, il che non è vero. E guarda caso l'assenza di corrente compilatori Python lo fanno.
danielkza

5
@danielkza: è vero, non volevo implicare che fosse teoricamente impossibile. Forse "non guarda al futuro" sarebbe più preciso di "non può vedere nel futuro". Si noti, tuttavia, che l'ottimizzazione potrebbe essere estremamente difficile o addirittura impossibile in generale. Per costanti operandi potrebbe essere ottimizzato, ma x ** y % n, xpotrebbe essere un oggetto che implementa __pow__e, sulla base di un numero casuale, restituisce uno dei diversi oggetti di attuazione __mod__in modi che dipendono anche numeri casuali, ecc
BrenBarn

2
@danielkza: Inoltre, le funzioni non hanno lo stesso dominio: .3 ** .4 % .5è perfettamente legale, ma se il compilatore lo trasformasse in pow(.3, .4, .5)quello solleverebbe un file TypeError. Il compilatore dovrebbe essere in grado di sapere che a, de nsono garantita valori di un tipo integrale (o forse solo specificamente di tipo int, perché la trasformazione non aiuta diversamente), ed dè garantito per essere non negativo. È qualcosa che un JIT potrebbe concettualmente fare, ma un compilatore statico per un linguaggio con tipi dinamici e nessuna inferenza semplicemente non può.
abarnert

37

BrenBarn ha risposto alla tua domanda principale. Da parte tua:

perché è quasi il doppio più veloce se eseguito con Python 2 o 3 rispetto a PyPy, quando di solito PyPy è molto più veloce?

Se leggi la pagina delle prestazioni di PyPy , questo è esattamente il tipo di cose in cui PyPy non è bravo, infatti, il primo esempio che danno:

Cattivi esempi includono l'esecuzione di calcoli con lunghi lunghi, che viene eseguita da un codice di supporto non ottimizzabile.

Teoricamente, trasformare un enorme esponenziamento seguito da una mod in un esponenziale modulare (almeno dopo il primo passaggio) è una trasformazione che un JIT potrebbe essere in grado di fare ... ma non il JIT di PyPy.

Come nota a margine, se hai bisogno di fare calcoli con numeri interi enormi, potresti voler guardare a moduli di terze parti come gmpy, che a volte possono essere molto più veloci dell'implementazione nativa di CPython in alcuni casi al di fuori degli usi tradizionali, e ha anche molto di funzionalità aggiuntive che altrimenti dovreste scrivere voi stessi, a costo di essere meno convenienti.


2
i desideri sono stati risolti. prova pypy 2.0 beta 1 (non sarà più veloce di CPython, ma non dovrebbe nemmeno essere più lento). gmpy non ha un modo per gestire MemoryError :(
fijal

@fijal: Sì, ed gmpyè anche più lento invece che più veloce in alcuni casi, e rende molte cose semplici meno convenienti. Non è sempre la risposta, ma a volte lo è. Quindi vale la pena guardare se hai a che fare con numeri interi enormi e il tipo nativo di Python non sembra abbastanza veloce.
abarnert

1
e se non ti interessa se i tuoi numeri sono grandi rende il tuo programma segfault
fijal

1
È il fattore che ha impedito a PyPy di ​​utilizzare la libreria GMP per i suoi lunghi tempi. Potrebbe essere ok per te, non va bene per gli sviluppatori di macchine virtuali Python. Il malloc può fallire senza utilizzare molta RAM, basta inserire un numero molto grande lì. Il comportamento di GMP da quel momento in poi non è definito e Python non può permetterlo.
fijal

1
@fijal: sono completamente d'accordo sul fatto che non dovrebbe essere usato per implementare il tipo integrato di Python. Ciò non significa che non dovrebbe mai essere usato per niente mai.
abarnert

11

Ci sono scorciatoie per fare esponenziazione modulare: ad esempio, puoi trovare a**(2i) mod nper ogni ida 1a log(d)e moltiplicare (mod n) i risultati intermedi di cui hai bisogno. Una funzione di esponenziazione modulare dedicata come 3 argomenti pow()può sfruttare questi trucchi perché sa che stai facendo aritmetica modulare. Il parser Python non può riconoscerlo data la semplice espressione a**d % n, quindi eseguirà il calcolo completo (che richiederà molto più tempo).


3

Il modo in cui x = a**d % nviene calcolato è aumentare aalla dpotenza, quindi il modulo con n. In primo luogo, se aè grande, crea un numero enorme che viene poi troncato. Tuttavia, x = pow(a, d, n)è molto probabilmente ottimizzato in modo che nvengano tracciate solo le ultime cifre, che sono tutto ciò che è necessario per calcolare la moltiplicazione modulo un numero.


6
"richiede d moltiplicazioni per calcolare x ** d" - non corretto. Puoi farlo in moltiplicazioni O (log d) (molto ampie). L'esponenziazione per quadratura può essere utilizzata senza modulo. L'enorme dimensione dei moltiplicati è ciò che prende il comando qui.
John Dvorak

@ JanDvorak Vero, non sono sicuro del motivo per cui pensavo che Python non avrebbe utilizzato lo stesso algoritmo di esponenziazione per **quanto riguarda pow.
Yuushi

5
Non le ultime "n" cifre .. mantiene solo i calcoli in Z / nZ.
Thomas
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.