python - transpuesta - numpy tutorial español pdf
El tiempo de ejecución de la función de resto(%) en arrays numpy es mucho más largo que el cálculo del resto manual (1)
En los últimos días he estado trabajando para mejorar el tiempo de ejecución de una función de python que requiere muchos usos de la función de resto (%) entre otras cosas. Mi caso de prueba principal es sobre una matriz numpy de 80,000 elementos (monótonamente creciente), con 10000 iteraciones, aunque también lo he probado en otros tamaños.
Finalmente, llegué a un punto en el que la función de resto es un gran cuello de botella y probé varias soluciones. Este es el comportamiento que encontré al ejecutar el siguiente código:
import numpy as np
import time
a = np.random.rand(80000)
a = np.cumsum(a)
d = 3
start_time1 = time.time()
for i in range(10000):
b = a % d
d += 0.001
end_time1 = time.time()
d = 3
start_time2 = time.time()
for i in range(10000):
b = a - (d * np.floor(a / d))
d += 0.001
end_time2 = time.time()
print((end_time1 - start_time1) / 10000)
print((end_time2 - start_time2) / 10000)
La salida es:
0.0031344462633132934
0.00022937238216400147
al aumentar el tamaño de la matriz a 800,000:
0.014903099656105041
0.010498356819152833
(Para esta publicación, ejecuté el código solo una vez para la salida real, mientras trato de comprender el problema, obtuve estos resultados de manera constante).
Si bien esto resuelve mi problema de tiempo de ejecución, me cuesta entender por qué. ¿Me estoy perdiendo de algo? La única diferencia que se me ocurre es la sobrecarga de una llamada de función adicional, pero el primer caso es bastante extremo (y 1.5 veces el tiempo de ejecución tampoco es lo suficientemente bueno), y si ese fuera el caso, pensaría que la existencia de La función np.remainder
no tiene sentido.
Edición: Intenté probar el mismo código con bucles no numpy:
import numpy as np
import time
def pythonic_remainder(array, d):
b = np.zeros(len(array))
for i in range(len(array)):
b[i] = array[i] % d
def split_pythonic_remainder(array, d):
b = np.zeros(len(array))
for i in range(len(array)):
b[i] = array[i] - (d * np.floor(array[i] / d))
def split_remainder(a, d):
return a - (d * np.floor(a / d))
def divide(array, iterations, action):
d = 3
for i in range(iterations):
b = action(array, d)
d += 0.001
a = np.random.rand(80000)
a = np.cumsum(a)
start_time = time.time()
divide(a, 10000, split_remainder)
print((time.time() - start_time) / 10000)
start_time = time.time()
divide(a, 10000, np.remainder)
print((time.time() - start_time) / 10000)
start_time = time.time()
divide(a, 10000, pythonic_remainder)
print((time.time() - start_time) / 10000)
start_time = time.time()
divide(a, 10000, split_pythonic_remainder)
print((time.time() - start_time) / 10000)
El resultado que obtengo es:
0.0003770533800125122
0.003932329940795899
0.018835473942756652
0.10940513386726379
Me parece interesante que lo contrario sea cierto en el caso no numpy.
Mi mejor hipótesis es que su instalación NumPy está utilizando un modo no fmod
dentro del cálculo de %
. Este es el por qué.
Primero, no puedo reproducir sus resultados en una versión instalada normal de PIP de NumPy 1.15.1. Solo obtengo una diferencia de rendimiento del 10% (asdf.py contiene su código de tiempo):
$ python3.6 asdf.py
0.0006543657302856445
0.0006025806903839111
Puedo reproducir una discrepancia de rendimiento importante con una compilación manual ( python3.6 setup.py build_ext --inplace -j 4
) de v1.15.1 de un clon del repositorio NumPy Git, sin embargo:
$ python3.6 asdf.py
0.00242799973487854
0.0006397026300430298
Esto sugiere que el %
de mi compilación instalada en pip está mejor optimizado que mi compilación manual o lo que ha instalado.
Mirando bajo el capó, es tentador observar la implementation del %
punto flotante en NumPy y culpar a la desaceleración del cálculo floordiv innecesario ( npy_divmod@c@
calcula ambos //
y %
):
NPY_NO_EXPORT void
@TYPE@_remainder(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
{
BINARY_LOOP {
const @type@ in1 = *(@type@ *)ip1;
const @type@ in2 = *(@type@ *)ip2;
npy_divmod@c@(in1, in2, (@type@ *)op1);
}
}
pero en mis experimentos, eliminar el floordiv no proporcionó ningún beneficio. Parece bastante fácil que un compilador se optimice, así que tal vez se optimizó, o tal vez fue solo una fracción insignificante del tiempo de ejecución en primer lugar.
En lugar de floordiv, concentrémonos en una sola línea en npy_divmod@c@
, la llamada fmod
:
mod = npy_fmod@c@(a, b);
Este es el cálculo del resto inicial, antes del manejo de casos especiales y el ajuste del resultado para que coincida con el signo del operando de la derecha. Si comparamos el rendimiento de %
con numpy.fmod
en mi compilación manual:
>>> import timeit
>>> import numpy
>>> a = numpy.arange(1, 8000, dtype=float)
>>> timeit.timeit(''a % 3'', globals=globals(), number=1000)
0.3510419335216284
>>> timeit.timeit(''numpy.fmod(a, 3)'', globals=globals(), number=1000)
0.33593094255775213
>>> timeit.timeit(''a - 3*numpy.floor(a/3)'', globals=globals(), number=1000)
0.07980139832943678
Vemos que fmod
parece ser responsable de casi todo el tiempo de ejecución de %
.
No he desmontado el binario generado ni lo he revisado en un depurador de nivel de instrucción para ver exactamente qué se ejecuta, y por supuesto, no tengo acceso a su máquina o su copia de NumPy. Sin embargo, a partir de la evidencia anterior, fmod
parece un culpable bastante probable.