Go används ofta för att skriva distribuerade system, avancerade datalager och microservices. Prestanda är nyckeln i dessa domäner.
I den här handledningen lär du dig hur du profilerar dina program för att få dem bli blixtsnabbt (använd CPU bättre) eller fjäderljus (använd mindre minne). Jag kommer att täcka CPU och minnesprofilering, med hjälp av pprof (Go-profileraren), visualisering av profilerna och till och med flammediagram.
Profilering mäter resultatet av ditt program i olika dimensioner. Go kommer med stort stöd för profilering och kan profilera följande dimensioner ur lådan:
Du kan även skapa egna profiler om du vill. Go profilering innebär att skapa en profilfil och sedan analysera den med hjälp av pprof
gå verktyg.
Det finns flera sätt att skapa en profilfil.
Det enklaste sättet är att använda gå test
. Det har flera flaggor som låter dig skapa profilfiler. Så här skapar du både en CPU-profilfil och en minnesprofilfil för testet i den aktuella katalogen: gå test -cpuprofile cpu.prof -memprofile mem.prof -bench .
Om du vill profilera en långsiktig webbtjänst kan du använda det inbyggda HTTP-gränssnittet för att tillhandahålla profildata. Lägg till någonstans följande importdeklaration:
import _ "net / http / pprof"
Nu kan du hämta liveprofildata från / Debug / pprof /
URL. Mer information finns i dokumentationen för nätet / http / pprof-paketet.
Du kan också lägga direkt profilering till din kod för fullständig kontroll. Först måste du importera runtime / pprof
. CPU-profilering styrs av två samtal:
pprof.StartCPUProfile ()
pprof.StopCPUProfile ()
Minnesprofilering görs genom att ringa runtime.GC ()
följd av pprof.WriteHeapProfile ()
.
Alla profilfunktioner accepterar ett filhandtag som du ansvarar för att öppna och stänga på lämpligt sätt.
För att se profiler i åtgärd använder jag ett program som löser Project Eulers problem 8. Problemet är: med ett 1000-siffrigt tal, hitta de 13 intilliggande siffrorna inom det här numret som har störst produkt.
Här är en trivial lösning som itererar över alla sekvenser med 13 siffror, och för varje sådan sekvens multiplicerar den alla 13 siffror och returnerar resultatet. Det största resultatet lagras och återlämnas slutligen:
paketet trivial import ("strängar") func calcProduct (seriestring) int64 siffror: = gör ([] int64, len (serie)) för i, c: = serieserie siffror [i] = int64 (c) - 48 produkt: = int64 (1) för i: = 0; jag < 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 text = text serie: = text [i: slutet] resultat: = calcProdukt (serie) om resultatet> störstaProdukt largestProduct = result returnera störstProdukt
Senare, efter profilering, ser vi några sätt att förbättra prestanda med en annan lösning.
Låt oss profilera CPU i vårt program. Jag använder go testmetoden med detta test:
import ( "tester") const text = '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) för i: = 0; jag < 100000; i++ res := FindLargestProduct(text) expected := int64(23514624000) if res != expected t.Errorf("Wrong!")
Observera att jag kör testet 100 000 gånger eftersom go-profiler är en provprofil som behöver koden att faktiskt tillbringa en viss betydande tid (flera millisekunder kumulativ) på varje kodrad. Här är kommandot att förbereda profilen:
gå test -cpuprofile cpu.prof -bench. ok _ / github.com / the-gigi / project-euler / 8 / go / trivial 13.243s
Det tog lite över 13 sekunder (för 100 000 iterationer). Nu, för att visa profilen, använd pprof go-verktyget för att komma in i den interaktiva prompten. Det finns många kommandon och alternativ. Det mest grundläggande kommandot är topN; med -c alternativet visar de översta N-funktionerna som tog den mest kumulativa tiden att utföra (så en funktion som tar väldigt liten tid att köra, men kallas många gånger, kan vara högst upp). Detta är vanligtvis vad jag börjar med.
> go tool pprof cpu.prof Typ: cpu Tid: 23 oktober 2017 kl 8:05 (PDT) Varaktighet: 13.22s, Totalt antal prov = 13.10s (99.06%) Entering interactive mode (typ "hjälp" för kommandon) (pprof ) top5-cum Visar noder som svarar för 1.23s, 9.39% av 13.10s totala 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
Låt oss förstå produktionen. Varje rad representerar en funktion. Jag ledde vägen till varje funktion på grund av rymdbegränsningar, men den kommer att visas i den verkliga utgången som den sista kolumnen.
Platt betyder tiden (eller procentsatsen) som används inom funktionen, och Cum står för kumulativ-tiden som används inom funktionen och alla funktioner som den samtalar. I detta fall, testing.tRunner
ringer faktiskt TestFindLargestProduct ()
, som kallar FindLargestProduct ()
, men eftersom praktiskt taget ingen tid spenderas där, tar provprofilen sin platta tid som 0.
Minnesprofilering är liknande, förutom att du skapar en minnesprofil:
gå test -profil mem.prof-bench. PASS ok _ / github.com / the-gigi / project-euler / 8 / go / trivial
Du kan analysera din minnesanvändning med samma verktyg.
Låt oss se vad vi kan göra för att lösa problemet snabbare. Titta på profilen ser vi det calcProduct ()
tar 8,17% av den platta körtiden, men makeSlice ()
, som kallas från calcProduct ()
, tar 72% (kumulativ eftersom det kallar andra funktioner). Detta ger en ganska bra indikation på vad vi behöver optimera. Vad gör koden? För varje sekvens av 13 angränsande nummer allokerar den en skiva:
func calcProduct (seriestring) int64 cif: = make ([] int64, len (serie)) ...
Det är nästan 1000 gånger per körning, och vi kör 100.000 gånger. Minnesfördelningen är långsam. I det här fallet är det verkligen inte nödvändigt att allokera en ny skiva varje gång. Faktum är att det inte finns något behov av att allokera någon skiva alls. Vi kan bara skanna input-matrisen.
Följande kodsekvens visar hur du beräknar löpande produkt genom att helt enkelt dividera med den första siffran i föregående sekvens och multiplicera med byracka
siffra.
om cur == 1 currProduct / = old fortsätt om gammal == 1 currProduct * = cur else currProduct = currProduct / old * cur om currProduct> largestProduct largestProduct = currProduct
Här är en kort lista över några av de algoritmiska optimeringarna:
Det fullständiga programmet är här. Det finns en viss tornig logik att arbeta runt nollorna, men förutom det är det ganska enkelt. Huvuddelen är att vi bara fördelar en uppsättning av 1000 byte i början, och vi skickar det med pekaren (så ingen kopia) till findLargestProductInSeries ()
funktion med ett antal index.
paketsökning func findLargeProductInSeries (siffror * [1000] byte, start, slut 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 > largestProduct largestProduct = currProduct returnera störstProdukt func FindLargeProduct (textsträng) int64 var siffror [1000] byte digIndex: = 0 för _, c: = intervalltext if c == 10 fortsätt siffror [digIndex] = byte (c) - 48 digIndex ++ start: = -1 slut: = -1 findStart: = true var largestProduct int64 för 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örstaProdukt largestProduct = result findStart = true returnera largestProduct
Testet är detsamma. Låt oss se hur vi gjorde med profilen:
> gå test -cpuprofile cpu.prof -bench. PASS ok _ / github.com / the-gigi / project-euler / 8 / go / scan 0.816s
Strax utanför flaggan kan vi se att körtiden sjönk från mer än 13 sekunder till mindre än en sekund. Det är ganska bra. Tid att kika inuti. Låt oss bara använda topp 10
, vilken sorterar av platt tid.
(pprof) top10 Visar noder som svarar för 560ms, 100% av 560ms totala platta% summa% cum cum% 290ms 51.79% 51.79% 290ms 51.79% hittaStörstaProdukterInSeries 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% test.tRunner
Det här är bra. Ganska mycket hela körtiden spenderas i vår kod. Inga minnesallokeringar alls. Vi kan dyka djupare och titta på uttalingsnivån med listkommandot:
(pprof) lista FindLargeProduct Total: 560ms ROUTINE ========================== scan.FindLargeProduct 250ms 540ms (flat, cum) 96.43% av totalt ... 44: ... 45: ... 46: func FindLargeProduct (t string) int64 ... 47: var siffror [1000] byte ... 48: digIndex: = 0 70ms 70ms 49: för _, c: = textintervall ... 50: om c == 10 ... 51: fortsätt ... 52: ... 53: siffror [digIndex] = byte (c) - 48 10ms 10ms 54: digIndex ++ ... 55: ... 56: ... 57: start: = -1 ... 58: slut: = -1 ... 59: findStart: = true ... 60: var largestProduct int64 ... 61: för 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örstaProdukt ... 75: störstProdukt = resultat ... 76: ... 77: findStart = true ... 78: ... 79:
Detta är ganska fantastiskt. Du får ett uttalande genom uttalandet timing av alla viktiga punkter. Observera att samtalet på rad 73 till funktion f ()
är faktiskt ett samtal till findLargestProductInSeries ()
, som jag bytte namn på i profilen på grund av utrymme begränsningar. Detta samtal tar 20 ms. Kanske, genom att inbädda funktionskoden på plats, kan vi spara funktionsanropet (inklusive allokering av stack och kopieringsargument) och spara de 20 ms. Det kan finnas andra värdefulla optimeringar som den här uppfattningen kan hjälpa till att hitta.
Att titta på dessa textprofiler kan vara svårt för stora program. Go ger dig många visualiseringsalternativ. Du måste installera Graphviz för nästa avsnitt.
Pprof-verktyget kan generera produktionen i många format. Ett av de enklaste sätten (svg-utdata) är helt enkelt att skriva 'web' från den interaktiva prompten pprof, och din webbläsare kommer att visa ett fint diagram med den heta vägen markerad med rosa.
De inbyggda graferna är trevliga och hjälpsamma, men med stora program kan även dessa grafer vara svåra att utforska. Ett av de mest populära verktygen för att visualisera resultatresultaten är flammediagrammet. Pprof-verktyget stöder det inte ur lådan ännu, men du kan spela med flammediagram som redan använder Ubers brännare. Det finns pågående arbete för att lägga till inbyggt stöd för flamgrafer till pprof.
Go är ett systemprogrammeringsspråk som används för att bygga högpresterande distribuerade system och datalager. Go kommer med utmärkt stöd som håller på att bli bättre för att profilera dina program, analysera deras prestanda och visualisera resultaten.
Det går en hel del betoning av Go-teamet och samhället om att förbättra verktyg kring prestanda. Den fullständiga källkoden med tre olika algoritmer finns på GitHub.