6.19. Wydajność¶
Te same decyzje projektowe, które sprawiają, że numpy jest szybki na kamerze – wywołania biblioteczne operujące na całych tablicach, upakowane bufory typowane, widoki współdzielące dane ze swoim źródłem – ujawniają również zestaw nawyków, o których warto wiedzieć. Strona Kształt i kroki omówiła już regułę układu ostatniej osi; ta strona kataloguje nawyki dotyczące alokacji i dtype, które mają największe znaczenie w pętli strumieniowej.
6.19.1. Wybierz rozsądny dtype¶
Domyślnym dtype każdego konstruktora jest float. W przypadku danych, które są z natury 8-bitowe lub 16-bitowe – próbki ADC, piksele obrazu, odczyty sensora – przekaż jawnie dtype= z jednym z typów całkowitych:
adc = np.array(adc_samples, dtype=np.uint16)
Oszczędność RAM wynosi 2x dla uint16 i 4x dla uint8 w porównaniu z domyślnym 4-bajtowym float. Obliczenia działają również szybciej, ponieważ ścieżki kodu dla liczb całkowitych wewnątrz numpy są bardziej zwięzłe niż ogólne ścieżki dla liczb zmiennoprzecinkowych. Obowiązuje reguła przepełnienia liczb całkowitych omówiona na Dtypes – rzutuj na szerszy typ przed wykonaniem działań arytmetycznych, które mogą się przepełnić.
6.19.2. Preferuj ndarray zamiast iterowalnego obiektu¶
Większość redukcji i funkcji uniwersalnych akceptuje zarówno obiekt iterowalny, jak i ndarray
np.sum([1, 2, 3, 4, 5]) # works, but slow
np.sum(np.array([1, 2, 3, 4, 5])) # ~3x faster
Forma iterowalna zmusza numpy do przechodzenia przez wejście pojedynczo, obiekt Python po obiekcie, konwertując każdy z nich na liczbę, zanim będzie go można użyć. W przypadku ndarray konwersja jest już wykonana, a wywołanie przebiega bezpośrednio przez upakowany bufor.
Gdy te same dane są używane więcej niż raz, zbuduj ndarray jeden raz i przekazuj go dalej. Gdy dane istnieją tylko jako lista Pythona i są wykorzystywane jednokrotnie, koszt konwersji może przewyższyć przyspieszenie – sam konstruktor array() musi przejść przez listę i dokonać alokacji.
6.19.3. Preferuj widoki zamiast kopii¶
Wycinanie, indeksowanie pojedynczej osi tablicy o wyższym rzędzie, reshape(), transpose() oraz frombuffer() – wszystkie zwracają widoki, które współdzielą dane ze źródłem. Są one praktycznie darmowe.
copy(), flatten(), indeksowanie logiczne (a[mask]) oraz każde wyrażenie arytmetyczne alokują kopię. Sięgaj po nie tylko wtedy, gdy faktycznie potrzebny jest niezależny bufor.
W razie wątpliwości ndinfo() wypisuje lokalizację bazowego bufora; dwie tablice, które raportują ten sam adres, współdzielą swoje dane. Pełna tabela widok-kontra-kopia znajduje się na Widoki i kopie.
6.19.4. Alokuj raz, potem zapisuj¶
Największą pojedynczą pułapką wydajnościową na kamerze jest alokowanie świeżych tablic wewnątrz pętli wykonywanej wiele razy na sekundę. Każdy nowy ndarray prosi kamerę o RAM, a częste świeże alokacje marnują go.
Większość funkcji uniwersalnych akceptuje out=, dzięki czemu wynik można zapisać do już istniejącej tablicy:
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() akceptuje buffer= z tego samego powodu; spectrogram() oraz konwertery w stylu from_int32_buffer() akceptują zarówno out=, jak i scratchpad=. Zaalokuj wszystko raz i wykorzystuj ponownie.
6.19.5. Używaj operatorów działających w miejscu¶
b = b + 1 alokuje tymczasową tablicę wielkości b, kopiuje i przypisuje ponownie. b += 1 modyfikuje b bezpośrednio:
# makes a temporary
b = b + 1
# no temporary
b += 1
Ta sama idea dotyczy wyrażeń złożonych. a + b * c alokuje tablicę tymczasową dla b * c. Rozbicie wyrażenia na proste pod-przypisania zapisujące do wstępnie zaalokowanego bufora eliminuje tablice tymczasowe:
# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2
# zero temporaries
out[:] = a
out += b
out *= 2
6.19.6. Buduj wynik, nie dołączaj do niego¶
ndarray nie ma metody append – celowo. Powiększanie tablicy oznaczałoby alokowanie świeżego, większego bufora i kopiowanie do niego starej zawartości. Na mikrokontrolerze wstępnie zaalokuj końcowy rozmiar i wypełnij go:
out = np.zeros(N, dtype=np.float)
for i in range(N):
out[i] = some_calculation(i)
Gdy N faktycznie nie jest znane z góry, zapisuj do listy Pythona list i przekonwertuj raz na końcu za pomocą array().
6.19.7. Przypisanie do wycinka zamiast nowych tablic¶
Wiele wzorców typu „zbuduj nową tablicę z fragmentów innych” można wyrazić jako przypisania do wycinków wstępnie zaalokowanego bufora zamiast świeżej alokacji przy każdym wywołaniu.
Przesuwne okno nad strumieniem próbek – podstawa filtra średniej ruchomej – jest kanonicznym przykładem. Bufor przechowuje ostatnie N próbek; każda iteracja usuwa najstarszą i dołącza najnowszą. Oczywista forma przebudowuje bufor w każdej iteracji:
while True:
sample = read_sample()
buf = np.concatenate((buf[1:], # new buffer every loop
np.array([sample])))
avg = np.mean(buf)
To świeża alokacja – i kopia N - 1 elementów – na każdą próbkę. Forma z przypisaniem do wycinka przesuwa w miejscu:
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:] to interesujący wiersz: dwa nakładające się widoki na ten sam bufor, przy czym wycinek po prawej stronie jest czytany z jednego końca i zapisywany na drugim. numpy przechodzi przez bazową pamięć w kolejności, która zapewnia bezpieczeństwo przesunięcia w miejscu. Wewnątrz pętli nie jest nigdy alokowana żadna nowa tablica.
6.19.8. Uważaj na maski logiczne w pętlach strumieniowych¶
Indeksowanie logiczne oraz where() tworzą nową tablicę przy każdym wywołaniu – rozmiar wyniku zależy od danych, więc żaden wstępnie zaalokowany bufor nie może wchłonąć tej alokacji. Powtarzane budowanie masek w pętli strumieniowej zapełnia RAM tablicami jednorazowego użytku. Okresowe gc.collect() odzyskuje miejsce:
import gc
for i in range(1000):
mask = a < threshold
_ = a[mask]
if i % 100 == 0:
gc.collect()
To samo zastrzeżenie dotyczy złożonych wyrażeń logicznych takich jak (a > lo) & (a < hi) – każdy operator alokuje nową tablicę typu bool. Gdy maska jest używana ponownie, zbuduj ją raz i zachowaj:
mask = a < threshold
foo[mask] = 0
bar[mask] = 1