Existem numexpr , numba e cython , o objetivo desta resposta é levar essas possibilidades em consideração.
Mas primeiro vamos declarar o óbvio: não importa como você mapeie uma função Python em um array numpy, ela permanece uma função Python, o que significa para todas as avaliações:
- O elemento numpy-array deve ser convertido em um objeto Python (por exemplo, a
Float
).
- todos os cálculos são feitos com objetos Python, o que significa ter a sobrecarga de intérprete, despacho dinâmico e objetos imutáveis.
Portanto, quais máquinas são usadas para fazer um loop na matriz não desempenham um grande papel por causa da sobrecarga mencionada acima - ele permanece muito mais lento do que usar a funcionalidade incorporada do numpy.
Vamos dar uma olhada no seguinte exemplo:
# numpy-functionality
def f(x):
return x+2*x*x+4*x*x*x
# python-function as ufunc
import numpy as np
vf=np.vectorize(f)
vf.__name__="vf"
np.vectorize
é escolhido como um representante da classe de abordagens da função python puro. Usando perfplot
(veja o código no apêndice desta resposta) obtemos os seguintes tempos de execução:
Podemos ver que a abordagem numpy é 10x-100x mais rápida que a versão python pura. A diminuição do desempenho para tamanhos de matriz maiores é provavelmente porque os dados não se ajustam mais ao cache.
Vale mencionar também, que vectorize
também usa muita memória, e muitas vezes o uso da memória é o gargalo (consulte a pergunta SO relacionada ). Observe também que a documentação da numpy np.vectorize
afirma que é "fornecida principalmente por conveniência, não por desempenho".
Outras ferramentas devem ser usadas, quando o desempenho é desejado, além de escrever uma extensão C a partir do zero, existem as seguintes possibilidades:
Ouve-se com frequência que o desempenho numpy é tão bom quanto ele ganha, porque é puro C sob o capô. No entanto, há muito espaço para melhorias!
A versão numpy vetorizada usa muita memória e acessos à memória adicionais. A biblioteca Numexp tenta agrupar as matrizes numpy e, assim, obter uma melhor utilização do cache:
# less cache misses than numpy-functionality
import numexpr as ne
def ne_f(x):
return ne.evaluate("x+2*x*x+4*x*x*x")
Leva à seguinte comparação:
Não posso explicar tudo no gráfico acima: podemos ver uma sobrecarga maior para a biblioteca numexpr no início, mas como ela utiliza melhor o cache, é cerca de 10 vezes mais rápida para matrizes maiores!
Outra abordagem é compilar rapidamente a função e, assim, obter um UFunc C puro puro. Esta é a abordagem da numba:
# runtime generated C-function as ufunc
import numba as nb
@nb.vectorize(target="cpu")
def nb_vf(x):
return x+2*x*x+4*x*x*x
É 10 vezes mais rápido que a abordagem numpy original:
No entanto, a tarefa é embaraçosamente paralelelizável, portanto, também poderíamos usar prange
para calcular o loop em paralelo:
@nb.njit(parallel=True)
def nb_par_jitf(x):
y=np.empty(x.shape)
for i in nb.prange(len(x)):
y[i]=x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y
Como esperado, a função paralela é mais lenta para entradas menores, mas mais rápida (quase fator 2) para tamanhos maiores:
Enquanto a numba se especializa em otimizar operações com matrizes numpy, o Cython é uma ferramenta mais geral. É mais complicado extrair o mesmo desempenho que o numba - geralmente é o llvm (numba) versus o compilador local (gcc / MSVC):
%%cython -c=/openmp -a
import numpy as np
import cython
#single core:
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_f(double[::1] x):
y_out=np.empty(len(x))
cdef Py_ssize_t i
cdef double[::1] y=y_out
for i in range(len(x)):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
#parallel:
from cython.parallel import prange
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_par_f(double[::1] x):
y_out=np.empty(len(x))
cdef double[::1] y=y_out
cdef Py_ssize_t i
cdef Py_ssize_t n = len(x)
for i in prange(n, nogil=True):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
O Cython resulta em funções um pouco mais lentas:
Conclusão
Obviamente, testar apenas uma função não prova nada. Também devemos ter em mente que, para o exemplo de função escolhido, a largura de banda da memória era o gargalo para tamanhos maiores que 10 ^ 5 elementos - portanto, tivemos o mesmo desempenho para numba, numexpr e cython nessa região.
No final, a resposta definitiva depende do tipo de função, hardware, distribuição Python e outros fatores. Por exemplo Anaconda-de distribuição usa VML da Intel para funções de numpy e assim Supera numba (a menos que ele usa SVML, consulte este SO-post ) facilmente para funções transcendentais como exp
, sin
, cos
e semelhante - ver, por exemplo o seguinte SO-post .
No entanto, a partir desta investigação e da minha experiência até agora, eu afirmaria que o numba parece ser a ferramenta mais fácil com melhor desempenho, desde que nenhuma função transcendental esteja envolvida.
Plotando tempos de execução com perfplot -package :
import perfplot
perfplot.show(
setup=lambda n: np.random.rand(n),
n_range=[2**k for k in range(0,24)],
kernels=[
f,
vf,
ne_f,
nb_vf, nb_par_jitf,
cy_f, cy_par_f,
],
logx=True,
logy=True,
xlabel='len(x)'
)