Por que pow (a, d, n) é tão mais rápido que a ** d% n?

110

Eu estava tentando implementar um teste de primalidade Miller-Rabin e fiquei intrigado por que estava demorando tanto (> 20 segundos) para números de tamanho médio (~ 7 dígitos). Acabei descobrindo que a seguinte linha de código é a origem do problema:

x = a**d % n

(onde a,, de nsão todos semelhantes, mas desiguais, números de tamanho médio, **é o operador de exponenciação e %é o operador de módulo)

Em seguida, tentei substituí-lo pelo seguinte:

x = pow(a, d, n)

e, em comparação, é quase instantâneo.

Para contexto, aqui está a função 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))

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

Resultado (executado com PyPy 1.9.0):

2642565
time: 23.785543s
2642565
time: 0.000030s

Saída (executado com Python 3.3.0, 2.7.2 retorna tempos muito semelhantes):

2642565
time: 14.426975s
2642565
time: 0.000021s

E uma questão relacionada, por que esse cálculo é quase duas vezes mais rápido quando executado com Python 2 ou 3 do que com PyPy, quando geralmente PyPy é muito mais rápido ?

Lyallcooper
fonte

Respostas:

164

Veja o artigo da Wikipedia sobre exponenciação modular . Basicamente, quando você faz isso a**d % n, você realmente tem que calcular a**d, o que pode ser muito grande. Mas existem maneiras de computar a**d % nsem precisar computar a a**dsi mesmo, e é isso que powacontece. O **operador não pode fazer isso porque ele não pode "ver o futuro" para saber que você vai tomar o módulo imediatamente.

BrenBarn
fonte
14
+1 é realmente o que o docstring implica>>> 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
Dependendo da sua versão do Python, isso pode ser verdade apenas sob certas condições. IIRC, em 3.xe 2.7, você só pode usar a forma de três argumentos com tipos integrais (e potência não negativa), e você sempre obterá exponenciação modular com o inttipo nativo , mas não necessariamente com outros tipos integrais. Mas em versões mais antigas, havia regras sobre o encaixe em um C long, a forma de três argumentos era permitida float, etc. (Espero que você não esteja usando 2.1 ou anterior e não esteja usando nenhum tipo integral personalizado de módulos C, portanto, nenhum isso importa para você.)
abarnert
13
Pela sua resposta, parece que é impossível para um compilador ver a expressão e otimizá-la, o que não é verdade. Ela só acontece que há compiladores atuais Python fazê-lo.
danielkza
5
@danielkza: É verdade, não quis dizer que é teoricamente impossível. Talvez "não olhe para o futuro" seja mais preciso do que "não consigo ver o futuro". Observe, porém, que a otimização pode ser extremamente difícil ou mesmo impossível em geral. Para operandos constantes, ele poderia ser otimizado, mas em x ** y % n, xpoderia ser um objeto que implementa __pow__e, com base em um número aleatório, retorna um dos vários objetos diferentes implementados __mod__de maneiras que também dependem de números aleatórios, etc.
BrenBarn
2
@danielkza: Além disso, as funções não têm o mesmo domínio: .3 ** .4 % .5é perfeitamente legal, mas se o compilador transformasse isso em pow(.3, .4, .5)isso geraria um TypeError. O compilador teria que ser capaz de saber que a, de ntêm a garantia de serem valores de um tipo integral (ou talvez apenas especificamente do tipo int, porque a transformação não ajuda de outra forma) e dsão garantidos como não negativos. Isso é algo que um JIT poderia concebivelmente fazer, mas um compilador estático para uma linguagem com tipos dinâmicos e sem inferência simplesmente não pode.
abarnert em
37

BrenBarn respondeu à sua pergunta principal. Para sua parte:

por que é quase duas vezes mais rápido quando executado com Python 2 ou 3 do que PyPy, quando geralmente PyPy é muito mais rápido?

Se você ler a página de desempenho do PyPy , esse é exatamente o tipo de coisa em que o PyPy não é bom - na verdade, o primeiro exemplo que eles dão:

Exemplos ruins incluem fazer cálculos com longos longos - que é executado por código de suporte não otimizado.

Teoricamente, transformar uma exponenciação enorme seguida por um mod em uma exponenciação modular (pelo menos após a primeira passagem) é uma transformação que um JIT pode ser capaz de fazer ... mas não o JIT de PyPy.

Como uma observação lateral, se você precisa fazer cálculos com números inteiros enormes, você pode querer olhar para módulos de terceiros como gmpy, que às vezes pode ser muito mais rápido do que a implementação nativa do CPython em alguns casos fora dos usos principais, e também tem muito de funcionalidade adicional que você mesmo teria que escrever, ao custo de ser menos conveniente.

abarnert
fonte
2
longs foi corrigido. experimente o pypy 2.0 beta 1 (não será mais rápido que o CPython, mas também não deve ser mais lento). gmpy não tem uma maneira de lidar com MemoryError :(
fijal
@fijal: Sim, e gmpytambém é mais lento em vez de mais rápido em alguns casos, e torna muitas coisas simples menos convenientes. Nem sempre é a resposta - mas às vezes é. Portanto, vale a pena verificar se você está lidando com números inteiros enormes e o tipo nativo do Python não parece rápido o suficiente.
abarnert de
1
e se você não se importa se seus números forem grandes, seu programa
falhará
1
É o fator que fez com que o PyPy não usasse a biblioteca GMP por muito tempo. Pode ser bom para você, mas não para desenvolvedores Python VM. O malloc pode falhar sem usar muita RAM, basta colocar um número muito grande lá. O comportamento do GMP desse ponto em diante é indefinido e o Python não pode permitir isso.
fijal
1
@fijal: Concordo plenamente que não deve ser usado para implementar o tipo integrado do Python. Isso não significa que nunca deva ser usado para nada.
abarnert em
11

Existem atalhos para fazer a exponenciação modular: por exemplo, você pode encontrar a**(2i) mod npara cada ide 1a log(d)e multiplicar (mod n) os resultados intermediários de que precisa. Uma função de exponenciação modular dedicada como 3 argumentos pow()pode alavancar tais truques porque sabe que você está fazendo aritmética modular. O analisador Python não pode reconhecer isso dada a expressão nua a**d % n, então ele executará o cálculo completo (o que levará muito mais tempo).

atomicinf
fonte
3

O modo como x = a**d % nse calcula é elevar aà dpotência, então módulo isso com n. Em primeiro lugar, se afor grande, isso cria um grande número que é truncado. No entanto, x = pow(a, d, n)é mais provável que seja otimizado para que apenas os últimos ndígitos sejam rastreados, que são tudo o que é necessário para calcular o módulo de multiplicação de um número.

Yuushi
fonte
6
"requer d multiplicações para calcular x ** d" - incorreto. Você pode fazer isso em multiplicações O (log d) (muito largas). A exponenciação por quadratura pode ser usada sem um módulo. O tamanho dos multiplicandos é o que lidera aqui.
John Dvorak
@JanDvorak True, não tenho certeza por que pensei que python não utilizaria o mesmo algoritmo de exponenciação para **como para pow.
Yuushi
5
Não os últimos "n" dígitos .. ele apenas mantém os cálculos em Z / nZ.
Thomas