6.19. Teljesítmény

Ugyanazok a tervezési döntések, amelyek a numpy-t gyorssá teszik a kamerán – a teljes tömbökön végzett függvényhívások, a tömörített típusos pufferek, valamint a forrásukkal adatot megosztó nézetek – egyúttal néhány olyan szokást is napvilágra hoznak, amelyeket érdemes ismerni. A Alak és lépésközök oldal már bemutatta az utolsó tengely elrendezésére vonatkozó szabályt; ez az oldal azokat a foglalási és dtype-szokásokat gyűjti össze, amelyek a legtöbbet számítanak egy streamelő ciklusban.

6.19.1. Válassz ésszerű dtype-ot

Minden konstruktor alapértelmezett dtype-ja a float. Olyan adatokhoz, amelyek természetüknél fogva 8 vagy 16 bitesek – ADC-minták, képpontok, érzékelőadatok –, add meg explicit módon a dtype= paramétert valamelyik egész típussal:

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

A RAM-megtakarítás 2-szeres az uint16 és 4-szeres az uint8 esetén a 4 bájtos float alapértelmezetthez képest. A matematika is gyorsabban fut, mert a numpy egészeket kezelő kódútjai feszesebbek az általános float-os útvonalaknál. A Dtype-ok oldalon tárgyalt egész túlcsordulási szabály érvényes – alakítsd szélesebb típusra a túlcsordulást okozható aritmetika előtt.

6.19.2. Részesítsd előnyben az ndarray-t az iterálhatóval szemben

A legtöbb redukció és univerzális függvény elfogad iterálhatót vagy ndarray-t is:

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

Az iterálható forma arra kényszeríti a numpy-t, hogy a bemeneten egyszerre egy Python-objektumonként lépkedjen végig, mindegyiket számmá alakítva, mielőtt használni tudná. Egy ndarray esetén az átalakítás már megtörtént, és a hívás közvetlenül a tömörített pufferen fut végig.

Ha ugyanazokat az adatokat többször is felhasználod, építsd fel egyszer a ndarray-t, és add tovább. Ha az adatok csak Python-listaként léteznek, és csak egyszer kerülnek felhasználásra, az átalakítás költsége felülmúlhatja a gyorsulást – magának a array() konstruktornak végig kell járnia a listát és foglalnia kell.

6.19.3. Részesítsd előnyben a nézeteket a másolatokkal szemben

A szeletelés, egy magasabb rangú tömb egyetlen tengely menti indexelése, a reshape(), a transpose() és a frombuffer() mind nézeteket ad vissza, amelyek a forrással osztoznak az adatokon. Ezek lényegében ingyenesek.

A copy(), a flatten(), a logikai indexelés (a[mask]) és bármilyen aritmetikai kifejezés másolatot foglal. Csak akkor nyúlj hozzájuk, ha valóban szükség van egy független pufferre.

Ha kétségeid vannak, a ndinfo() kiírja az alapul szolgáló puffer helyét; két tömb, amely ugyanazt a címet jelenti, megosztja az adatait. A teljes nézet kontra másolat táblázat a Nézetek és másolatok oldalon található.

6.19.4. Foglalj egyszer, aztán írj

A kamerán a legnagyobb teljesítménybeli buktató, ha friss tömböket foglalsz egy másodpercenként sokszor lefutó cikluson belül. Minden új ndarray RAM-ot kér a kamerától, és a gyakori friss foglalások elpazarolják azt.

A legtöbb univerzális függvény elfogadja az out= paramétert, így az eredmény egy már létező tömbbe írható:

x = np.linspace(0, 2 * np.pi, num=512)
y = np.zeros(512)        # allocate once

while True:
    np.sin(x, out=y)
    # use y ...

Az image.Image.to_ndarray() ugyanezen okból elfogadja a buffer= paramétert; a spectrogram() és a from_int32_buffer()-stílusú átalakítók mind az out=, mind a scratchpad= paramétert elfogadják. Foglalj mindent egyszer, és használd újra.

6.19.5. Használj helyben módosító operátorokat

A b = b + 1 egy b méretű ideiglenes tömböt foglal, másol, és újraértékadást végez. A b += 1 közvetlenül módosítja a b-t:

# makes a temporary
b = b + 1

# no temporary
b += 1

Ugyanez az ötlet érvényes az összetett kifejezésekre. Az a + b * c egy ideiglenes tömböt foglal a b * c számára. Ha a kifejezést egyszerű, egy előre lefoglalt pufferbe író részértékadásokra bontod, az kiküszöböli az ideiglenes tömböket:

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

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

6.19.6. Építsd fel az eredményt, ne fűzz hozzá

Az ndarray-nek nincs append metódusa – szándékosan. Egy tömb növelése azt jelentené, hogy egy friss, nagyobb puffert kell foglalni, és a régi tartalmat bele kell másolni. Egy mikrovezérlőn foglald le előre a végleges méretet, és töltsd fel

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

Ha az N valóban nem ismert előre, írj egy Python list-be, és alakítsd át egyszer a végén a array() segítségével.

6.19.7. Szeletértékadás új tömbök helyett

Számos „új tömb felépítése mások részeiből” minta kifejezhető egy előre lefoglalt pufferbe történő szeletértékadásként ahelyett, hogy minden híváskor friss foglalás történne.

Egy mintafolyam fölötti gördülő ablak – a mozgóátlag-szűrő alapja – a klasszikus eset. A puffer az utolsó N mintát tárolja; minden iteráció eldobja a legrégebbit, és hozzáfűzi a legújabbat. A kézenfekvő forma minden iterációban újraépíti a puffert:

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

Ez egy friss foglalás – és N - 1 elem másolata – mintánként. A szeletértékadásos forma helyben tol:

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)

A buf[:-1] = buf[1:] az érdekes sor: két átfedő nézet ugyanabba a pufferbe, ahol a jobb oldali szelet az egyik végéről olvas, és a másikra ír. A numpy olyan sorrendben járja végig az alapul szolgáló memóriát, amely a helyben történő eltolást biztonságossá teszi. A cikluson belül soha nem foglalódik új tömb.

6.19.8. Vigyázz a logikai maszkokkal a streamelő ciklusokban

A logikai indexelés és a where() minden híváskor új tömböt hoz létre – az eredmény mérete az adatoktól függ, így egyetlen előre lefoglalt puffer sem tudja elnyelni a foglalást. Az ismételt maszképítés egy streamelő ciklusban eldobható tömbökkel tölti meg a RAM-ot. Egy időszakos gc.collect() visszanyeri a helyet:

import gc

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

Ugyanez a fenntartás vonatkozik az olyan összetett logikai kifejezésekre, mint az (a > lo) & (a < hi) – minden operátor egy új bool tömböt foglal. Ha egy maszkot újra felhasználsz, építsd fel egyszer, és tartsd meg:

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