Hay numexpr , numba y cython , el objetivo de esta respuesta es tener en cuenta estas posibilidades.
Pero primero expongamos lo obvio: no importa cómo mapees una función de Python en una matriz numpy, sigue siendo una función de Python, eso significa para cada evaluación:
- El elemento numpy-array debe convertirse en un objeto Python (por ejemplo, un
Float
).
- Todos los cálculos se realizan con objetos Python, lo que significa tener la sobrecarga del intérprete, el despacho dinámico y los objetos inmutables.
Entonces, qué maquinaria se usa para recorrer el conjunto no juega un papel importante debido a la sobrecarga mencionada anteriormente: se mantiene mucho más lenta que el uso de la funcionalidad incorporada de numpy.
Echemos un vistazo al siguiente ejemplo:
# 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
se selecciona como representante de la clase de enfoques de la función de python puro. Usando perfplot
(vea el código en el apéndice de esta respuesta) obtenemos los siguientes tiempos de ejecución:
Podemos ver que el enfoque numpy es 10x-100x más rápido que la versión pura de Python. La disminución del rendimiento para tamaños de matriz más grandes probablemente se deba a que los datos ya no se ajustan al caché.
También vale la pena mencionar que vectorize
también usa mucha memoria, por lo que a menudo el uso de la memoria es el cuello de botella (consulte la pregunta SO relacionada ). También tenga en cuenta que la documentación de Numpy np.vectorize
dice que "se proporciona principalmente por conveniencia, no por desempeño".
Deben usarse otras herramientas, cuando se desea rendimiento, además de escribir una extensión C desde cero, existen las siguientes posibilidades:
A menudo se escucha que el rendimiento de numpy es tan bueno como es posible, porque es puro C debajo del capó. ¡Sin embargo, hay mucho margen de mejora!
La versión numpy vectorizada utiliza mucha memoria adicional y accesos a la memoria. Numexp-library intenta enlosar las matrices numpy y así obtener una mejor utilización de la caché:
# 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")
Lleva a la siguiente comparación:
No puedo explicar todo en el diagrama anterior: podemos ver una sobrecarga mayor para numexpr-library al principio, pero debido a que utiliza mejor el caché, ¡es aproximadamente 10 veces más rápido para matrices más grandes!
Otro enfoque es compilar jit la función y así obtener un UFunc puro en C real. Este es el enfoque de 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
Es 10 veces más rápido que el enfoque numpy original:
Sin embargo, la tarea es vergonzosamente paralelizable, por lo que también podríamos usarla prange
para calcular el ciclo en 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 se esperaba, la función paralela es más lenta para entradas más pequeñas, pero más rápida (casi factor 2) para tamaños más grandes:
Mientras que numba se especializa en optimizar operaciones con matrices numpy, Cython es una herramienta más general. Es más complicado extraer el mismo rendimiento que con numba: a menudo se reduce a llvm (numba) frente al 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
Cython resulta en funciones algo más lentas:
Conclusión
Obviamente, probar solo una función no prueba nada. También se debe tener en cuenta que, para el ejemplo de función elegido, el ancho de banda de la memoria era el cuello de botella para tamaños mayores de 10 ^ 5 elementos, por lo que tuvimos el mismo rendimiento para numba, numexpr y cython en esta región.
Al final, la respuesta definitiva depende del tipo de función, hardware, distribución de Python y otros factores. Por ejemplo Anaconda-de distribución utiliza VML de Intel para funciones de numpy y por lo tanto supera a numba (a menos que utiliza SVML, ver este SO-post ) fácilmente para funciones trascendentales como exp
, sin
, cos
y similares - véase, por ejemplo la siguiente SO-post .
Sin embargo, a partir de esta investigación y de mi experiencia hasta el momento, afirmaría que la numba parece ser la herramienta más fácil con el mejor rendimiento siempre que no se involucren funciones trascendentales.
Trazado de tiempos de ejecución con 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)'
)