6.19. Performanță¶
Aceleași decizii de proiectare care fac numpy rapid pe cameră – apeluri de bibliotecă pe întregul tablou, tampoane tipizate compacte, vederi care partajează datele cu sursa lor – expun de asemenea un set de obiceiuri pe care merită să le cunoști. Pagina Formă și pași (strides) a acoperit deja regula de dispunere a ultimei axe; această pagină catalogează obiceiurile de alocare și de dtype care contează cel mai mult într-o buclă de tip streaming.
6.19.1. Alege un dtype rezonabil¶
Dtype-ul implicit al fiecărui constructor este float. Pentru date care sunt în mod natural pe 8 biți sau 16 biți – eșantioane ADC, pixeli de imagine, citiri de la senzori – transmite dtype= în mod explicit către unul dintre tipurile întregi:
adc = np.array(adc_samples, dtype=np.uint16)
Economia de RAM este de 2x pentru uint16 și de 4x pentru uint8 față de valoarea implicită float de 4 octeți. Calculele rulează de asemenea mai rapid, deoarece căile de cod întregi din numpy sunt mai compacte decât cele generice cu virgulă mobilă. Se aplică regula privind depășirea pentru întregi tratată în Dtype-uri – convertește la un tip mai larg înainte de operații aritmetice care ar putea depăși.
6.19.2. Preferă un ndarray în locul unui iterabil¶
Majoritatea reducerilor și a funcțiilor universale acceptă fie un iterabil, fie un ndarray
np.sum([1, 2, 3, 4, 5]) # works, but slow
np.sum(np.array([1, 2, 3, 4, 5])) # ~3x faster
Forma iterabilă forțează numpy să parcurgă intrarea câte un obiect Python pe rând, convertind fiecare la un număr înainte de a-l putea utiliza. În cazul unui ndarray conversia este deja făcută, iar apelul rulează direct prin tamponul compact.
Atunci când aceleași date sunt utilizate de mai multe ori, construiește ndarray o singură dată și transmite-l mai departe. Atunci când datele există doar ca listă Python și sunt consumate o singură dată, costul conversiei poate depăși câștigul de viteză – constructorul array() însuși trebuie să parcurgă lista și să aloce memorie.
6.19.3. Preferă vederile în locul copiilor¶
Segmentarea (slicing), indexarea pe o singură axă a unui tablou de rang superior, reshape(), transpose() și frombuffer() returnează toate vederi care partajează datele cu sursa. Acestea sunt în esență gratuite.
copy(), flatten(), indexarea booleană (a[mask]) și orice expresie aritmetică alocă o copie. Recurge la ele doar atunci când este nevoie cu adevărat de un tampon independent.
În caz de îndoială, ndinfo() afișează locația tamponului subiacent; două tablouri care raportează aceeași adresă își partajează datele. Tabelul complet vedere vs. copie se află în Vederi și copii.
6.19.4. Alocă o dată, apoi scrie¶
Cea mai mare capcană de performanță pe cameră este alocarea de tablouri noi în interiorul unei bucle care rulează de multe ori pe secundă. Fiecare nou ndarray solicită RAM de la cameră, iar alocările frecvente de tablouri noi o irosesc.
Majoritatea funcțiilor universale acceptă out= astfel încât rezultatul să poată fi scris într-un tablou care există deja:
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() acceptă buffer= din același motiv; spectrogram() și convertoarele de tip from_int32_buffer() acceptă atât out=, cât și scratchpad=. Alocă totul o singură dată și reutilizează.
6.19.5. Folosește operatori in-place¶
b = b + 1 alocă un temporar de dimensiunea lui b, copiază și reatribuie. b += 1 modifică b direct:
# makes a temporary
b = b + 1
# no temporary
b += 1
Aceeași idee se aplică expresiilor compuse. a + b * c alocă un temporar pentru b * c. Împărțirea expresiei în subatribuiri simple care scriu într-un tampon prealocat elimină temporarele:
# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2
# zero temporaries
out[:] = a
out += b
out *= 2
6.19.6. Construiește rezultatul, nu adăuga la el¶
ndarray nu are append – în mod intenționat. Creșterea unui tablou ar însemna alocarea unui tampon nou, mai mare, și copierea conținutului vechi în el. Pe un microcontroler, prealocă dimensiunea finală și umple-o
out = np.zeros(N, dtype=np.float)
for i in range(N):
out[i] = some_calculation(i)
Atunci când N chiar nu este cunoscut dinainte, scrie într-un list Python și convertește o singură dată la final cu array().
6.19.7. Atribuire de segment în locul tablourilor noi¶
Multe tipare de tipul „construiește un tablou nou din bucăți ale altora” pot fi exprimate ca atribuiri de segment într-un tampon prealocat în loc de o alocare nouă la fiecare apel.
O fereastră glisantă peste un flux de eșantioane – fundamentul unui filtru cu medie mobilă – este cazul canonic. Tamponul reține ultimele N eșantioane; fiecare iterație elimină cel mai vechi și adaugă cel mai nou. Forma evidentă reconstruiește tamponul la fiecare iterație:
while True:
sample = read_sample()
buf = np.concatenate((buf[1:], # new buffer every loop
np.array([sample])))
avg = np.mean(buf)
Aceasta este o alocare nouă – și o copie a N - 1 elemente – per eșantion. Forma cu atribuire de segment deplasează in-place:
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:] este linia interesantă: două vederi suprapuse asupra aceluiași tampon, segmentul din dreapta citit dintr-un capăt și scris la celălalt. numpy parcurge memoria subiacentă în ordinea care face ca deplasarea in-place să fie sigură. Niciun tablou nou nu este vreodată alocat în interiorul buclei.
6.19.8. Atenție la măștile booleene în buclele de tip streaming¶
Indexarea booleană și where() produc un tablou nou la fiecare apel – dimensiunea rezultatului depinde de date, așa că niciun tampon prealocat nu poate absorbi alocarea. Construirea repetată de măști într-o buclă de tip streaming umple RAM-ul cu tablouri de unică folosință. Un gc.collect() periodic recuperează spațiul:
import gc
for i in range(1000):
mask = a < threshold
_ = a[mask]
if i % 100 == 0:
gc.collect()
Aceeași precauție se aplică expresiilor booleene compuse precum (a > lo) & (a < hi) – fiecare operator alocă un nou tablou de tip bool. Atunci când o mască este reutilizată, construiește-o o singură dată și păstreaz-o:
mask = a < threshold
foo[mask] = 0
bar[mask] = 1