Gjør dine Go-programmer lynrask med profiling

Go brukes ofte til å skrive distribuerte systemer, avanserte datalager og microservices. Ytelse er nøkkelen i disse domenene. 

I denne veiledningen lærer du hvordan du profilerer programmene dine for å gjøre dem lynrask (bruk CPUen bedre) eller fjærlampe (bruk mindre minne). Jeg vil dekke CPU og minneprofilering ved hjelp av pprof (Go-profiler), visualisere profilene, og til og med flammediagrammer.

Profilering måler ytelsen til programmet i ulike dimensjoner. Go kommer med stor støtte til profilering og kan profilere følgende dimensjoner ut av esken:

  • et utvalg av CPU-tid per funksjon OG instruksjon
  • et utvalg av alle heapallokeringer
  • stable spor av alle gjeldende goroutiner
  • stable spor som førte til etableringen av nye OS-tråder
  • stable spor som førte til blokkering av synkroniserings primitiver
  • stable spor av holdere av anstrengte mutexes

Du kan til og med lage egne profiler hvis du vil. Go profilering innebærer å opprette en profilfil og deretter analysere den ved hjelp av pprof gå verktøyet.

Slik oppretter du profilfiler

Det er flere måter å opprette en profilfil på.

Bruk "gå test" for å generere profilfiler

Den enkleste måten er å bruke gå test. Den har flere flagg som lar deg lage profilfiler. Slik genererer du både en CPU-profilfil og en minneprofilfil for testen i gjeldende katalog: gå test -cpuprofile cpu.prof -memprofile mem.prof-bench .

Last ned live profildata fra en langvarig tjeneste

Hvis du vil profilere en langvarig webtjeneste, kan du bruke det innebygde HTTP-grensesnittet for å gi profildata. Legg til et sted følgende importerklæring:

import _ "netto / http / pprof"

Nå kan du laste ned live profildata fra / Debug / pprof / URL. Mer informasjon er tilgjengelig i dokumentasjonen til nettverket / http / pprof-pakken.

Profilering i kode

Du kan også legge til direkte profilering i koden din for fullstendig kontroll. Først må du importere runtime / pprof. CPU profilering styres av to samtaler:

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

Minneprofilering gjøres ved å ringe runtime.GC () etterfulgt av pprof.WriteHeapProfile ().

Alle profileringsfunksjonene godtar et filhåndtak som du er ansvarlig for å åpne og lukke på riktig måte.

Eksempelprogrammet

For å se profiler i aksjon, vil jeg bruke et program som løser Project Eulers problem 8. Problemet er: gitt et 1000-sifret tall, finn de 13 tilstøtende sifrene i dette nummeret som har det største produktet. 

Her er en triviell løsning som itererer over alle sekvensene med 13 siffer, og for hver slik sekvens multipliserer alle 13 siffer og returnerer resultatet. Det største resultatet er lagret og til slutt returnert:

pakke trivial import ("strenger") func calcProdukt (serie streng) int64 siffer: = lage ([] int64, len (serie)) for i, c: = rekkevidde serier siffer [i] = int64 (c) - 48  produkt: = int64 (1) for i: = 0; Jeg < 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 > len (tekst) end = len (tekst) serie: = tekst [i: ende] resultat: = calcProdukt (serie) hvis resultat> størsteProdukt størsteProdukt =resultat returnere størsteProdukt 

Senere, etter profilering, vil vi se noen måter å forbedre ytelsen med en annen løsning.

CPU-profilering

La oss profilere CPUen til programmet vårt. Jeg bruker go-testmetoden ved hjelp av denne testen:

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

Vær oppmerksom på at jeg kjører testen 100 000 ganger fordi go profiler er en prøveprofil som trenger at koden faktisk bruker litt betydelig tid (flere millisekunder kumulativ) på hver linje med kode. Her er kommandoen for å forberede profilen:

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

Det tok litt over 13 sekunder (for 100.000 iterasjoner). Nå, for å se profilen, bruk pprof go-verktøyet for å komme inn i den interaktive spørringen. Det er mange kommandoer og alternativer. Den mest grunnleggende kommandoen er toppN; med -c-alternativet viser de øverste N-funksjonene som tok den mest kumulative tiden til å utføre (slik at en funksjon som tar svært lite tid til å utføre, men kalles mange ganger, kan være øverst). Dette er vanligvis det jeg starter med.

> gå verktøy pprof cpu.prof Type: cpu Tid: 23. oktober 2017 klokka 8:05 (PDT) Varighet: 13.22s, Samlet antall prøver = 13.10s (99.06%) Entering interactive mode (type "hjelp" for kommandoer) (pprof ) top5-cum Viser noder som regner med 1.23s, 9.39% av 13.10s totalt Dropped 76 noder (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 

La oss forstå utgangen. Hver rad representerer en funksjon. Jeg ledet banen til hver funksjon på grunn av plassbegrensninger, men det vil vises i den virkelige produksjonen som den siste kolonnen. 

Flat betyr tidspunktet (eller prosentdelen) som tilbys innenfor funksjonen, og Cum står for kumulativ-tiden som tilbys i funksjonen og alle funksjonene det ringer. I dette tilfellet, testing.tRunner faktisk ringer TestFindLargestProduct (), som ringer FindLargestProduct (), men siden praktisk talt ingen tid er brukt der, teller prøveprofileren sin flate tid som 0.

Memory Profiling

Minneprofilering er lik, bortsett fra at du lager en minneprofil:

gå test -profil mem.prof-bench. PASS ok _ / github.com / the-gigi / project-euler / 8 / go / trivial

Du kan analysere minnebruken din ved hjelp av det samme verktøyet.

Bruke pprof for å optimalisere programmets hastighet

La oss se hva vi kan gjøre for å løse problemet raskere. Ser vi på profilen ser vi det calcProduct () tar 8,17% av den flate kjøretiden, men makeSlice (), som kalles fra calcProduct (), tar 72% (kumulativ fordi den kaller andre funksjoner). Dette gir en ganske god indikasjon på hva vi trenger for å optimalisere. Hva gjør koden? For hver sekvens av 13 tilgrensende tall tildeler den et stykke:

func calcProdukt (serie streng) int64 siffer: = lage ([] int64, len (serie)) ... 

Det er nesten 1000 ganger per løp, og vi løper 100.000 ganger. Minneallokeringene er tregte. I dette tilfellet er det egentlig ikke nødvendig å tildele en ny stykke hver gang. Egentlig er det ikke nødvendig å tildele noe stykke i det hele tatt. Vi kan bare skanne inngangsarrangementet. 

Følgende kodestykke viser hvordan du beregner løpeproduktet ved å bare dividere med det første sifferet i forrige sekvens og multiplisere med cur sifret. 

hvis det går == 1 currProduct / = old fortsetter hvis gammel == 1 currProduct * = cur else currProduct = currProduct / old * cur hvis currProduct> largestProduct largestProduct = currProduct 

Her er en kort liste over noen av de algoritmiske optimaliseringene:

  • Beregning av et løpende produkt. Anta at vi har beregnet produktet på indeksen N ... N + 13 og kaller det P (N). Nå må vi beregne produktet på indeksen N + 1 ... N + 13. P (N + 1) tilsvarer P (N) bortsett fra at det første nummeret på indeksen N er borte, og vi må ta hensyn til det nye nummeret på indeksen N + 14T. Dette kan gjøres ved å dele det forrige produktet med sitt første nummer og multiplisere med det nye nummeret. 
  • Beregner ikke noen sekvens av 13 tall som inneholder 0 (produktet vil alltid være null).
  • Unngå divisjon eller multiplikasjon med 1.

Det komplette programmet er her. Det er noe tornet logikk å jobbe rundt nuller, men annet enn at det er ganske greit. Det viktigste er at vi bare tildeler en rekke 1000 byte i begynnelsen, og vi sender det med pekeren (så ingen kopi) til  findLargestProductInSeries () fungere med en rekke indekser.

pakke skanning func findLargeProductInSeries (siffer * [1000] byte, start, slutt 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 > størsteProduct largestProduct = currProduct returnere størsteProdukt func FindLargeProdukt (tekststreng) int64 var siffer [1000] byte digIndex: = 0 for _, c: = rekkevidde tekst hvis c == 10 forts sifre [digIndex] = byte (c) - 48 digIndex ++ start: = -1 ende: = -1 findStart: = true var largestProdukt int64 for 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 > størsteProdukt largestProduct = result findStart = true returnere størsteProdukt

Testen er den samme. La oss se hvordan vi gjorde med profilen:

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

Rett utenfor flaggermuset ser vi at kjøretiden falt fra mer enn 13 sekunder til mindre enn ett sekund. Det er ganske bra. Tid til å se på innsiden. La oss bruke bare topp 10, som sorterer etter flat tid.

(pprof) top10 Viser noder som tegner seg for 560ms, 100% av 560ms totalt flat flat% sum% cum cum% 290ms 51,79% 51,79% 290ms 51,79% foundLargeProductInSeries 250ms 44,64% 96,43% 540ms 96,43% FindLargeProduct 20ms 3,57% 100% 20ms 3,57% runtime .usleep 0 0% 100% 540ms 96.43% TestFindLargeProduct 0 0% 100% 20ms 3,57% runtime.mstart 0 0% 100% 20ms 3,57% runtime.mstart1 0 0% 100% 20ms 3,57% runtime.sysmon 0 0% 100% 540ms 96.43% testing.tRunner 

Dette er flott. Ganske mye hele kjøretiden er brukt i vår kode. Ingen minneallokeringer i det hele tatt. Vi kan dykke dypere og se på setningsnivået med listekommandoen:

(pprof) liste Finn StørsteProdukt Total: 560ms ROUTINE ======================== scan.FindLargeProdukt 250ms 540ms (flat, cum) 96.43% av totalt ... 44: ... 45: ... 46: func FindLargeProduct (t streng) int64 ... 47: var siffer [1000] byte ... 48: digIndex: = 0 70ms 70ms 49: for _, c: = rekkefølge tekst ... 50: hvis c == 10 ... 51: fortsett ... 52: ... 53: siffer [digIndex] = byte (c) - 48 10ms 10ms 54: digIndex ++ ... 55: ... 56: ... 57: start: = -1 ... 58: ende: = -1 ... 59: findStart: = true ... 60: var largestProduct int64 ... 61: for 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 > størsteProdukt ... 75: størsteProdukt = resultat ... 76: ... 77: findStart = true ... 78: ... 79:

Dette er ganske utrolig. Du får en redegjørelse ved oppsummering av alle viktige punkter. Legg merke til at samtalen på linje 73 til funksjon f () er faktisk et kall til findLargestProductInSeries (), som jeg omdøpt i profilen på grunn av plassbegrensninger. Denne anropet tar 20 ms. Kanskje, ved å legge inn funksjonskoden på plass, kan vi lagre funksjonsanropet (inkludert allokering av stakk- og kopieringsargumenter) og lagre de 20 ms. Det kan være andre lønnsomme optimaliseringer som denne visningen kan bidra til å identifisere.

visualisering

Ser på disse tekstprofiler kan være vanskelig for store programmer. Go gir deg mange visualiseringsalternativer. Du må installere Graphviz for neste avsnitt.

Pprof-verktøyet kan generere produksjon i mange formater. En av de enkleste måtene (svg-utdata) er ganske enkelt å skrive «web» fra pprof-interaktiv spørring, og nettleseren din vil vise en fin graf med den varme banen merket med rosa.

Flammediagrammer

De innebygde grafene er hyggelige og hjelpsomme, men med store programmer kan det være vanskelig å utforske disse grafene. Et av de mest populære verktøyene for å visualisere resultatresultater er flammediagrammet. Pprof-verktøyet støtter det ikke ut av esken ennå, men du kan spille med flammediagrammer som allerede bruker Ubers fakkelverktøy. Det er pågående arbeid for å legge til innebygd støtte for flammediagrammer til pprof.

Konklusjon

Go er et system programmeringsspråk som brukes til å bygge high performance distribuerte systemer og datalager. Go kommer med utmerket støtte som fortsetter å bli bedre til profilering av programmene dine, analyse av ytelsen og visualisering av resultatene. 

Det legges stor vekt på Go-teamet og samfunnet om å forbedre verktøyet rundt ytelsen. Den fullstendige kildekoden med tre forskjellige algoritmer finnes på GitHub.