in

python: la forma más eficiente de mapear la función sobre una matriz numpy

apple touch icon@2

Existen numexpr, numba y cython alrededor, el objetivo de esta respuesta es tener en cuenta estas posibilidades.

Pero primero digamos 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 de intérprete, despacho dinámico y objetos inmutables.

Entonces, qué maquinaria se usa para recorrer la matriz no juega un papel importante debido a la sobrecarga mencionada anteriormente: permanece mucho más lenta que usar 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 elige como representante de la clase de enfoques de función pura-python. Utilizando perfplot (vea el código en el apéndice de esta respuesta) obtenemos los siguientes tiempos de ejecución:

ingrese la descripción de la imagen aquí

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 arreglos de mayor tamaño se debe probablemente a que los datos ya no se ajustan a la memoria caché.

Vale la pena mencionar también, que vectorize también usa mucha memoria, por lo que a menudo el uso de la memoria es el cuello de botella (vea la pregunta SO relacionada). También tenga en cuenta, la documentación de ese numpy en np.vectorize establece que «se proporciona principalmente por conveniencia, no por rendimiento».

Se deben usar 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 numérico es tan bueno como es posible, porque es pura C bajo el capó. ¡Sin embargo, hay mucho margen de mejora!

La versión de 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")

Conduce a la siguiente comparación:

ingrese la descripción de la imagen aquí

No puedo explicar todo en el gráfico anterior: podemos ver una sobrecarga más grande para la biblioteca numexpr 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 en jit la función y así obtener una UFunc C pura 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 numérico original:

ingrese la descripción de la imagen aquí


Sin embargo, la tarea es vergonzosamente paralelizable, por lo que también podríamos usar prange para calcular el bucle 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 era de esperar, 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:

ingrese la descripción de la imagen aquí


Si bien 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 debe 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 da como resultado funciones algo más lentas:

ingrese la descripción de la imagen aquí


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 la botella para tamaños superiores a 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, la distribución de Anaconda usa VML de Intel para las funciones de numpy y, por lo tanto, supera a numba (a menos que use SVML, vea esta publicación SO) fácilmente para funciones trascendentales como exp, sin, cos y similares – ver, por ejemplo, el siguiente SO-post.

Sin embargo, a partir de esta investigación y de mi experiencia hasta ahora, diría que numba parece ser la herramienta más fácil con el mejor rendimiento siempre que no estén involucradas funciones trascendentales.


Trazar tiempos de ejecución con perfplot-paquete:

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)"
    )

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

60 58410 1512537824

Cancelar la tarea del temporizador en Java

4abe5885ed6d0270832545ee8c1f1f31 1200 80

10 juegos de castigo que valen la pena