sexta-feira, 13 de novembro de 2015

Acelerando programas usando Cython - parte 2

Série de posts sobre Cython:

  1. Introdução ao desenvolvimento usando Cython
  2. Distribuição de extensões em Cython
  3. Análise de eficiência de operações
  4. Paralelismo e NoGIL

No texto anterior apresentei o básico da utilização de Cython dentro do iPython Notebook. No segundo texto desta série exploramos maneiras de distribuir código Cython que pode ser importado como qualquer outro módulo escrito em Python.

Preparação

Vamos agora disponibilizar nossa função de detecção de bordas (que fizemos na parte 1) para utilização em outros scripts. Para isto, salve o seguinte código como gradiente.pyx. Note que usamos a extensão pyx para o arquivo para indicar que ele contém código em Cython.

In [ ]:
# file: gradiente.pyx
import cython
import numpy as np

@cython.boundscheck(False)
@cython.nonecheck(False)
def morpho_gradient_cython(long[:,:] img, long[:,:] out, p):
    cdef int i, j, pi, pj, minv, maxv
    
    pi, pj = p
    minv = 255
    maxv = 0
    
    for i in range(-1, 2):
        for j in range(-1, 2):
            if img[pi+i, pj+j] < minv:
                minv = img[pi+i, pj+j]
            if img[pi+i, pj+j] > maxv:
                maxv = img[pi+i, pj+j]
    out[pi, pj] = maxv - minv

def gradiente_cython(long[:,:] img):
    cdef int i, j, w, h
    cdef long[:,:] out
    h = img.shape[0]; w = img.shape[1]
    out = np.zeros((h, w), np.int)
    for i in range(1, h-1):
        for j in range(1, w-1):
            morpho_gradient_cython(img, out, (i, j))
    return out

Opção 1: pyximport

A maneira mais fácil de se importar um módulo em Cython é usar o módulo pyximport. Quando fazemos um import XXX, o pyximport procura por arquivos .pyx e, se houver um XXX.pyx, o compila automaticamente e o importa. Se o módulo já tiver sido compilado anteriormente e não houveram mudanças ele é importado diretamente.

In [ ]:
#file: teste_import1.py
import pyximport
pyximport.install()
import numpy as np
import scipy as sp
import scipy.misc

import gradiente

img = sp.misc.lena()
out = gradiente.gradiente_cython(img)
sp.misc.imsave('result.png', out)

Apesar do pyximport ser muito conveniente, ele é adequado somente para scripts menores e que cabem inteiros em um só arquivo. Além disto, os fontes são compilados na primeira utilização e, dependendo do caso, isto pode demorar um bocado. Por outro lado, para testes rápidos ou scripts pequenos o pyximport é muito prático e adequado.

Nota Windows: se você está usando mingw para compilar extensões no Windows, pode ser necessário adicionar o argumento setup_args={'options': {'build_ext': {'compiler': 'mingw32'}}} na segunda linha para forçar a utilização do mingw.

Nota 2: recentemente tenho tido problemas com o pyximport pegando o Python errado

Opção 2: distutils

Projetos mais complexos com Cython devem utilizar distutils para gerenciar a compilação dos fontes em Cython (e também de outros fontes em C usados no projeto). A configuração fica um pouco mais complexa devido à criação de um setup.py, porém todos os fontes são compilados antes da importação do módulo e é possível inclusive criar pacotes binários para utilização em sistemas em que não é fácil compilar fontes (Windows, basicamente).

Veja abaixo um setup.py básico para compilar módulos em Cython.

In [ ]:
#file: setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

import numpy as np

pyx_files = [Extension('gradiente', ['gradiente.pyx']) # para cada arquivo colocamos o nome do modulo desejado e o arquivo pyx.

setup(ext_modules=cythonize(pyx_files))

Podemos, então, compilar nosso código em Cython executando

$ python setup.py build_ext --inplace

A instrução --inplace é usada para compilar o módulo na pasta atual. Se ela não é usada, o módulo compilado fica disponível na pasta build/. Com o código compilado, podemos importar nosso módulo normalmente em scripts.

In [ ]:
#file: teste-import2.py
import numpy as np
import scipy as sp
import scipy.misc

import gradiente

img = sp.misc.lena()
out = gradiente.gradiente_cython(img)
sp.misc.imsave('result.png', out)

Opção 3: compilação manual

Esta opção não é recomendada, mas pode ser usada eventualmente para que possamos inspecionar o código C gerado. O módulo distutils executa estes passos automaticamente e verifica se os arquivos mudaram, compilando somente as partes necessárias. Em geral, é melhor usar distutils do que compilar manualmente.

O seguinte comando gera um arquivo gradiente.c que contém o resultado da "tradução" feita pelo Cython. Como na utilização em notebooks, a opção -a pode ser usada para gerar um relatório que mostra a utilização do interpretador pelo código gerado.

$cython gradiente.pyx

Então, compilamos a extensão usando

$ gcc -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -shared -I(caminho_includes_python) -o gradiente.(so|pyd) gradiente.c

No anaconda python, o caminho usado é anaconda_dir/include/pythonX.Ym, onde X.Y é a sua versão do python instalada.


Com isto, vimos como utilizar código em Cython não só no iPython notebook, mas também em qualquer script Python. Nos próximos textos vamos explorar mais a relação entre o Cython e o interpretador, focando em criar programas que explicitamente liberam o interpretador Python. Até lá!

Nenhum comentário:

Postar um comentário