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