Módulo dis Python e convolução de constantes

Olá pessoal. Hoje, queremos compartilhar outra tradução preparada antes do lançamento do curso "Desenvolvedor Web em Python" . Vamos lá!



Fiquei muito surpreso recentemente quando descobri que


>>> pow(3,89) 

trabalha mais devagar que


 >>> 3**89 

Tentei encontrar uma explicação aceitável, mas não consegui. Acompanhei o tempo de execução dessas duas expressões usando o módulo timeit do Python 3:


 $ python3 -m timeit 'pow(3,89)' 500000 loops, best of 5: 688 nsec per loop $ python3 -m timeit '3**89' 500000 loops, best of 5: 519 nsec per loop 

A diferença é pequena. Apenas 0,1 μs, mas isso me assombrou. Se não consigo explicar nada na programação, começo a sofrer de insônia


Encontrei a resposta usando o feed de IRC do Python no Freenode. O motivo pelo qual o pow funciona um pouco mais devagar é porque o CPython já possui uma etapa extra para carregar o pow a partir do namespace. Enquanto que ao chamar 3 ** 9, essa carga não é necessária em princípio. Isso também significa que essa diferença de tempo permanecerá mais ou menos constante se os valores de entrada aumentarem.


A hipótese foi confirmada:


 $ python3 -m timeit 'pow(3,9999)' 5000 loops, best of 5: 58.5 usec per loop $ python3 -m timeit '3**9999' 5000 loops, best of 5: 57.3 usec per loop 

No processo de encontrar uma solução para esse problema, também aprendi sobre o módulo dis. Ele permite descompilar o bytecode do Python e aprendê-lo. Foi uma descoberta extremamente empolgante, pois recentemente estudei engenharia reversa de arquivos binários, e o módulo descoberto foi útil nesse assunto.


Descompilei o bytecode das expressões acima e obtive o seguinte:


 >>> import dis >>> dis.dis('pow(3,89)') # 1 0 LOAD_NAME 0 (pow) # 2 LOAD_CONST 0 (3) # 4 LOAD_CONST 1 (89) # 6 CALL_FUNCTION 2 # 8 RETURN_VALUE >>> dis.dis('3**64') # 1 0 LOAD_CONST 0 (3433683820292512484657849089281) # 2 RETURN_VALUE >>> dis.dis('3**65') # 1 0 LOAD_CONST 0 (3) # 2 LOAD_CONST 1 (65) # 4 BINARY_POWER # 6 RETURN_VALUE 

Você pode ler sobre como entender corretamente a saída do dis.dis consultando esta resposta no Stackoverflow.


Ok, voltando ao código. Descompilar o pow faz sentido. O bytecode carrega pow do espaço de nomes, carrega nos registradores 3 e 89 e, finalmente, chama a função pow. Mas por que os resultados das próximas duas descompilações são diferentes? Afinal, tudo o que mudamos é o valor do expoente de 64 para 65!


Esta pergunta me apresentou outro novo conceito chamado "convolução de constantes". Isso significa que, quando temos uma expressão constante, o Python calcula seu valor no estágio de compilação; portanto, quando você executa o programa, não leva muito tempo, porque o Python usa o valor já calculado. Dê uma olhada nisso:


 def one_plue_one(): return 1+1 # --vs-- def one_plue_one(): return 2 

Python compila a primeira função na segunda e a usa ao executar o código. Nada mal, né?


Então, por que a convolução de constantes funciona para 3 ** 64, mas não para 3 ** 65? Bem, eu não sei. Provavelmente, isso está de alguma forma relacionado à limitação do número de graus calculados anteriormente pelo sistema na memória. Eu posso estar errado. O próximo passo que pretendo dar é aprofundar o código-fonte Python no meu tempo livre e tentar entender o que está acontecendo. Ainda estou procurando uma resposta para minha pergunta. Portanto, se você tiver alguma idéia, compartilhe-a nos comentários.


Quero que você se inspire neste post para encontrar uma solução para seus problemas. Você nunca sabe onde as respostas o levarão. Por fim, você pode aprender algo completamente novo, como aconteceu comigo. Espero que a chama da curiosidade ainda esteja queimando em você!


Você já reparou coisas semelhantes? Aguardando seus comentários!

Source: https://habr.com/ru/post/pt460143/


All Articles