Asigurați-vă că programele dvs. de du-te rapid cu profilare

Go este adesea folosit pentru scrierea sistemelor distribuite, a stocurilor avansate de date și a microservicii. Performanța este esențială în aceste domenii. 

În acest tutorial, veți învăța cum să vă programați programele pentru a le face rapid (folosiți mai bine CPU-ul) sau lumină de pene (utilizați mai puțină memorie). Voi acoperi CPU-ul și profilul de memorie, folosind pprof (profilele go), vizualizând profilurile și chiar și graficele de flacără.

Profilarea măsoară performanța programului dvs. în diferite dimensiuni. Du-te vine cu un mare sprijin pentru profilare și poate profil următoarele dimensiuni din cutie:

  • o eșantionare a timpului CPU pe funcție și instrucțiune
  • o eșantionare a tuturor alocărilor de heap
  • stive de urme de toate actualele gorutine
  • stiluri de stiva care au condus la crearea de noi fire de OS
  • stive care au dus la blocarea primitivelor de sincronizare
  • stiluri de stive ale deținătorilor mutexelor susținute

Puteți crea chiar și profile personalizate dacă doriți. Deplasarea profilării implică crearea unui fișier de profil și apoi analizarea lui folosind pprof du-te.

Cum se creează fișiere de profil

Există mai multe moduri de a crea un fișier de profil.

Folosind "go test" pentru generarea fișierelor de profil

Cea mai ușoară cale este de a utiliza du-te test. Are mai multe steaguri care vă permit să creați fișiere de profil. Iată cum puteți genera un fișier de profil CPU și un fișier de profil de memorie pentru test în directorul curent: mergeți-test -cpuprofile cpu.prof -memprofile mem.prof -bench .

Descărcați date de profil live dintr-un serviciu de lungă durată

Dacă doriți să faceți profil pentru un serviciu Web de lungă durată, puteți utiliza interfața HTTP încorporată pentru a furniza date de profil. Adăugați undeva următoarea declarație de import:

importul "net / http / pprof"

Acum, puteți descărca date de profil live din / Depanare / pprof / URL-ul. Mai multe informații sunt disponibile în documentația pachetului net / http / pprof.

Profilarea în cod

De asemenea, puteți adăuga profiluri directe în codul dvs. pentru control complet. Mai întâi trebuie să importați rulare / pprof. Procesarea profilului CPU este controlată de două apeluri:

  • pprof.StartCPUProfile ()
  • pprof.StopCPUProfile ()

Profilul de memorie se face prin apelare runtime.GC () urmat de pprof.WriteHeapProfile ().

Toate funcțiile de profilare acceptă un mâner de fișiere pe care sunteți responsabil pentru deschiderea și închiderea corespunzătoare.

Programul de probă

Pentru a vedea profiler în acțiune, voi folosi un program care rezolvă problema proiectului 8. Problema este: dat fiind un număr de 1000 de cifre, găsiți cele 13 cifre adiacente din acest număr care au cel mai mare produs. 

Aici este o soluție trivială care repetă peste toate secvențele de 13 cifre și pentru fiecare astfel de secvență se multiplică toate cele 13 cifre și returnează rezultatul. Cel mai mare rezultat este stocat și în cele din urmă returnat:

pachet import trivial ("string") func calcProduct (string string) int64 digits: = make ([] int64, len (serie)) pentru i, c:  produs: = int64 (1) pentru i: = 0; eu < len(digits); i++  product *= digits[i]  return product  func FindLargestProduct(text string) int64  text = strings.Replace(text, "\n", "", -1) largestProduct := int64(0) for i := 0; i < len(text); i++  end := i + 13 if end > textul [i: end] rezultat: = calcProduct (serie) dacă rezultatul> largestProduct largestProduct = rezultat returnă cel mai mareProduct 

Mai târziu, după profilare, vom vedea câteva modalități de îmbunătățire a performanței cu o altă soluție.

Profilarea procesorului

Să ne prezentăm CPU-ul programului nostru. Voi folosi metoda de test go folosind acest test:

import ( "testare") Text const = '73167176531330624919225119674426574742355349194934 96983520312774506326239578318016984801869478851843 85861560789112949495459501737958331952853208805511 12540698747158523863050715693290963295227443043557 66896648950445244523161731856403098711121722383113 62229893423380308135336276614282806444486645238749 30358907296290491560440772390713810515859307960866 70172427121883998797908792274921901699720888093776 65727333001053367881220235421809751254540594752243 52584907711670556013604839586446706324415722155397 53697817977846174064955149290862569321978468622482 83972241375657056057490261407972968652414535100474 82166370484403199890008895243450658541227588666881 16427171479924442928230863465674813919123162824586 17866458359124566529476545682848912883142607690042 24219022671055626321111109370544217506941658960408 07198403850962455444362981230987879927244284909188 84580156166097919133875499200524063689912560717606 0588611646710940507754100225698315520005593572 9725 71636269561882670428252483600823257530420752963450 'func TestFindLargestProduct (t * test.T) pentru i: = 0; eu < 100000; i++  res := FindLargestProduct(text) expected := int64(23514624000) if res != expected  t.Errorf("Wrong!")    

Rețineți că rulez testul de 100.000 de ori pentru că profilul de navigare este un profil de eșantionare care are nevoie de codul pentru a petrece efectiv un timp semnificativ (cumulativ de câteva milisecunde) pe fiecare linie de cod. Iată comanda pentru pregătirea profilului:

du-te test -cpuprofile cpu.prof -bench. ok _ / github.com / the-gigi / project-euler / 8 / go / trivial 13.243s 

A fost nevoie de puțin peste 13 secunde (pentru 100 000 de iterații). Acum, pentru a vizualiza profilul, utilizați instrumentul pprof go pentru a intra în interfața interactivă. Există multe comenzi și opțiuni. Comanda cea mai de bază este topN; cu opțiunea -cum afișează funcțiile de top N care au avut timpul cel mai cumulativ de executat (deci o funcție care durează foarte puțin timp pentru a executa, dar este numită de mai multe ori, poate fi în partea de sus). De obicei, de aici încep.

> go tool pprof cpu.prof Tip: cpu Timp: Oct 23, 2017 la 8:05 am (PDT) Durata: 13.22s, Total eșantioane = 13.10s (99.06%) Intrarea în modul interactiv ) top5 -cum Arată noduri reprezentând 1,23s, 9,39% din 13,10s total Dropped 76 nodes (cum <= 0.07s) Showing top 5 nodes out of 53 flat flat% sum% cum cum% 0.07s 0.53% 0.53% 10.64s 81.22% FindLargestProduct 0 0% 0.53% 10.64s 81.22% TestFindLargestProduct 0 0% 0.53% 10.64s 81.22% testing.tRunner 1.07s 8.17% 8.70% 10.54s 80.46% trivial.calcProduct 0.09s 0.69% 9.39% 9.47s 72.29% runtime.makeslice 

Să înțelegem rezultatul. Fiecare rând reprezintă o funcție. Am ascuns calea către fiecare funcție din cauza constrângerilor de spațiu, dar va apărea în ieșirea reală ca ultima coloană. 

Plătit înseamnă timpul (sau procentul) petrecut în cadrul funcției și Cum înseamnă cumulativ - timpul petrecut în cadrul funcției și toate funcțiile pe care le numește. În acest caz, testing.tRunner sună efectiv TestFindLargestProduct (), care solicită FindLargestProduct (), dar din moment ce practic nu este cheltuit acolo, profilul de eșantionare numără timpul lor plat ca 0.

Profilul memoriei

Profilul de memorie este similar, cu excepția faptului că creați un profil de memorie:

du-te test -memprofile mem.prof -bench. PASS ok _ / github. Com / the-gigi / project-euler / 8 / go / trivial

Puteți analiza utilizarea memoriei utilizând același instrument.

Folosind pprof pentru a optimiza viteza programului

Să vedem ce putem face pentru a rezolva problema mai repede. Privind la profil, vedem asta calcProduct () ia 8.17% din timpul de execuție plat, dar makeSlice (), care este chemat de la calcProduct (), este de 72% (cumulativ, deoarece numeste alte functii). Acest lucru oferă o indicație destul de bună a ceea ce trebuie să optimizăm. Ce face codul? Pentru fiecare secvență de 13 numere adiacente, aceasta alocă o felie:

func calcProduct (șir de serii) int64 digits: = make ([] int64, len (serie)) ... 

Asta e de aproape 1.000 de ori pe fugă și avem 100.000 de ori. Alocările de memorie sunt lente. În acest caz, nu este nevoie să alocați o felie nouă de fiecare dată. De fapt, nu este nevoie să alocați deloc o felie. Putem doar să scanăm matricea de intrare. 

Următorul fragment de cod arată cum se calculează produsul rulat prin simpla împărțire cu prima cifră a secvenței anterioare și înmulțirea cu javră cifră. 

daca currProduct = = 1 currProduct / = vechi continua daca veche == 1 currProduct * = cur altceva currProduct = currProduct / 

Iată o scurtă listă a unor optimizări algoritmice:

  • Calculul unui produs care rulează. Să presupunem că am calculat produsul la indexul N ... N + 13 și îl numim P (N). Acum trebuie să calculăm produsul la indexul N + 1 ... N + 13. P (N + 1) este egal cu P (N), cu excepția faptului că primul număr din indexul N a dispărut și trebuie să luăm în considerare numărul nou la indexul N + 14T. Acest lucru se poate face prin împărțirea produsului anterior cu primul său număr și înmulțirea cu noul număr. 
  • Nu se calculează nici o succesiune de 13 numere care conțin 0 (produsul va fi întotdeauna zero).
  • Evitarea împărțirii sau multiplicării cu 1.

Programul complet este aici. Există o anumită logică spinoasă pentru a lucra în jurul zerourilor, dar în afară de asta este destul de simplă. Principalul lucru este că noi atribuim doar o singură matrice de 1000 de octeți la început și îl transmitem prin pointer (deci nu copie) la  findLargestProductInSeries () funcționează cu o serie de indici.

pachet scan func findLargestProductInSeries (cifre * [1000] byte, start, end int) int64 if (end - start) < 13  return -1  largestProduct := int64((*digits)[start]) for i := 1; i < 13 ; i++  d := int64((*digits)[start + i]) if d == 1  continue  largestProduct *= d  currProduct := largestProduct for ii := start + 13; ii < end; ii++  old := int64((*digits)[ii-13]) cur := int64((*digits)[ii]) if old == cur  continue  if cur == 1  currProduct /= old continue  if old == 1  currProduct *= cur  else  currProduct = currProduct / old * cur  if currProduct > cel mai mareProduct cel mai mareProduct return funcția FindLargestProduct (text string) int64 var digiți [1000] byte digIndex: = 0 pentru _, c: = intervalul if c == 10 continue digits [digIndex] = octet (c) - 48 digIndex ++ start: = -1 sfârșit: = -1 findStart: = true var cel mai mareProduct int64 pentru ii: = 0; ii < len(digits) - 13; ii++  if findStart  if digits[ii] == 0  continue  else  start = ii findStart = false   if digits[ii] == 0  end = ii result := findLargestProductInSeries(&digits, start, end) if result > largestProduct biggestProduct = rezultat findStart = true cel mai mare produs returnat

Testul este același. Să vedem cum am făcut-o cu profilul:

> merge test -cpuprofile cpu.prof -bench. PASS ok _ / github.com / the-gigi / project-euler / 8 / go / scan 0.816s 

Chiar de pe lilieci, vedem că timpul de rulare a scăzut de la mai mult de 13 secunde la mai puțin de o secundă. Asta e destul de bine. E timpul să privim înăuntru. Să folosim doar top 10, care sortează după un timp plat.

(pprof) top10 Afișare noduri reprezentând 560ms, 100% din totalul de 560ms plat plat% sum cum cum% 290ms 51,79% 51,79% 290ms 51,79% findLargestProductInSeries 250ms 44,64% 96,43% 540ms 96,43% FindLargestProduct 20ms 3,57% 100% 20ms 3,57% .saumon 0 0% 100% 20ms 3.57% runtime.mstart 0 0% 100ms 20ms 3.57% runtime.mstart1 0 0% 100% 20ms 3.57% runtime.sysmon 0 0% 100% 540ms 96,43% test.Runner 

Asta e super. Destul de mult întreaga durată de execuție se cheltuiește în codul nostru. Nu există nicio alocare de memorie. Ne putem scufunda mai adânc și ne uităm la nivelul declarației cu comanda de listă:

(pprof) listă FindLargestProduct Total: 560ms ROUTINE ======================== scan.FindLargestProduct 250ms 540ms (plat, cum) 96.43% din Total ... 44: ... 45: ... 46: func FindLargestProduct (t string) int64 ... 47: var cifre [1000] octet ... 48: digIndex: = 0 70ms 70ms 49: pentru _, c: 10 ... 51: continuă ... 52: ... 53: digiți [digIndex] = octet (c) - 48 10ms 10ms 54: digIndex ++ ... 55: -1 ... 59: findStart: = true ... 60: var cel mai mareProduct int64 ... 61: pentru ii: = 0; ii < len(digits)-13; ii++  10ms 10ms 62: if findStart … 63: if digits[ii] == 0 … 64: continue… 65:  else … 66: start = ii… 67: findStart = false… 68: … 69: … 70: 70ms 70ms 71: if digits[ii] == 0 … 72: end = ii 20ms 310ms 73: result := f(&digits,start,end) 70ms 70ms 74: if result > cea mai mareProduct ... 75: largestProduct = rezultat ... 76: ... 77: findStart = true ... 78: ... 79:

Acest lucru este destul de uimitor. Veți primi o declarație prin sincronizarea declarațiilor tuturor punctelor importante. Rețineți că apelul de la linia 73 la funcția f () este de fapt un apel către findLargestProductInSeries (), pe care l-am redenumit în profil datorită limitărilor spațiului. Acest apel durează 20 ms. Poate, prin încorporarea codului funcției în loc, putem salva apelul pentru funcții (inclusiv alocarea argumentelor stack și copiere) și salvați cele 20 ms. S-ar putea să existe alte optimizări valoroase pe care această viziune le poate ajuta să identifice.

Vizualizare

Privirea acestor profiluri de text poate fi dificilă pentru programele mari. Go vă oferă o mulțime de opțiuni de vizualizare. Va trebui să instalați Graphviz pentru următoarea secțiune.

Instrumentul pprof poate genera rezultate în mai multe formate. Una dintre cele mai ușoare căi (ieșire svg) este pur și simplu să tastați 'web' din promptul pprof interactiv și browserul dvs. va afișa un grafic frumos cu calea fierbinte marcată în roz.

Flame Graphs

Graficele încorporate sunt plăcute și utile, dar cu programe mari, chiar și aceste grafice pot fi dificil de explorat. Unul dintre cele mai populare instrumente pentru vizualizarea rezultatelor de performanță este graficul de flacără. Instrumentul pprof nu-l suporta încă din cutie, dar puteți juca cu grafice de flacără folosind deja instrumentul Uber's torch. Există activități în curs de desfășurare pentru a adăuga suportul încorporat pentru graficele de flacără la pprof.

Concluzie

Go este un limbaj de programare a sistemelor care este utilizat pentru a construi sisteme de distribuție de înaltă performanță și magazine de date. Go vine cu un suport excelent, care continuă să devină mai bun pentru profilarea programelor, analizarea performanțelor acestora și vizualizarea rezultatelor. 

Există o mare atenție a echipei Go și a comunității în ceea ce privește îmbunătățirea instrumentelor în jurul performanței. Codul sursă complet cu trei algoritmi diferiți poate fi găsit pe GitHub.

Cod