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.vectorizeafirma 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.vectorizemas 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)',
)