Python, Bytecode & Otimização

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.

Otimização

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.

Sugestão de Otimização

É 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%.

Conclusão

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.