A linguagem Python funciona através da compilação e interpretação de bytecode em uma máquina virtual Python (Python Virtual Machine ou apenas VM neste artigo). Esta VM é baseada em uma máquina de pilha, as operações dos bytecodes (os opcodes) envolvem a manipulação (push/empurra e pop/retira) de elementos nessa pilha.
O CPython não é o único Python existente, é possível encontrar outras implementações de Python, como Jython, Stackless Python ou PyPy e estes podem utilizar mecanismos diferente de interpretação na VM.
Por exemplo, algumas implementações de VM Python utilizam registradores, ao invés da manipulação direta de pilha.
Os bytecodes Python são a forma mais de baixo-nível que o código fonte em Python pode ser traduzido. Para saber quais são eles, pode-se utilizar o módulo opcode
. O módulo dis
permite reconstruir o nome das instruções a partir dos bytecodes.
Por exemplo, para trecho em Python
def ex1(): return 2011
este é compilado no seguinte bytecode
>>> import dis >>> dis.dis(ex1) 2 0 LOAD_CONST 1 (2011) 3 RETURN_VALUE
O bytecode LOAD_CONST
value tem como efeito empurrar a constante value (2011 neste caso) para o topo da pilha. Já o bytecode RETURN_VALUE
retorna o valor do topo da pilha à função que fez a chamada.
Em linhas gerais, é dessa forma que os bytecodes do interpretador Python funcionam e são interpretados.
Para uma função que retorna a soma de 2000, 10 e 1 assim definida:
def ex2(): return 2000 + 10 + 1
o interpretador Python 2.4 não faz otimizações no bytecode, do tipo pré-calcular um valor constante, portanto, o bytecode gerado na versão 2.4 é:
2 0 LOAD_CONST 1 (2000) 3 LOAD_CONST 2 (10) 6 BINARY_ADD 7 LOAD_CONST 3 (1) 10 BINARY_ADD 11 RETURN_VALUE
Ou seja, um valor constante é sempre calculado com os vários bytecodes, em cada chamada de função. Mas nas versões posteriores de Python estes já possuem (alguma) otimização de bytecode, e o resultado é mais inteligente.
No interpretador Python 2.6:
2 0 LOAD_CONST 5 (2011) 3 RETURN_VALUE
o resultado foi previamente calculado e guardado como uma constante, ao invés de ser sempre calculado via instruções de bytecode, como faz o Python 2.4.
É comum guardar o resultado de uma função em uma variável antes de retornar o valor, por exemplo:
def ex3(): res = 2000 + 10 + 1 return res
O bytecode gerado na versão 2.6 do Python é:
2 0 LOAD_CONST 5 (2011) 3 STORE_FAST 0 (res) 3 6 LOAD_FAST 0 (res) 9 RETURN_VALUE
O otimizador poderia perceber esse tipo de construção e gerar uma versão otimizada (que respeite a semântica do código), pois a variável res
tem um escopo local e não tem efeito colateral e pode ser removida sem dano a interpretação.
O bytecode otimizado poderia ser então:
2 0 LOAD_CONST 5 (2011) 9 RETURN_VALUE
semelhante a construção em Python do exemplo ex2.
Mas qual seria o ganho e impacto dessa modificação? É perceptível para muitas repetições, conforme mostra o resultado do uso de timeit
, módulo para pequenos benchmarks de trechos de Pyhton.
>>> print timeit.timeit(ex3) 0.365698814392 >>> print timeit.timeit(ex2) 0.325710058212
O uso de STORE_FAST
e LOAD_FAST
(no exemplo ex3) para guardar e ler variáveis causou um impacto nas repetições do timeit
de mais de 10%.
Conforme foi demostrado em um exemplo simples no Python versão 2.6, há ainda algumas brechas de otimização. Ou pelo menos existe alguma razão para continuar a estudar que tipo de otimizações já consagradas (chamadas de peephole) podem ser aplicadas ao interpretador Python.
O Skip Montanaro fez um artigo em 1998 comentado técnicas de otimização. O artigo é A Peephole Optimizer for Python.
Para quem deseja manipular diretamente os bytecodes em Python ou visualizar melhor o código em bytecode, recomendo olhar o módulo byteplay.