¿Por qué no es perezoso “numpy.any” (cortocircuito)

No entiendo por qué aún no se ha hecho una optimización tan básica:

In [1]: %timeit np.ones(10**6).any() 100 loops, best of 3: 7.32 ms per loop In [2]: %timeit np.ones(10**7).any() 10 loops, best of 3: 59.7 ms per loop 

Se analiza toda la matriz, incluso si la conclusión es una evidencia en el primer elemento.

Es una regresión de rendimiento no fijada. NumPy número 3446. En realidad, hay una lógica de cortocircuito , pero un cambio en la maquinaria ufunc.reduce introdujo un bucle externo innecesario basado en fragmentos alrededor de la lógica de cortocircuito, y ese bucle externo no sabe cómo cortocircuitar. Puedes ver una explicación de la maquinaria de fragmentación aquí .

Sin embargo, los efectos de cortocircuito no habrían aparecido en su prueba incluso sin la regresión. Primero, está cronometrando la creación de la matriz, y segundo, no creo que alguna vez pongan la lógica de cortocircuito para cualquier tipo de entrada, pero booleano. A partir de la discusión, parece que los detalles de la maquinaria de reducción numpy.any detrás de numpy.any hubiera hecho eso difícil.

La discusión argmin el punto sorprendente de que los métodos argmin y argmax parecen cortocircuitarse para la entrada booleana. Una prueba rápida muestra que a partir de NumPy 1.12 (no es la versión más reciente, pero sí la versión actualmente en Ideone), x[x.argmax()] cortocircuitos, y supera a x.any() y x.max() para la entrada booleana unidimensional no importa si la entrada es pequeña o grande y no importa si el cortocircuito se amortiza. ¡Extraño!

Hay un precio que se paga por el cortocircuito. Necesitas introducir sucursales en tu código.

El problema con las sucursales (p. Ej., Declaraciones) es que pueden ser más lentos que usar operaciones alternativas (sin sucursales) y, a continuación, también tiene una predicción de ramificación que podría incluir una sobrecarga significativa.

Además, dependiendo del comstackdor y el procesador, el código sin sucursales podría utilizar la vectorización del procesador. No soy un experto en esto, pero tal vez algún tipo de SIMD o SSE?

Usaré numba aquí porque el código es fácil de leer y es lo suficientemente rápido para que el rendimiento cambie en función de estas pequeñas diferencias:

 import numba as nb import numpy as np @nb.njit def any_sc(arr): for item in arr: if item: return True return False @nb.njit def any_not_sc(arr): res = False for item in arr: res |= item return res arr = np.zeros(100000, dtype=bool) assert any_sc(arr) == any_not_sc(arr) %timeit any_sc(arr) # 126 µs ± 7.12 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) %timeit any_not_sc(arr) # 15.5 µs ± 962 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) %timeit arr.any() # 31.1 µs ± 184 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each) 

Es casi 10 veces más rápido en el peor de los casos sin sucursales. Pero en el mejor de los casos, la función de cortocircuito es mucho más rápida:

 arr = np.zeros(100000, dtype=bool) arr[0] = True %timeit any_sc(arr) # 1.97 µs ± 12.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) %timeit any_not_sc(arr) # 15.1 µs ± 368 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) %timeit arr.any() # 31.2 µs ± 2.23 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) 

Entonces, es una pregunta qué caso debería optimizarse: ¿El mejor caso? ¿El peor caso? El caso promedio (¿cuál es el caso promedio con any )?

Podría ser que los desarrolladores de NumPy quisieran optimizar el peor de los casos y no el mejor. O simplemente no les importaba? O tal vez solo querían un rendimiento “predecible” en cualquier caso.


Solo una nota en tu código: mides el tiempo que lleva crear una matriz y el tiempo que lleva ejecutar any . ¡Si any fuera de cortocircuito, no lo habrías notado con tu código!

 %timeit np.ones(10**6) # 9.12 ms ± 635 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) %timeit np.ones(10**7) # 86.2 ms ± 5.15 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) 

Para los tiempos concluyentes que respaldan tu pregunta, deberías haber usado esto en su lugar:

 arr1 = np.ones(10**6) arr2 = np.ones(10**7) %timeit arr1.any() # 4.04 ms ± 121 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) %timeit arr2.any() # 39.8 ms ± 1.34 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)