Quando a matriz 2d (ou matriz nd) é contígua em C ou F, essa tarefa de mapear uma função em uma matriz 2d é praticamente a mesma que a tarefa de mapear uma função em uma matriz 1d - nós apenas tem que vê-lo dessa maneira, por exemplo, via np.ravel(A,'K')
.
A possível solução para a matriz 1d foi discutida, por exemplo, aqui .
No entanto, quando a memória do 2d-array não é contígua, a situação é um pouco mais complicada, porque se deseja evitar possíveis falhas de cache se o eixo for tratado na ordem errada.
A Numpy já possui um mecanismo para processar os eixos na melhor ordem possível. Uma possibilidade de usar esta maquinaria é np.vectorize
. No entanto, a documentação da numpy np.vectorize
afirma que ela é "fornecida principalmente por conveniência, não por desempenho" - uma função python lenta permanece uma função python lenta com toda a sobrecarga associada! Outra questão é seu enorme consumo de memória - veja, por exemplo, este SO-post .
Quando alguém deseja executar uma função C, mas usar o maquinário de numpy, uma boa solução é usar o numba para criação de ufuncs, por exemplo:
# 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
Ele bate facilmente, np.vectorize
mas também quando a mesma função seria executada como multiplicação / adição de array numpy, ou seja,
# 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"
Veja o apêndice desta resposta para o código de medição do tempo:
A versão do Numba (verde) é cerca de 100 vezes mais rápida que a função python (ou seja np.vectorize
), o que não é surpreendente. Mas também é 10 vezes mais rápido que a funcionalidade numpy, porque a versão numbas não precisa de matrizes intermediárias e, portanto, usa o cache com mais eficiência.
Embora a abordagem não-funcional da numba seja uma boa alternativa entre usabilidade e desempenho, ela ainda não é a melhor que podemos fazer. No entanto, não existe uma bala de prata ou uma abordagem melhor para qualquer tarefa - é preciso entender quais são as limitações e como elas podem ser mitigadas.
Por exemplo, para as funções transcendentes (por exemplo exp
, sin
, cos
) numba não fornece quaisquer vantagens em relação aos da numpy np.exp
(não há matrizes temporários criados - a principal fonte do aumento de velocidade). No entanto, minha instalação do Anaconda utiliza o VML da Intel para vetores maiores que 8192 - apenas não pode ser feito se a memória não for contígua. Portanto, pode ser melhor copiar os elementos para uma memória contígua para poder usar o VML da Intel:
import numba as nb
@nb.vectorize(target="cpu")
def nb_vexp(x):
return np.exp(x)
def np_copy_exp(x):
copy = np.ravel(x, 'K')
return np.exp(copy).reshape(x.shape)
Para a equidade da comparação, desativei a paralelização da VML (consulte o código no apêndice):
Como se pode ver, uma vez que a VML entra em ação, a sobrecarga da cópia é mais do que compensada. No entanto, uma vez que os dados se tornam grandes demais para o cache L3, a vantagem é mínima, pois as tarefas se tornam novamente vinculadas à largura de banda da memória.
Por outro lado, a numba também poderia usar o SVML da Intel, conforme explicado neste post :
from llvmlite import binding
# set before import
binding.set_option('SVML', '-vector-library=SVML')
import numba as nb
@nb.vectorize(target="cpu")
def nb_vexp_svml(x):
return np.exp(x)
e usando VML com paralelização produz:
A versão do numba tem menos sobrecarga, mas, para alguns tamanhos, o VML supera o SVML, apesar da sobrecarga adicional de cópia - o que não é uma surpresa, já que os ufuncs do numba não são paralelos.
Listagens:
A. comparação da função polinomial:
import perfplot
perfplot.show(
setup=lambda n: np.random.rand(n,n)[::2,::2],
n_range=[2**k for k in range(0,12)],
kernels=[
f,
vf,
nb_vf
],
logx=True,
logy=True,
xlabel='len(x)'
)
B. comparação de exp
:
import perfplot
import numexpr as ne # using ne is the easiest way to set vml_num_threads
ne.set_vml_num_threads(1)
perfplot.show(
setup=lambda n: np.random.rand(n,n)[::2,::2],
n_range=[2**k for k in range(0,12)],
kernels=[
nb_vexp,
np.exp,
np_copy_exp,
],
logx=True,
logy=True,
xlabel='len(x)',
)