6.19. Rendimiento¶
Las mismas decisiones de diseño que hacen que numpy sea rápido en la cámara – llamadas a la biblioteca sobre arreglos completos, búferes tipados y compactos, vistas que comparten datos con su origen – también dan lugar a una serie de hábitos que conviene conocer. La página Forma y pasos ya cubrió la regla de disposición del último eje; esta página recopila los hábitos de asignación de memoria y de dtype que más importan en un bucle de transmisión.
6.19.1. Elige un dtype razonable¶
El dtype por defecto de cada constructor es float. Para datos que son naturalmente de 8 bits o 16 bits – muestras del ADC, píxeles de imagen, lecturas de sensores – pasa dtype= explícitamente con uno de los tipos enteros:
adc = np.array(adc_samples, dtype=np.uint16)
El ahorro de RAM es de 2x para uint16 y de 4x para uint8 frente al valor por defecto float de 4 bytes. El cálculo también se ejecuta más rápido porque las rutas de código para enteros dentro de numpy son más eficientes que las genéricas de coma flotante. Se aplica la regla de desbordamiento de enteros descrita en Dtypes – convierte a un tipo más ancho antes de realizar aritmética que pudiera desbordarse.
6.19.2. Prefiere un ndarray a un iterable¶
La mayoría de las reducciones y funciones universales aceptan tanto un iterable como un ndarray:
np.sum([1, 2, 3, 4, 5]) # works, but slow
np.sum(np.array([1, 2, 3, 4, 5])) # ~3x faster
La forma iterable obliga a numpy a recorrer la entrada un objeto de Python a la vez, convirtiendo cada uno a número antes de poder usarlo. Frente a un ndarray la conversión ya está hecha y la llamada se ejecuta directamente sobre el búfer compacto.
Cuando los mismos datos se usan más de una vez, construye el ndarray una sola vez y pásalo de un sitio a otro. Cuando los datos existen únicamente como una lista de Python y se consumen una sola vez, el coste de la conversión puede superar la mejora de velocidad – el propio constructor array() tiene que recorrer la lista y asignar memoria.
6.19.3. Prefiere vistas a copias¶
El rebanado, la indexación de un solo eje de un arreglo de rango superior, reshape(), transpose() y frombuffer() devuelven todos vistas que comparten datos con el origen. Son prácticamente gratuitas.
copy(), flatten(), la indexación booleana (a[mask]) y cualquier expresión aritmética asignan una copia. Recurre a ellas solo cuando realmente se necesite un búfer independiente.
En caso de duda, ndinfo() imprime la ubicación del búfer subyacente; dos arreglos que reporten la misma dirección comparten sus datos. La tabla completa de vista frente a copia está en Vistas y copias.
6.19.4. Asigna una vez y luego escribe¶
El mayor inconveniente de rendimiento en la cámara es asignar arreglos nuevos dentro de un bucle que se ejecuta muchas veces por segundo. Cada nuevo ndarray solicita RAM a la cámara, y las asignaciones nuevas frecuentes la desperdician.
La mayoría de las funciones universales aceptan out= para que el resultado pueda escribirse en un arreglo que ya existe:
x = np.linspace(0, 2 * np.pi, num=512)
y = np.zeros(512) # allocate once
while True:
np.sin(x, out=y)
# use y ...
image.Image.to_ndarray() acepta buffer= por la misma razón; spectrogram() y los conversores del estilo de from_int32_buffer() aceptan tanto out= como scratchpad=. Asígnalo todo una sola vez y reutilízalo.
6.19.5. Usa operadores in situ¶
b = b + 1 asigna un temporal del tamaño de b, copia y reasigna. b += 1 modifica b directamente:
# makes a temporary
b = b + 1
# no temporary
b += 1
La misma idea se aplica a las expresiones compuestas. a + b * c asigna un temporal para b * c. Dividir la expresión en subasignaciones simples que escriben en un búfer preasignado elimina los temporales:
# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2
# zero temporaries
out[:] = a
out += b
out *= 2
6.19.6. Construye el resultado, no lo amplíes¶
ndarray no tiene append – a propósito. Hacer crecer un arreglo implicaría asignar un búfer nuevo y más grande y copiar en él el contenido anterior. En un microcontrolador, preasigna el tamaño final y rellénalo:
out = np.zeros(N, dtype=np.float)
for i in range(N):
out[i] = some_calculation(i)
Cuando N realmente no se conoce de antemano, escribe en una list de Python y conviértela una sola vez al final con array().
6.19.7. Asignación por rebanadas en lugar de arreglos nuevos¶
Muchos patrones de «construir un arreglo nuevo a partir de partes de otros» pueden expresarse como asignaciones por rebanadas en un búfer preasignado, en lugar de una asignación nueva en cada llamada.
Una ventana deslizante sobre un flujo de muestras – la base de un filtro de media móvil – es el caso canónico. El búfer guarda las últimas N muestras; cada iteración descarta la más antigua y añade la más reciente. La forma obvia reconstruye el búfer en cada iteración:
while True:
sample = read_sample()
buf = np.concatenate((buf[1:], # new buffer every loop
np.array([sample])))
avg = np.mean(buf)
Eso es una asignación nueva – y una copia de N - 1 elementos – por muestra. La forma con asignación por rebanadas desplaza los datos in situ:
N = 16
buf = np.zeros(N, dtype=np.float) # allocate once
while True:
sample = read_sample()
buf[:-1] = buf[1:] # shift left by one
buf[-1] = sample # append at the end
avg = np.mean(buf)
buf[:-1] = buf[1:] es la línea interesante: dos vistas solapadas sobre el mismo búfer, la rebanada del lado derecho se lee de un extremo y se escribe en el otro. numpy recorre la memoria subyacente en el orden que hace seguro el desplazamiento in situ. Nunca se asigna un arreglo nuevo dentro del bucle.
6.19.8. Cuidado con las máscaras booleanas en los bucles de transmisión¶
La indexación booleana y where() producen un arreglo nuevo en cada llamada – el tamaño del resultado depende de los datos, por lo que ningún búfer preasignado puede absorber la asignación. La construcción repetida de máscaras en un bucle de transmisión llena la RAM de arreglos desechables. Un gc.collect() periódico recupera el espacio:
import gc
for i in range(1000):
mask = a < threshold
_ = a[mask]
if i % 100 == 0:
gc.collect()
La misma advertencia se aplica a las expresiones booleanas compuestas como (a > lo) & (a < hi) – cada operador asigna un nuevo arreglo de booleanos. Cuando una máscara se reutiliza, constrúyela una vez y consérvala:
mask = a < threshold
foo[mask] = 0
bar[mask] = 1