6.19. Výkon

Tatáž návrhová rozhodnutí, díky nimž je numpy na kameře rychlý – volání knihovny nad celými poli, zabalené typované buffery, pohledy sdílející data se svým zdrojem – zároveň odhalují sadu návyků, které je dobré znát. Stránka Tvar a kroky již popsala pravidlo rozložení podle poslední osy; tato stránka katalogizuje návyky týkající se alokace a dtype, které jsou nejdůležitější ve smyčce zpracovávající datový proud.

6.19.1. Zvolte rozumný dtype

Výchozí dtype každého konstruktoru je float. U dat, která jsou přirozeně 8bitová nebo 16bitová – vzorky z ADC, pixely obrazu, údaje ze senzoru – předejte explicitně dtype= s jedním z celočíselných typů:

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

Úspora RAM je 2x u uint16 a 4x u uint8 oproti výchozímu 4bajtovému float. Výpočty také běží rychleji, protože celočíselné cesty kódu uvnitř numpy jsou těsnější než obecné cesty pro float. Platí pravidlo přetečení celých čísel popsané na Dtypy – před aritmetikou, která by mohla přetéct, proveďte přetypování na širší typ.

6.19.2. Upřednostněte ndarray před iterovatelným objektem

Většina redukcí a univerzálních funkcí přijímá buď iterovatelný objekt, nebo ndarray

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

Forma s iterovatelným objektem nutí numpy procházet vstup po jednom Python objektu a každý před použitím převádět na číslo. U ndarray je převod již hotový a volání běží přímo přes zabalený buffer.

Pokud se stejná data používají více než jednou, sestavte ndarray jednou a předávejte jej dál. Pokud data existují pouze jako Python seznam a spotřebují se jednou, může cena převodu převážit nad zrychlením – konstruktor array() sám musí projít seznam a alokovat paměť.

6.19.3. Upřednostněte pohledy před kopiemi

Slicing, indexování jediné osy u pole vyšší úrovně, reshape(), transpose() a frombuffer() všechny vracejí pohledy, které sdílejí data se zdrojem. Jsou v podstatě zdarma.

copy(), flatten(), booleovské indexování (a[mask]) a jakýkoli aritmetický výraz alokují kopii. Sáhněte po nich pouze tehdy, když je skutečně potřeba nezávislý buffer.

Pokud si nejste jisti, ndinfo() vypíše umístění podkladového bufferu; dvě pole, která hlásí stejnou adresu, sdílejí svá data. Kompletní tabulka pohled vs. kopie je na Pohledy a kopie.

6.19.4. Alokujte jednou, pak zapisujte

Největším úskalím výkonu na kameře je alokace nových polí uvnitř smyčky, která běží mnohokrát za sekundu. Každý nový ndarray si od kamery vyžádá RAM a časté nové alokace ji plýtvají.

Většina univerzálních funkcí přijímá out=, takže výsledek lze zapsat do již existujícího pole:

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() přijímá buffer= ze stejného důvodu; spectrogram() a převodníky typu from_int32_buffer() přijímají jak out=, tak scratchpad=. Alokujte vše jednou a opakovaně to využívejte.

6.19.5. Používejte operátory pracující na místě

b = b + 1 alokuje dočasné pole velikosti b, zkopíruje a znovu přiřadí. b += 1 upravuje b přímo:

# makes a temporary
b = b + 1

# no temporary
b += 1

Stejná myšlenka platí pro složené výrazy. a + b * c alokuje dočasné pole pro b * c. Rozdělení výrazu na jednoduchá dílčí přiřazení zapisující do předem alokovaného bufferu odstraní dočasná pole:

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

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

6.19.6. Sestavte výsledek, nepřipojujte k němu

ndarray nemá append – záměrně. Růst pole by znamenal alokaci nového, většího bufferu a kopírování starého obsahu do něj. Na mikrokontroléru předem alokujte výslednou velikost a naplňte ji:

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

Pokud N skutečně není dopředu známé, zapisujte do Python objektu list a na konci jednou převeďte pomocí array().

6.19.7. Přiřazení do řezu namísto nových polí

Mnoho vzorů typu „sestav nové pole z částí jiných“ lze vyjádřit jako přiřazení do řezu předem alokovaného bufferu namísto nové alokace při každém volání.

Posuvné okno nad proudem vzorků – základ filtru klouzavého průměru – je kanonický případ. Buffer drží posledních N vzorků; každá iterace zahodí nejstarší a připojí nejnovější. Zřejmá forma přebudovává buffer při každé iteraci:

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

To je nová alokace – a kopie N - 1 prvků – na každý vzorek. Forma s přiřazením do řezu posouvá na místě:

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:] je ten zajímavý řádek: dva překrývající se pohledy do téhož bufferu, pravostranný řez čtený z jednoho konce a zapisovaný na druhý. numpy prochází podkladovou paměť v pořadí, které činí posun na místě bezpečným. Uvnitř smyčky se nikdy nealokuje žádné nové pole.

6.19.8. Pozor na booleovské masky ve smyčkách zpracovávajících datový proud

Booleovské indexování a where() vytvářejí při každém volání nové pole – velikost výsledku závisí na datech, takže žádný předem alokovaný buffer nemůže alokaci pohltit. Opakované sestavování masek ve smyčce zpracovávající datový proud zaplní RAM jednorázovými poli. Periodické gc.collect() paměť uvolní:

import gc

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

Stejná výhrada platí pro složené booleovské výrazy jako (a > lo) & (a < hi) – každý operátor alokuje nové pole typu bool. Pokud se maska opakovaně používá, sestavte ji jednou a uchovejte:

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