6.19. Performanse

Iste projektne odluke koje čine numpy brzim na kameri – pozivi biblioteke nad cijelim poljem, zapakirani tipizirani međuspremnici, pogledi koji dijele podatke sa svojim izvorom – ujedno otkrivaju i niz navika o kojima je vrijedno znati. Stranica Oblik i koraci već je pokrila pravilo rasporeda posljednje osi; ova stranica katalogizira navike vezane uz alokaciju i dtype koje su najvažnije u petlji za obradu toka podataka.

6.19.1. Odaberite razuman dtype

Zadani dtype svakog konstruktora je float. Za podatke koji su prirodno 8-bitni ili 16-bitni – ADC uzorci, pikseli slike, očitanja senzora – proslijedite dtype= eksplicitno jednom od cjelobrojnih tipova:

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

Ušteda RAM-a iznosi 2x za uint16 i 4x za uint8 u odnosu na 4-bajtni zadani float. Matematika se također izvodi brže jer su cjelobrojne staze koda unutar numpy čvršće od generičkih float staza. Vrijedi pravilo o prelijevanju cjelobrojnih vrijednosti opisano na Dtypes – pretvorite u širi tip prije aritmetike koja bi se mogla preliti.

6.19.2. Preferirajte ndarray pred iterabilnim objektom

Većina redukcija i univerzalnih funkcija prihvaća ili iterabilni objekt ili ndarray

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

Iterabilni oblik prisiljava numpy da koraka kroz ulaz jedan po jedan Python objekt, pretvarajući svaki u broj prije nego što ga može upotrijebiti. Kod ndarray pretvorba je već obavljena i poziv prolazi ravno kroz zapakirani međuspremnik.

Kada se isti podaci koriste više puta, izgradite ndarray jednom i prosljeđujte ga. Kada podaci postoje samo kao Python lista i koriste se jednom, trošak pretvorbe može nadmašiti ubrzanje – sam konstruktor array() mora proći kroz listu i alocirati.

6.19.3. Preferirajte poglede pred kopijama

Rezanje, indeksiranje po jednoj osi polja višeg ranga, reshape(), transpose() i frombuffer() svi vraćaju poglede koji dijele podatke s izvorom. Oni su u biti besplatni.

copy(), flatten(), booleovo indeksiranje (a[mask]) i bilo koji aritmetički izraz alociraju kopiju. Posegnite za njima samo kada je doista potreban neovisan međuspremnik.

Kada niste sigurni, ndinfo() ispisuje lokaciju temeljnog međuspremnika; dva polja koja prijavljuju istu adresu dijele svoje podatke. Potpuna tablica pogled-naspram-kopije nalazi se na Pogledi i kopije.

6.19.4. Alocirajte jednom, zatim pišite

Najveća pojedinačna zamka za performanse na kameri je alociranje svježih polja unutar petlje koja se izvodi mnogo puta u sekundi. Svaki novi ndarray traži od kamere RAM, a česte svježe alokacije ga troše.

Većina univerzalnih funkcija prihvaća out= tako da se rezultat može upisati u polje koje već postoji:

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() prihvaća buffer= iz istog razloga; spectrogram() i pretvarači u stilu from_int32_buffer() prihvaćaju i out= i scratchpad=. Alocirajte sve jednom i ponovno upotrijebite.

6.19.5. Koristite operatore na licu mjesta

b = b + 1 alocira privremeni objekt veličine b, kopira i ponovno dodjeljuje. b += 1 mijenja b izravno:

# makes a temporary
b = b + 1

# no temporary
b += 1

Ista ideja vrijedi i za složene izraze. a + b * c alocira privremeni objekt za b * c. Razbijanje izraza na jednostavne pod-dodjele koje pišu u unaprijed alocirani međuspremnik uklanja privremene objekte:

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

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

6.19.6. Izgradite rezultat, nemojte mu dodavati

ndarray nema append – namjerno. Rast polja značio bi alociranje svježeg, većeg međuspremnika i kopiranje starog sadržaja u njega. Na mikrokontroleru unaprijed alocirajte konačnu veličinu i ispunite je:

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

Kada N doista nije unaprijed poznat, pišite u Python list i pretvorite jednom na kraju pomoću array().

6.19.7. Dodjela rezanjem umjesto novih polja

Mnogi obrasci tipa „izgradi novo polje iz dijelova drugih” mogu se izraziti kao dodjele rezanjem u unaprijed alocirani međuspremnik umjesto svježe alokacije pri svakom pozivu.

Klizni prozor preko toka uzoraka – temelj filtra pomičnog prosjeka – kanonski je slučaj. Međuspremnik drži posljednjih N uzoraka; svaka iteracija ispušta najstariji i dodaje najnoviji. Očiti oblik iznova izgrađuje međuspremnik pri svakoj iteraciji:

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

To je svježa alokacija – i kopija N - 1 elemenata – po uzorku. Oblik s dodjelom rezanjem pomiče na licu mjesta:

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 zanimljiva linija: dva preklapajuća pogleda u isti međuspremnik, desni rez čitan s jednog kraja i pisan na drugi. numpy prolazi temeljnu memoriju redoslijedom koji čini pomak na licu mjesta sigurnim. Nijedno novo polje nikada se ne alocira unutar petlje.

6.19.8. Pazite na booleove maske u petljama za obradu toka

Booleovo indeksiranje i where() proizvode novo polje pri svakom pozivu – veličina rezultata ovisi o podacima, pa nijedan unaprijed alocirani međuspremnik ne može apsorbirati alokaciju. Ponavljano izgrađivanje maski u petlji za obradu toka puni RAM jednokratnim poljima. Periodični gc.collect() vraća prostor:

import gc

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

Isto upozorenje vrijedi za složene booleove izraze poput (a > lo) & (a < hi) – svaki operator alocira novo bool polje. Kada se maska ponovno koristi, izgradite je jednom i zadržite:

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