6.19. Prestanda

Samma designbeslut som gör numpy snabbt på kameran – biblioteksanrop på hela arrayer, packade typade buffertar, vyer som delar data med sin källa – exponerar också en uppsättning vanor som är värda att känna till. Sidan Form och strides har redan gått igenom regeln om layout för den sista axeln; den här sidan katalogiserar de allokerings- och dtype-vanor som spelar störst roll i en strömmande loop.

6.19.1. Välj en rimlig dtype

Standard-dtype för varje konstruktor är float. För data som naturligt är 8-bitars eller 16-bitars – ADC-sampel, bildpixlar, sensoravläsningar – skicka dtype= explicit till en av heltalstyperna:

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

RAM-besparingen är 2x för uint16 och 4x för uint8 jämfört med 4-bytes-standarden float. Beräkningarna går också snabbare eftersom heltalskodvägarna inuti numpy är snävare än de generiska float-vägarna. Regeln om heltalsspill som täcks i Dtyper gäller – konvertera till en bredare typ före aritmetik som kan spilla över.

6.19.2. Föredra en ndarray framför en iterabel

De flesta reduktioner och universella funktioner accepterar antingen en iterabel eller en ndarray

np.sum([1, 2, 3, 4, 5])               # works, but slow
np.sum(np.array([1, 2, 3, 4, 5]))     # ~3x faster

Den iterabla formen tvingar numpy att stega igenom indata ett Python-objekt i taget och konvertera vart och ett till ett tal innan det kan användas. Mot en ndarray är konverteringen redan gjord och anropet löper rakt igenom den packade bufferten.

När samma data används mer än en gång, bygg ndarray en gång och skicka runt den. När datan endast finns som en Python-lista och konsumeras en gång kan konverteringskostnaden överväga uppsnabbningen – konstruktorn array() måste själv gå igenom listan och allokera.

6.19.3. Föredra vyer framför kopior

Slicing, indexering längs en enda axel av en array med högre rang, reshape(), transpose() och frombuffer() returnerar alla vyer som delar data med källan. De är i praktiken gratis.

copy(), flatten(), boolesk indexering (a[mask]) och alla aritmetiska uttryck allokerar en kopia. Ta till dem endast när en oberoende buffert verkligen behövs.

När du är osäker skriver ndinfo() ut platsen för den underliggande bufferten; två arrayer som rapporterar samma adress delar sin data. Den fullständiga tabellen över vy kontra kopia finns på Vyer och kopior.

6.19.4. Allokera en gång, skriv sedan

Den enskilt största prestandafällan på kameran är att allokera nya arrayer inuti en loop som körs många gånger per sekund. Varje ny ndarray ber kameran om RAM, och frekventa nyallokeringar slösar bort det.

De flesta universella funktioner accepterar out= så att resultatet kan skrivas in i en array som redan finns:

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() accepterar buffer= av samma anledning; spectrogram() och konverterare av typen from_int32_buffer() accepterar både out= och scratchpad=. Allokera allt en gång och återanvänd det.

6.19.5. Använd operatorer på plats

b = b + 1 allokerar ett temporärt värde av storleken b, kopierar och tilldelar på nytt. b += 1 modifierar b direkt:

# makes a temporary
b = b + 1

# no temporary
b += 1

Samma idé gäller sammansatta uttryck. a + b * c allokerar ett temporärt värde för b * c. Att dela upp uttrycket i enkla deltilldelningar som skriver in i en förallokerad buffert eliminerar de temporära värdena:

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

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

6.19.6. Bygg resultatet, lägg inte till i det

ndarray har inget append – med avsikt. Att låta en array växa skulle innebära att allokera en ny, större buffert och kopiera in det gamla innehållet i den. På en mikrokontroller, förallokera den slutliga storleken och fyll den:

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

När N verkligen inte är känt i förväg, skriv till en Python-list och konvertera en gång i slutet med array().

6.19.7. Slicetilldelning istället för nya arrayer

Många mönster av typen ”bygg en ny array från bitar av andra” kan uttryckas som slicetilldelningar in i en förallokerad buffert istället för en ny allokering vid varje anrop.

Ett glidande fönster över en ström av sampel – grunden för ett glidande medelvärdesfilter – är det kanoniska fallet. Bufferten håller de senaste N samplen; varje iteration släpper det äldsta och lägger till det nyaste. Den uppenbara formen bygger om bufferten vid varje iteration:

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

Det är en ny allokering – och en kopia av N - 1 element – per sampel. Slicetilldelningsformen skiftar på plats:

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:] är den intressanta raden: två överlappande vyer in i samma buffert, där den högra slicen läses från ena änden och skrivs till den andra. numpy går igenom det underliggande minnet i den ordning som gör skiftet på plats säkert. Ingen ny array allokeras någonsin inuti loopen.

6.19.8. Se upp för booleska masker i strömmande loopar

Boolesk indexering och where() producerar en ny array vid varje anrop – storleken på resultatet beror på datan, så ingen förallokerad buffert kan absorbera allokeringen. Upprepad maskbyggnad i en strömmande loop fyller RAM med engångsarrayer. Ett periodiskt gc.collect() återvinner utrymmet:

import gc

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

Samma förbehåll gäller sammansatta booleska uttryck som (a > lo) & (a < hi) – varje operator allokerar en ny bool-array. När en mask återanvänds, bygg den en gång och behåll den:

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