Performanța Python: bucle native NumPy, callback-uri și costuri de conversie
Un test numeric controlat, doar pe CPU, urmărește valori identice prin bucle CPython, aritmetică NumPy, un wrapper Python callback și o buclă Numba compilată.
Un test numeric controlat, doar pe CPU, urmărește valori identice prin bucle CPython, aritmetică NumPy, un wrapper Python callback și o buclă Numba compilată. NumPy cu intrare pregătită a avut un avans mare, în timp ce conversia din și înapoi la liste Python a redus acest avantaj la o egalitate aproape. Episodul explică limita de execuție și cum să măsurați rezultatul de care are nevoie o aplicație.
Citiți ediția scrisă (engleză) ↗
Ce acoperă acest videoclip
- CPython compilează codul sursă în bytecode. Lucrul repetat al interpretorului/obiectului și o buclă numerică nativă tipizată sunt aranjamente de execuție diferite.
- Aritmetica numerică NumPy poate preda loturi tipizate operațiilor compilate; bucla aritmetică încă există, iar vizualizările pot avea aranjamente necontigue.
- Măsurați conversia și reprezentarea necesară a ieșirii. În această rulare, conversia listelor Python în array-uri și înapoi a șters marele avans al array-ului pregătit.
- Sarcini de lucru mici pot favoriza Python-ul obișnuit deoarece costurile fixe de apelare a array-ului contează. Comprehensiunea cu opt valori a câștigat această comparație măsurată.
- np.vectorize este un wrapper de conveniență care apelează Python pentru elemente succesive; o interfață de lot singură nu stabilește execuția numerică nativă.
- Buclele compilate cu sintaxă Python pot fi, de asemenea, rapide. Includeți costurile de compilare/configurare la rece atunci când contează pentru apelant.
- Execuția nativă, CPU SIMD, multithreading și backend-urile GPU sunt idei separate. Caracteristicile CPU disponibile nu urmăresc instrucțiunile utilizate într-o anumită rulare.
Transcrierea tradusă
Tradus din narațiunea originală în engleză. Audio-ul și subtitrările disponibile sunt controlate de YouTube.
De ce poate același CPU termina mai devreme?
0:00 Crezi că Python are nevoie de un limbaj nou pentru a fi rapid. Același CPU a rulat lotul nostru mai repede după ce bucla a fost mutată. Unde a plecat munca? Când dispare creșterea de viteză? Ce scurtătură readuce Python-ul? Acesta este The Daily Diff, din interior. Nu uita de acea scurtătură. CPython este implementarea standard Python.
0:17 Compilează codul sursă în bytecode, apoi execută acele instrucțiuni într-un interpretator. NumPy este o bibliotecă numerică ale cărei array-uri conțin valori cu un tip de date declarat. Aritmetica sa încorporată poate preda un întreg lot operațiilor compilate. Ia un milion de valori în virgulă mobilă, ridică fiecare la pătrat, apoi adaugă valoarea originală. Fiecare versiune primește valori identice și produce răspunsuri corespondente. Valorile sunt suficient de mici pentru a menține diferențele de rotunjire în afara cursei.
0:40 Pe această mașină, înțelegerea a durat aproximativ treizeci și trei de milisecunde. NumPy a durat mai puțin de una. Ambele au rulat pe CPU. Aceasta este o dovadă utilă pentru această sarcină, și un necrolog teribil pentru un limbaj de programare. Începeți cu bucla obișnuită.
Ce lucru repetă o buclă Python?
0:53 Python obține următoarea valoare, efectuează aritmetica și adaugă răspunsul. Apoi face asta din nou. Înmulțirea este ieftină. Aranjarea următoarei înmulțiri și gestionarea obiectelor Python în jurul ei, adaugă muncă de fiecare dată. Aceasta este dezasamblarea reală a funcției testate. Iterația, înmulțirea și adunarea apar ca instrucțiuni bytecode. Versiunile și interpretoarele Python fac alegeri diferite.
1:14 Aceste timpi aparțin versiunii de pe ecran, care este Python 3.10. O listă Python stochează referințe la obiecte, astfel încât diferite tipuri de lucruri pot împărți o listă. Sarcina noastră aritmetică folosește numere peste tot. Plătește pentru un aranjament general în timp ce solicită un serviciu repetitiv. Imaginați-vă că comandați aceeași cafea de un milion de ori și completați un formular proaspăt pentru fiecare ceașcă. Mașina de cafea este în regulă.
1:36 Hârtia a dezvoltat o carieră proprie.
Unde mută NumPy bucla?
1:38 NumPy schimbă contractul. Acest array conține valori în virgulă mobilă de dimensiune fixă cu un singur tip declarat. Array-ul nostru este contiguu, astfel încât valorile sale stau împreună în memorie. Vizualizările pot avea alte aranjamente. Operația aritmetică știe ce fel de date primește. Apelul predă lotul unei operații native. O buclă vizitează încă valorile în codul compilat. Poate refolosi cunoștințele despre tip pe parcursul acelei lucrări.
2:00 Python rămâne persoana care predă sarcina. Itamar Turner Trauring explică distincția în Python Speed. Interfețele de lot, implementările native și instrucțiunile vectoriale ale procesorului sunt lucruri separate. Numele funcției oferă foarte puține dovezi.
Înseamnă rapid GPU sau multe nuclee CPU?
2:13 Acest rezultat a folosit NumPy pe CPU. Un backend GPU, cum ar fi CuPy, alege o casă diferită pentru array-uri și operații. Cumpărarea unui GPU lasă această buclă Python obișnuită să ruleze acolo unde rula deja. Instrucțiunile vectoriale ale CPU pot procesa mai multe valori împreună. NumPy are mecanisme pentru alegerea kernelurilor suportate. Timpul nostru de execuție raportează caracteristicile disponibile, dar nu urmărește instrucțiunile acestei expresii. Atribuirea întregului câștig măsurat instrucțiunilor vectoriale ar exagera dovezile.
2:40 Coșul de cumpărături GPU poate rămâne închis. Există ceva profund satisfăcător într-o remediere a performanței care anulează și o achiziție. Din păcate, benchmark-ul continuă.
Când dispare creșterea impresionantă de viteză?
2:49 Cu opt valori, comprehensiunea a câștigat. Apelarea mecanismului array a costat mai mult decât mica lucrare. Array-urile gata mai mari au preluat conducerea. Am folosit încercări repetate și un proces încălzit. Noile versiuni Python pot schimba punctul de intersecție. Apoi schimbați ceea ce are nevoie aplicația. Începeți cu o listă Python și terminați cu o listă Python nouă. NumPy trebuie să convertească intrarea într-un array și să transforme răspunsul înapoi în obiecte Python.
3:11 Acești pași apar pe factură dacă aplicația dumneavoastră are nevoie de ei. La un milion de valori, acest traseu complet a durat aproximativ douăzeci și opt de milisecunde. Comprehensiunea a durat și ea aproximativ douăzeci și opt. Avansul nostru dramatic a devenit o egalitate aproape. Partea rapidă a fost reală. Avantajul a depins de a lăsa datele să rămână în forma lor numerică.
Ce limite aparțin benchmark-ului?
3:28 Urmărește datele. Dacă următoarea operație acceptă un array, păstrați array-ul. Dacă convertiți în mod repetat între liste și array-uri, măsurați acele traversări. Oprirea înainte de o predare costisitoare oferă numere precise pentru o întrebare greșită. Înmulțirea acestui array creează un rezultat temporar. Adunarea valorilor originale creează ieșirea. Execuția nativă necesită încă memorie și mută date.
3:47 Unele operații acceptă un buffer de ieșire, reducând alocările atunci când codul dumneavoastră poate să îl reutilizeze în siguranță. O singură linie frumoasă poate comanda mai multe buffer-uri mari. Python Speed semnalează și această problemă. Alocatorul este remarcabil de indiferent la estetica dumneavoastră.
Poate fi și o buclă Python compilată rapidă?
4:00 De asemenea, puteți păstra sintaxa buclei și modifica execuția acesteia. Numba poate compila funcții Python numerice suportate în cod nativ. Iată bucla din testul nostru cu decoratorul său de compilare. Încă spune „for”. Calea costisitoare a interpretorului s-a schimbat. După compilare, această buclă a durat mai puțin de jumătate de milisecundă cu array-uri pregătite. Compilarea în sine a durat aproximativ o secundă în această rulare. Acest cost de configurare a fost exclus din timpii repetați,
4:22 deci o mică sarcină unică merită o comparație diferită. Ghidul Numba însuși este bucuros să arate bucle explicite. Începerea unei migrări de limbaj înainte de a găsi calea fierbinte este o modalitate elaborată de a evita deschiderea unui profiler. Mai întâi găsiți ce consumă timpul. Așteptările rețelei, analiza obiectelor și aritmetica în masă necesită remedieri diferite. Operațiile native pe array ajută atunci când calculul și datele se potrivesc.
4:43 Măsurați rezultatul de care apelantul dumneavoastră are nevoie de fapt. Acum, despre acea scurtătură. Aveți o funcție Python și doriți ca aceasta să accepte un array. Un wrapper numit vectorize sună ca butonul de performanță lipsă. Documentația sa are o propoziție importantă în așteptare.
Ce execută de fapt np.vectorize?
4:56 NumPy spune că wrapper-ul său vectorize este oferit în primul rând pentru comoditate. Implementarea sa este, în esență, o buclă for. Wrapper-ul acceptă loturi, dar continuă să apeleze funcția Python pentru elemente succesive. Interfața s-a scurtat. Limita de execuție a rămas ocupată. Urmăriți aceleași valori revenind prin callback-ul Python. Cu array-uri pregătite, această versiune a durat aproximativ optzeci și șapte de milisecunde. A fost mai lentă decât comprehensiunea noastră.
5:18 Un wrapper util de broadcasting poate păstra exact lucrul per element pe care încercați să îl eliminați. Verificați implementarea din spatele unei interfețe de lot, păstrați datele compatibile în forma lor utilă și includeți conversia în comparație. Buclele native, de callback și compilate pot arăta similar din perspectiva apelantului. Numele vectorize a făcut o promisiune pe care cronometrul dumneavoastră nu a semnat-o niciodată. Păstrați-l atunci când comoditatea este ceea ce aveți nevoie. Pentru performanță, verificați bucla care rulează.
Ce ar trebui să schimbați în codul dumneavoastră?
5:41 Verdictul meu este SHIP IT pentru procesarea în loturi a unei căi fierbinți numerice măsurate. Păstrați buclele obișnuite atunci când funcționează deja bine. Verificați unde se întâmplă lucrul și ce are nevoie următorul apelant. Abonați-vă, apăsați clopoțelul și spuneți-mi în comentarii dacă ați fi ștampilat-o diferit. Și asta este diferența pentru astăzi. Sunt Niko de la Axrisi. Fuzionați responsabil.
Surse
- Universal functions (ufunc) basics — NumPy 2.2NumPy
- The N-dimensional array — NumPy 2.2NumPy
- numpy.vectorize — convenience and performance notesNumPy
- CPU/SIMD optimizations — NumPy 2.2NumPy
- dis — Disassembler for Python bytecodePython Software Foundation
- CPython 3.10.12 list representationCPython contributors
- The limits of Python vectorization as a performance techniqueItamar Turner-Trauring / Python⇒Speed
- Performance TipsNumba
- Basics of CuPyCuPy



