6.19. Prestazioni

Le stesse scelte di progettazione che rendono numpy veloce sulla camera – chiamate di libreria sull’intero array, buffer tipizzati impacchettati, viste che condividono i dati con la loro sorgente – espongono anche una serie di abitudini che vale la pena conoscere. La pagina Forma e strides ha gia trattato la regola di disposizione dell’ultimo asse; questa pagina cataloga le abitudini relative all’allocazione e al dtype che contano di piu in un ciclo di streaming.

6.19.1. Scegliere un dtype ragionevole

Il dtype predefinito di ogni costruttore e float. Per dati che sono naturalmente a 8 bit o 16 bit – campioni ADC, pixel di immagini, letture di sensori – passa esplicitamente dtype= con uno dei tipi interi:

adc = np.array(adc_samples, dtype=np.uint16)

Il risparmio di RAM e di 2x per uint16 e 4x per uint8 rispetto al float predefinito da 4 byte. Anche i calcoli risultano piu veloci perche i percorsi di codice intero all’interno di numpy sono piu compatti di quelli generici in virgola mobile. Si applica la regola dell’overflow degli interi trattata in Dtype – esegui il cast a un tipo piu ampio prima di un’operazione aritmetica che potrebbe andare in overflow.

6.19.2. Preferire un ndarray a un iterabile

La maggior parte delle riduzioni e delle funzioni universali accetta sia un iterabile sia 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 con iterabile costringe numpy a scorrere l’input un oggetto Python alla volta, convertendo ciascuno in un numero prima di poterlo usare. Con un ndarray la conversione e gia stata fatta e la chiamata scorre direttamente attraverso il buffer impacchettato.

Quando gli stessi dati vengono usati piu di una volta, costruisci l’oggetto ndarray una sola volta e passalo in giro. Quando i dati esistono solo come lista Python e vengono consumati una sola volta, il costo della conversione puo superare il guadagno di velocita – il costruttore array() stesso deve scorrere la lista e allocare.

6.19.3. Preferire le viste alle copie

Lo slicing, l’indicizzazione su un singolo asse di un array di rango superiore, reshape(), transpose() e frombuffer() restituiscono tutti viste che condividono i dati con la sorgente. Sono praticamente gratuite.

copy(), flatten(), l’indicizzazione booleana (a[mask]) e qualsiasi espressione aritmetica allocano una copia. Ricorri a esse solo quando serve davvero un buffer indipendente.

In caso di dubbio, ndinfo() stampa la posizione del buffer sottostante; due array che riportano lo stesso indirizzo condividono i dati. La tabella completa vista contro copia si trova in Viste e copie.

6.19.4. Allocare una volta, poi scrivere

La singola insidia piu grande per le prestazioni sulla camera e l’allocazione di nuovi array all’interno di un ciclo che viene eseguito molte volte al secondo. Ogni nuovo ndarray richiede RAM alla camera, e le frequenti allocazioni ex novo la sprecano.

La maggior parte delle funzioni universali accetta out= cosi che il risultato possa essere scritto in un array gia esistente:

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() accetta buffer= per la stessa ragione; spectrogram() e i convertitori in stile from_int32_buffer() accettano sia out= sia scratchpad=. Alloca tutto una sola volta e riutilizzalo.

6.19.5. Usare gli operatori in-place

b = b + 1 alloca un temporaneo delle dimensioni di b, copia e riassegna. b += 1 modifica b direttamente:

# makes a temporary
b = b + 1

# no temporary
b += 1

La stessa idea si applica alle espressioni composte. a + b * c alloca un temporaneo per b * c. Suddividere l’espressione in semplici sotto-assegnazioni che scrivono in un buffer pre-allocato elimina i temporanei:

# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2

# zero temporaries
out[:]  = a
out    += b
out    *= 2

6.19.6. Costruire il risultato, non aggiungervi elementi

ndarray non ha un metodo append – di proposito. Far crescere un array significherebbe allocare un nuovo buffer piu grande e copiarvi il vecchio contenuto. Su un microcontrollore, pre-alloca la dimensione finale e riempila

out = np.zeros(N, dtype=np.float)
for i in range(N):
    out[i] = some_calculation(i)

Quando N davvero non e noto in anticipo, scrivi in una list Python e converti una sola volta alla fine con array().

6.19.7. Assegnazione di slice invece di nuovi array

Molti schemi del tipo «costruisci un nuovo array a partire da pezzi di altri» possono essere espressi come assegnazioni di slice in un buffer pre-allocato invece di una nuova allocazione a ogni chiamata.

Una finestra mobile su un flusso di campioni – il fondamento di un filtro a media mobile – e il caso canonico. Il buffer contiene gli ultimi N campioni; ogni iterazione elimina il piu vecchio e aggiunge il piu recente. La forma ovvia ricostruisce il buffer a ogni iterazione:

while True:
    sample = read_sample()
    buf = np.concatenate((buf[1:],              # new buffer every loop
                          np.array([sample])))
    avg = np.mean(buf)

Quella e una nuova allocazione – e una copia di N - 1 elementi – per ogni campione. La forma con assegnazione di slice esegue lo spostamento in-place:

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:] e la riga interessante: due viste sovrapposte sullo stesso buffer, la slice di destra letta da un estremo e scritta nell’altro. numpy scorre la memoria sottostante nell’ordine che rende sicuro lo spostamento in-place. Nessun nuovo array viene mai allocato all’interno del ciclo.

6.19.8. Attenzione alle maschere booleane nei cicli di streaming

L’indicizzazione booleana e where() producono un nuovo array a ogni chiamata – la dimensione del risultato dipende dai dati, quindi nessun buffer pre-allocato puo assorbire l’allocazione. La ripetuta costruzione di maschere in un ciclo di streaming riempie la RAM di array usa e getta. Un gc.collect() periodico recupera lo spazio:

import gc

for i in range(1000):
    mask = a < threshold
    _    = a[mask]
    if i % 100 == 0:
        gc.collect()

Lo stesso avvertimento si applica alle espressioni booleane composte come (a > lo) & (a < hi) – ogni operatore alloca un nuovo array bool. Quando una maschera viene riutilizzata, costruiscila una volta e conservala:

mask = a < threshold
foo[mask] = 0
bar[mask] = 1