sexta-feira, 17 de abril de 2015

Cython

Nas últimas semanas tenho desenvolvido uma aplicação usando Cython e me surpreendi com o resultado.

Comecei usando o Cython apenas para compilar código Python – o que de fato rendeu o aumento de desempenho prometido –, mas então resolvi ir mais longe pra ver o quanto poderia extrair dessa plataforma: comecei a usar tipagem estática, funções C (cdef) e depois acabei migrando minhas classes para tipos de extensão (cdef classes).

A cada novo passo era perceptível o crescimento do desempenho e da coerência geral do código. Minha única crítica é quanto aos testes: o código fica muito difícil de ser testado, já que não é possível fazer monkey-patch para colocar os mocks.

Segundo a documentação, ao compilar código Python com Cython, você tem um aumento de 35% no desempenho do código. Usando a tipagem estática, o aumento é de 300% e usando funções C e tipos de extensão o aumento é de aproximadamente 15.000%.

Não posso confirmar esses números, pois não fiz medições exatas, mas uma fila do RabbitMQ que enchia com 6, 7 mil mensagens, encheu com 64 mil no mesmo período de tempo (eu estava fazendo apenas carga, sem consumir a fila).

Uma coisa que gostei muito no Cython é o feeling: tem uma pegada bastante parecida com a do Objective C, o que faz sentido.

Vou dar um exemplo da própria documentação do Cython:

Dado o seguinte código Python puro:
def f(x):
    return x**2-x

def integrate_f(a, b, N):
    s = 0
    dx = (b-a)/N
    for i in range(N):
        s += f(a+i*dx)
    return s * dx
Você pode salvar esse código em integrate.pyx e compilá-lo assim:
bash$ cythonize -ib integrate.pyx
Isso irá gerar o código equivalente em C (integrate.c) e compilá-lo na biblioteca integrate.so, que pode ser diretamente importada em Python:
bash$ python
>>> from integrate import integrate_f
>>>
Se você tiver o IPython, pode usar o Cython no prompt:
bash$ ipython
Python 2.7.9 (default, Jan  7 2015, 11:49:12) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> %load_ext Cython
>>> %%cython
from integrate cimport integrate_f
De qualquer forma, só usando a biblioteca compilada em vez do código Python importado, Cython já promete um aumento de 35% de performance – o que não me frustrou.

Além disso, podemos adicionar tipagem ao código: Cython é um superset em Python, ou seja, é uma linguagem em si e um código Python é também código válido Cython.
def f(double x):
    return x**2-x

def integrate_f(double a, double b, int N):
    cdef:
        int i
        double s, dx
    s = 0
    dx = (b-a)/N
    for i in range(N):
        s += f(a+i*dx)
    return s * dx
Segundo a documentação, isso garante um desempenho 4 vezes superior ao de Python.

No entanto ainda é possível otimizar mais ainda o código! Cython tem uma sintaxe específica que gera código C diretamente, cdef:
cdef double f(double x) except? -2:
    return x**2-x

def integrate_f(double a, double b, int N):
    cdef:
        int i
        double s, dx
    s = 0
    dx = (b-a)/N
    for i in range(N):
        s += f(a+i*dx)
    return s * dx
O desempenho dessa função f em C promete ser 150 vezes melhor do que a mesma função em Python puro.

O except? -2 na função é usado para passar exceções para o código C. Em um outro momento posso entrar em mais detalhes.

Tipo de extensão

Tipos de extensão são equivalentes às classes de Python, porém mais restritos e muito mais eficientes.

Por exemplo, da a seguinte classe:
class Consumer(object):

    def __init__(self, backend):
        self.backend = backend

    def run(self, handler):
        resp = self.backend.get()
        return handler.handle(resp)
Considerando que as instâncias só serão executadas em Cython, o código equivalente estaria em dois arquivos, o primeiro consumer.pxd:
from backends.base cimport Backend
from handlers.base cimport Handler

cdef class Consumer:

    cdef:
        Backend backend
        int run(self, Handler handler) except? -1
Esse código .pxd equivale ao cabeçalho .h de C. Agora, o arquivo de implementação deve ser chamado consumer.pyx:
from backends.base cimport Backend
from handlers.base cimport Handler

cdef class Consumer:

    def __cinit__(self, Backend backend):
        self.backend = backend

    cdef int run(self, Handler handler) except? -1:
        cdef dict resp = self.backend.get()
        return handler.handle(resp)
Esse código promete ser muito mais eficiente que sua versão em Python, infelizmente métodos declarados como cdef são acessíveis apenas em C (e em Cython). Para que o método seja acessível em Python, ele deve ser criado como um método Python comum (def) ou pode ser usado cpdef.

O comando cpdef (C/Python def) cria a função C (como cdef) e um wrapper em Python para torná-la acessível. Se o módulo for importando com cimport, será usada a função C original; se for importado com o clássico import, será usado o wrapper.

Conclusão

Até agora não tive motivos para me arrepender de usar Cython, recomendo.

[]’s
ℭacilhας, La Batalema
blog comments powered by Disqus