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