Back-End

23 mai, 2014

Ferramentas para acelerar programas Python

Publicidade

Python é uma das melhores linguagens de programação já criadas. É maravilhosamente fácil de usar, tem um grande suporte de bibliotecas e oferece excelente produtividade ao programador e flexibilidade, mas, infelizmente, perde frente ao desempenho. O Python é uma linguagem de programação interpretada e dinâmica baseada em bytecode que é executada através da máquina virtual, o que significa que os programas em Python são mais lentos que as linguagens de programação compiladas nativamente como C e C++.

Outro problema com o Python é a falta de suporte para processadores multicore, agora padrão. Esses fatores de amortecimento de velocidade no Python são frustrantes para os desenvolvedores, que muitas vezes precisam alternar na escrita de pedaços de código complicados em C ou C++ para equilibrar as desvantagens de desempenho do Python. Felizmente, a comunidade FLOSS fornece algumas ferramentas que ajudarão o usuário a turbinar seus programas em Python.

Porque o Python é lento

Se o usuário quiser saber por que seus programas em Python são mais lentos do que os programas criados em linguagens como C e C++, a resposta está nas camadas e transformações pelas quais um programa Python sofre antes de executar. Quando compilamos e linkamos um programa em linguagens como C e C++, o programa inteiro é convertido em instruções executáveis ​​nativas, que o mecanismo de execução de hardware interpreta.

Em contraste, a execução de um programa em Python é um processo de duas etapas. Em primeiro lugar, um programa Python é convertido em uma representação intermediária independente de plataforma, conhecida como bytecodes. Esta compilação bytecode dispara automaticamente quando executamos programas em Python, seja incluindo o caminho do interpretador Python no topo dos arquivos de origem ou através do python filename.py. As instruções de bytecode são então interpretadas através de um aplicativo da plataforma nativa conhecido como máquina virtual Python. A referência oficial – e a implementação mais popular da máquina virtual Python – é CPython, e isso deveria ser óbvio pelo próprio nome da linguagem de programação C. Cada instrução bytecode invoca uma rotina C que compreende uma instrução nativa múltipla para realizar a tarefa codificada pelo bytecode. Este processo inicia os atrasos de execução.

Python é uma linguagem de programação dinâmica, na qual cada tipo de dado é um objeto, mesmo que se pareça com um tipo de dado nativo. Se digitarmos i = 7; f = 3.14 na linha de comando interativa do Python e então dir(i); dir(f), veremos uma lista de vários métodos e atributos associados a i e f. Como apenas os valores possuem uma correspondência em Python, e não os objetos, atribuimos diretamente os valores para vários objetos em um programa Python sem declarar seus tipos, o oposto de linguagens estáticas como C e C++, em que primeiro temos que declarar os tipos dos objetos nativos ou abstratos antes de manipulá-los. Além disso, podemos ligar diferentes tipos de valores para objetos em Python no tempo de execução. Assim, o tempo de execução em Python também tem que deduzir os tipos de objetos e converter esses objetos para os tipos de dados nativos para formar as instruções nativas a serem executadas na CPU. Este tipo de dedução e tradução é outra fonte de sobrecarga para o Python.

Os vários objetos em Python são alocados e desalocados através de um subsistema para o gerenciamento automático de memória conhecido como gc (garbage collector). A atividade gc executa atrás de um certo número de alocações de objetos para gerenciar e liberar a memória de objetos inacessíveis. Durante os ciclos do gc, as outras atividades param, por isso, se os ciclos gc forem longos, introduzirão atrasos na execução do código Python.

A máquina virtual Python lida com muitos objetos globais e módulos de código nativo externos sem threads seguras. Assim, o interpretador Python garante que apenas uma única thread em execução modifica os objetos globais e garante a segurança da thread, garantindo um bloqueio de exclusão mútuo para a thread programada, conhecida como GIL (Global Interpreter Lock)1. O design do interpretador Python torna-se mais simples e mais fácil de manter, mas introduz outro efeito colateral de amortecimento de velocidade. Por conta do GIL, os programas em Python não podem tirar proveito dos processadores multicore modernos, que são capazes de executar múltiplas threads de instruções em paralelo. Isto significa que mesmo se o usuário escrever seus programas em Python, sob threads múltiplas, o interpretador Python só irá executar uma thread por vez em um core da CPU, e os cores restantes não são utilizados pelo interpretador Python.

image.NMHYFX
Figura 1: Baixe o Psyco pelo site do projeto.

Psyco

A opção mais fácil para acelerar seus aplicativos Python sem alterações no código é usar o Psyco2. O Psyco é um módulo Python carregável, então o usuário deve apenas importar o módulo Psyco e adicionar uma ou duas linhas para acelerar o código existente. O Psyco é um trabalho maduro que existe há muitos anos, mas atualmente encontra-se em modo de manutenção e só está disponível para distribuições Python de 32 bits em arquitetura de processador x86. De acordo com o autor do Psyco, o trabalho atual e futuro relacionado a ele é direcionado a uma nova implementação Python, conhecida como PyPy. O Psyco tem por objetivo fornecer acelerações na faixa de duas a cem vezes (mas geralmente é em torno de quatro vezes), mais rápido do que a fonte Python não modificada e o interpretador padrão. Ele está disponível para várias plataformas de sistemas operacionais, incluindo GNU/Linux, BSD, Mac OS X e Windows.

O Pysco funciona um pouco como os compiladores JIT (Just in Time) em outras linguagens de programação baseadas em máquinas virtuais, como o Java. Na verdade, o Psyco é como um compilador JIT que compila pedaços de bytecodes Python em instruções nativas da máquina em tempo de execução, mas faz mais do que um compilador JIT. Ele cria várias versões do código nativo compilado para diferentes tipos de objetos, correspondendo a um único pedaço de bytecode Python. O Psyco cria um programa Python em tempo de execução e marca algumas partes do programa como zonas quentes, para as quais emite as instruções de máquina nativa. Ele continua usando as várias versões dos pedaços nativos de código de máquina gerados para os vários tipos de objetos usados ​​nas zonas quentes. Dessa forma, o Psyco tenta adivinhar as partes dos programas Python orientados para o desempenho e os compila em pedaços de código nativo para fornecer o equivalente em desempenho a linguagens nativas compiladas como C e C++. Também realiza várias otimizações de tempo de execução para emitir as instruções de código nativo melhor otimizadas.

O Psyco é escrito em C, então precisamos de ferramentas de compilação padrão para compilá-lo. Também precisamos dos cabeçalhos Python padrão para instalar o programa a partir do pacote de código. Para completar as dependências, digite o seguinte comando em um console de texto em um computador Ubuntu de 32-bits:

sudo apt-get install build-essential &&
sudo apt-get install python-dev

Agora siga para a página do projeto Psyco (figura 1), baixe a última fonte tarball, e entre com o seguinte código no console de texto:

tar zxvf psyco-version-src.tar.gz && cd psyco-version

Finalmente, insira o comando com sudo ou direitos de su:

python setup.py install

para uma instalação completa do Psyco. 

Se tudo correr bem, digitar python -c ‘import psyco’ no console de texto deve dar certo, o que significa que agora estamos prontos para brincar com o Psyco. Se o usuário se sentir pronto para colocar o Psyco em seu aplicativo Python sem ler mais sobre ele, acrescente em seguida as seguintes linhas:

try:

import psyco

psyco.full()

except ImportError:

pass

Um lugar recomendado para colocar essas linhas é o início do caso if ‘__main__’ == __name__ em seu aplicativo Python. A função psyco.full() instrui o Psyco a invadir todo o código Python e compilar nativamente o máximo possível. Os aumentos de desempenho exatos do Psyco são difíceis de adivinhar por conta do intrincado mecanismo que o sistema usa para gerar versões compiladas nativamente dos programas em Python, mas existem algumas regras de ouro para extrair os maior aumento de desempenho.

O Psyco é útil principalmente para programas em Python que estão vinculados à CPU (por exemplo, com looping, cálculos matemáticos, walking lists, strings e operações com números). O Psyco pode não ser útil para código Python, que é limitado ao I/O (por exemplo, à espera de eventos de rede, leitura e escrita de disco, operações de bloqueio), porque o desempenho nesses casos é dificultado pelo Global Interpreter Lock (GIL) e o Psyco não tem controle sobre isso. Como o Psyco gera várias versões de pedaços de código nativo otimizado para diferentes tipos de objetos, ele precisa de uma grande quantidade de memória adicional. Isso degrada o desempenho do programa, se o usuário tentar fazer o Psyco trabalhar em programas Python inteiros, que não sejam adequados para ele. Por esta razão, o usuário só deve usar psyco.full() para casos em que os programas são curtos ou nos quais tenha a certeza de que a CPU é a origem dos problemas de desempenho.

Use psyco.profile() no lugar de psyco.full() se quiser instruir o Psyco a não invadir todo o programa Python, mas primeiro descrever e, em seguida, compilar nativamente apenas as partes críticas nessa descrição. A função profile() é recomendada no caso de grandes programas. Além disso, o usuário pode passar um valor de marca d’água entre 0.0 e 1.0, que indica a proporção de funções compiladas nativamente. O valor de marca d’água padrão é 0.09, se o usuário não passar qualquer valor para o profile(). As funções psyco full() e profile() possuem outro argumento memória que especifica o limite de kilobyte de onde as funções são compiladas. O Psyco também fornece uma função log() para habilitar o log. O código Psyco a seguir habilita o log e a descrição da compilação das funções que somam pelo menos 7% do tempo e menos de 200KB por função:

Figura 2: Google é o host do Unladen Swallow, projeto dedicado à construção de aplicativo Python otimizado.
Figura 2: Google é o host do Unladen Swallow, projeto dedicado à construção de aplicativo Python otimizado.
psyco.log()

psyco.profile(0.07, memory=200)

Com o psyco.bind() e o psyco.proxy() podemos controlar a seleção das funções que o Psyco compila. Ambas as funções Psyco levam o nome da função individual para compilar, mas proxy() apenas compila a instância particular da função que toma como argumento, não todas as instâncias da função chamada no programa Python. O bind() compila a função original passada a ele, assim como as funções chamadas no nome do argumento de função que ele carrega. No seguinte exemplo de código, todas as instâncias do function1 e as funções internas que ele chama são compiladas nativamente, mas apenas a instância g do function2 é compilada nativamente:

psyco.bind(function1)

g = psyco.proxy(function2)

g(args)

function2(args)

Assim, podemos melhorar o desempenho em tempo de execução através do Psyco com apenas algumas linhas adicionais e um pouco de entendimento sobre a natureza dos programas em Python. Com um pouco de tentativa e erro, é possível escolher os melhores modos de acelerar o Psyco em programas Python. O Psyco vem com muitos programas de teste em Python no subdiretório teste do diretório de fontes. Para mais informações, consulte a documentação fornecida na página do projeto Psyco.

Unladen Swallow

O fundador do Python, Guido van Rossum, trabalha para o Google, e o Python é uma das três linguagens de programação oficiais utilizadas lá – juntamente com C++ e Java. Um dos resultados das melhorias do Python no Google é o Unladen Swallow3, um ramo de otimização da base de código padrão do Python (figura 2). O principal objetivo do Unladen Swallow é manter compatibilidade total com o CPython e suas bibliotecas e ainda proporcionar um aumento de cinco vezes no desempenho. O Unladen Swallow é baseado em Python 2.6.1 e apenas muda o tempo de execução do Python. Atualmente, o YouTube faz uso maciço do programa.

O projeto Unladen Swallow implementou a compilação JIT para melhorar o desempenho de tempo de execução dos programas em Python. A implementação atual do tempo de execução do Unladen Swallow converte os bytecodes Python de todas as funções quentes para LLVM (Low-Level Virtual Machine) IR (Intermediate Representation), e este resultado intermediário é então compilado para o código de máquina nativo. Os planos de longo prazo do projeto são para mudar a máquina virtual baseada na pilha CPython para a máquina virtual baseada em registradores com base no código LLVM para extrair mais aceleração. O projeto também adicionou muitas melhorias ao garbage collector do CPython para executar menos ciclos de coleta e diminuir os atrasos causados ​​pelo garbage collection.

O Unladen Swallow é um projeto em andamento, então o usuário deve compilá-lo a partir do código fonte, depois de fazer o fetch do código do tronco do projeto SVN. Os pré-requisitos para compilá-lo são llvm e clang. (O usuário vai precisar das ferramentas de compilação padrão utilizadas anteriormente com Psyco para compilar llvm e clang). A página do projeto Unladen Swallow fornece informações detalhadas sobre a instalação. A página do projeto fornece também a suite de teste Python para o benchmark do Unladen Swallow contra o CPython.

Shed Skin

Shed Skin4 eleva o desempenho do código Python fazendo uso de uma abordagem diferente. Ele toma os programas escritos em Python em um subconjunto estático da linguagem Python padrão e gera o código C++ otimizado correspondente. O código C++ gerado pode ser compilado como um aplicativo nativo ou o módulo de extensão a ser importado em programas Python maiores. Desta forma, o Shed Skin pode impulsionar os programas que fazem uso intensivo da CPU com uma velocidade adicional. Atualmente, o Shed Skin está em fase experimental e suporta apenas cerca de 20 módulos da biblioteca padrão, mas parece um projeto muito promissor.

O usuário pode encontrar pacotes .deb e pacotes .rpm para o Shed Skin na página do projeto, além do pacote de código. Para instalar o Shed Skin do código, os pré-requisitos são g++, o garbage collector Boehm5, e a biblioteca PCRE6. Para instalar esses componentes, digite o seguinte comando em um console de texto em uma máquina Ubuntu:

sudo apt-get install g++ libgc-dev libpcre3-dev

Para compilar gc e libpcre para plataformas em que pacotes pré-compilados não estão disponíveis, baixe os tarballs e siga as instruções de compilação. Uma vez que os pré-requisitos estiverem instalados, baixe a última tarball do Shed Skin na página do projeto e execute o seguinte comando em um console de texto:

tar zxvf shedskin-version.tgz && cd shedskin-version

Finalmente, execute o seguinte comando com privilégios sudo ou su no console de texto para uma instalação completa do Shed Skin:

python setup.py install

O Shed Skin traduz os tipos Python implícitos para tipos explícitos C++. Ele usa técnicas de inferência de tipos para determinar os tipos usados ​​nos programas Python, e faz análise iterativa dos programas em Python para checar quais valores são atribuídos a diferentes objetos e deduzir seus tipos estáticos. O programa então permite que o usuário combine diferentes tipos atribuídos à variável. E também o obriga a usar somente tipos similares aos elementos de várias coleções Python (tuple, list, set etc).

O uso básico do Shed Skin é:

shedskin file.py

O Shed Skin cria o file.hpp, file.cpp e Makefile no diretório de trabalho atual. Ele também oferece algumas opções para controlar a forma como os arquivos de saída são gerados (por exemplo, um arquivo Python adicional é gerado com -a). Finalmente, invoque o makefile gerado para criar o executável compilado correspondente à fonte Python. Muitos programas de teste são fornecidos como um arquivo tar na página do projeto Shed Skin. A página menciona a velocidade de duas a 200 vezes sobre o CPython para os programas de teste. Para obter os programas de teste de exemplo, baixe shedskin-examples-version.tgz e execute o seguinte comando em um console de texto:

tar zxvf shedskin-examples-version.tgz &&
cd shedskin-examples-version

Para testar um programa Python JPEG-para-BMP, digite o seguinte comando no console de texto para compilar o executável nativo:

shedskin -m jpg2bmp.mk TonyJpegDecoder.py && make -f jpg2bmp.mk
Figura 3: A página inicial do projeto descreve os benefícios do Wirbel - e também destaca algumas limitações.
Figura 3: A página inicial do projeto descreve os benefícios do Wirbel – e também destaca algumas limitações.

Para comparar o desempenho da versão Python e a versão compilada do programa, digite os comandos time python TonyJpeg Decoder.py e time ./TonyJpegDecoder. A versão compilada do exemplo TonyJpegDecoder forneceu velocidade de aceleração sobre a versão Python de 1.5 na nossa máquina Ubuntu. Também tentamos alguns outros programas de teste; a versão compilada do chess.py proporcionou uma aceleração de 12 vezes e sudoku1.py uma aceleração de 31 vezes sobre as versões do Python.

Wirbel: linguagem compilada do tipo Python

O Wirbel7 é outro projeto experimental interessante para quem objetiva melhorar o desempenho de programas em Python. De acordo com a introdução na página inicial do Wirbel (figura 3), o “Wirbel é uma nova linguagem de programação. Possui uma sintaxe e semântica semelhante ao Python… mas, – ao contrário do Python – o Wirbel é uma linguagem de compilador que compila em código nativo de máquina”.

O Wirbel é como o Shed Skin em relação à utilização de tipos de dados estáticos, e ele também usa a inferência de tipos para deduzir informações sobre os tipos de dados e produzir código executável nativo. O Wirbel é um novo projeto com suporte limitado de biblioteca, por isso atualmente não suporta os amplos recursos do Python, mas pode ser útil para pequenos programas em sintaxes do tipo Python. Para instalar o Wirbel, baixe o pacote de código mais recente do site do projeto e digite o seguinte comando em um console de texto:

tar zxvf wirbel-version.tar.gz && cd wirbel-version

Agora digite ./configure && make para compilar Wirbel, e se a compilação for concluída sem erro, digite make install com sudo ou direitos su no console de texto para instalar tudo. Nas máquinas Puppy de 32 bit e Ubuntu de 64 bits, tínhamos algumas falhas de compilação por conta da remoção de alguns cabeçalhos nas versões mais recentes do g++. Para resolver isso, basta adicionar os cabeçalhos #include<cstdio> tanto em src/Type.cc como src/Location.cc e #include<stdint.h> em baustones/httpd/HTTPRequest.h, respectivamente.

Esses subdiretórios estão no diretório de nível superior de origem. Enviamos a correção para o autor do Wirbel, e esperamos que seja incluído na próxima versão do projeto. Agora devemos ter mais dois comandos no console de texto. Estes são o compilador Wirbel, wic, e um utilitário Wirbel que compila e linka o código .w e executa o código nativo. Podemos adicionar o caminho absoluto do Wirbel com uma hash bang (!#) nos arquivos .w para executá-los como scripts. O compilador Wirbel possui muitos switches, que podemos explorar com o comando man wic.

O Wirbel possui mais algumas diferenças em relação ao Python, além da natureza estática dos tipos de dados. Em primeiro lugar, a extensão para fontes Wirbel é .w e não .py, e se tentarmos compilar os arquivos .py através do compilador Wirbel, obteremos uma mensagem de erro. Outra diferença é que não precisamos importar nada na fonte Wirbel, porque ela não compreende a declaração de importação. O Wirbel encontra tudo o que precisa. E também suporta sobrecarga de função, um recurso não suportado pelo Python. A função de impressão no Wirbel usa parênteses como o Python 3.0. O Wirbel vem com muitos exemplos e testes. Para vê-lo em ação e medir o aumento de desempenho em comparação com programas Python semelhantes, o usuário pode ir além dos exemplos.

Conclusão

Se o usuário está cansado de esperar que seus programas Python executem rapidamente, tente estas ferramentas de otimização inovadoras. O Psyco aumenta o desempenho de forma muito significativa com apenas algumas linhas adicionais. O Unladen Swallow é o novo tempo de execução turbo para Python que otimiza o CPython sem quebrar a compatibilidade com versões anteriores. Finalmente, o Shed Skin e o Wirbel são projetos experimentais que direcionam o subconjunto estático de código Python para proporcionar um desempenho nativo compilado.

Mais informações

  1. Python GIL: http://blip.tv/file/2232410
  2. Página do projeto Psyco: http://psyco.sourceforge.net/
  3. Página do projeto Unladen Swallow: http://code.google.com/p/unladen-swallow/
  4. Página do projeto Shed Skin: http://code.google.com/p/shedskin/
  5. Home do garbage collector Boehm: http://www.hpl.hp.com/personal/Hans_Boehm/gc/
  6. Home da biblioteca PCRE: http://www.pcre.org/
  7. Home do Wirbel: http://mathias-kettner.de/wirbel_index.html