Maak uw Go-programma's bliksemsnel met profilering

Go wordt vaak gebruikt voor het schrijven van gedistribueerde systemen, geavanceerde datastores en microservices. Prestaties zijn de sleutelwoorden in deze domeinen. 

In deze tutorial leer je hoe je je programma's kunt profileren om ze razendsnel te maken (gebruik de CPU beter) of vederlicht (gebruik minder geheugen). Ik zal CPU- en geheugenprofilering behandelen met behulp van de pprof (de Go-profiler), de profielen visualiseren en zelfs vlammengrafieken.

Profilering meet de prestaties van uw programma in verschillende dimensies. Go biedt uitstekende ondersteuning voor profilering en kan de volgende dimensies uit de doos profileren:

  • een steekproef van CPU-tijd per functie EN instructie
  • een steekproef van alle heap allocaties
  • stapel sporen van alle huidige goroutines
  • stapels traceren die hebben geleid tot het maken van nieuwe OS-threads
  • stacksporen die leidden tot blokkering van synchronisatieprimitieven
  • stapel sporen van houders van betwiste mutexen

U kunt zelfs aangepaste profielen maken als u dat wilt. Ga profileren impliceert het maken van een profielbestand en het vervolgens analyseren met behulp van de pprof ga gereedschap.

Hoe profielbestanden te maken

Er zijn verschillende manieren om een ​​profielbestand te maken.

"Go-test" gebruiken om profielbestanden te genereren

De gemakkelijkste manier is om te gebruiken ga testen. Het heeft verschillende vlaggen waarmee u profielbestanden kunt maken. U kunt als volgt een CPU-profielbestand en een geheugenprofielbestand genereren voor de test in de huidige map: ga test -cpuprofile cpu.prof -memprofile mem.prof -bench .

Live-profielgegevens downloaden van een langlopende service

Als u een langlopende webservice wilt profileren, kunt u de ingebouwde HTTP-interface gebruiken voor het verstrekken van profielgegevens. Voeg ergens de volgende importverklaring toe:

import _ "net / http / pprof"

U kunt nu live profielgegevens downloaden van de / Debug / pprof / URL. Meer informatie is beschikbaar in de documentatie van het net / http / pprof-pakket.

Profilering in code

U kunt ook directe profilering toevoegen aan uw code voor volledige controle. Eerst moet je importeren runtime / pprof. CPU-profilering wordt geregeld door twee oproepen:

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

Geheugenprofilering gebeurt door te bellen runtime.GC () gevolgd door pprof.WriteHeapProfile ().

Alle profileringsfuncties accepteren een bestandsingreep die u zelf moet openen en sluiten.

Het voorbeeldprogramma

Om de Profiler in actie te zien, gebruik ik een programma dat Project Euler's Problem 8 oplost. Het probleem is: als je een getal van 1.000 cijfers krijgt, vind je de 13 aangrenzende cijfers binnen dit nummer die het grootste product hebben. 

Hier is een triviale oplossing die itereert over alle reeksen van 13 cijfers en voor elke reeks vermenigvuldigt deze alle 13 cijfers en retourneert het resultaat. Het grootste resultaat wordt opgeslagen en uiteindelijk geretourneerd:

package trivial import ("strings") func calcProduct (series string) int64 digits: = make ([] int64, len (series)) voor i, c: = reeks reeksen digits [i] = int64 (c) - 48  product: = int64 (1) voor i: = 0; ik < 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) reeks: = tekst [i: eind] resultaat: = calcProduct (serie) indien resultaat> grootsteProduct largestProduct = resultaat retourneer largestProduct 

Later, na het profileren, zullen we enkele manieren zien om de prestaties te verbeteren met een andere oplossing.

CPU-profilering

Laten we de CPU van ons programma profileren. Ik gebruik de go-testmethode met deze test:

import ( "test") 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 * testing.T) for i: = 0; ik < 100000; i++  res := FindLargestProduct(text) expected := int64(23514624000) if res != expected  t.Errorf("Wrong!")    

Houd er rekening mee dat ik de test 100.000 keer voer omdat de go-profiler een samplingprofiler is die de code nodig heeft om enige significante tijd (meerdere milliseconden cumulatief) aan elke regel code te spenderen. Hier is de opdracht om het profiel voor te bereiden:

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

Het duurde iets meer dan 13 seconden (voor 100.000 herhalingen). Om nu het profiel te bekijken, gebruik je de pprof go-tool om in de interactieve prompt te komen. Er zijn veel opdrachten en opties. Het meest elementaire commando is topN; met de optie -cum toont het de bovenste N-functies die de meest cumulatieve tijd namen om uit te voeren (dus een functie die heel weinig tijd kost om uit te voeren, maar vele malen wordt genoemd, kan bovenaan staan). Dit is meestal waar ik mee begin.

> go tool pprof cpu.prof Type: cpu Tijd: Oct 23, 2017 om 8:05 am (PDT) Duur: 13.22s, Total samples = 13.10s (99.06%) Interactieve modus activeren (type "help" voor opdrachten) (pprof ) top5 -circuleren Knoppen voor 1,23s tonen, 9,39% van 13,10s totaal Dropped 76-knooppunten (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 

Laten we de uitvoer begrijpen. Elke rij vertegenwoordigt een functie. Ik heb het pad naar elke functie weggelaten vanwege ruimtebeperkingen, maar dit wordt in de echte uitvoer weergegeven als de laatste kolom. 

Flat betekent de tijd (of het percentage) doorgebracht in de functie, en Cum staat voor cumulatief: de tijd die is besteed aan de functie en alle functies die deze aanroept. In dit geval, testing.tRunner roept eigenlijk TestFindLargestProduct (), welke oproepen FindLargestProduct (), maar aangezien er vrijwel geen tijd wordt besteed, telt de bemonsteringsprofiler hun vaste tijd als 0.

Geheugen Profilering

Geheugenprofilering is vergelijkbaar, behalve dat u een geheugenprofiel maakt:

ga testen -memprofile mem.prof -bench. PASS ok _ / github.com / the-gigi / project-euler / 8 / go / triviaal

U kunt uw geheugengebruik analyseren met behulp van dezelfde tool.

Gebruik van pprof om de snelheid van uw programma te optimaliseren

Laten we eens kijken wat we kunnen doen om het probleem sneller op te lossen. Kijkend naar het profiel, zien we dat calcProduct () duurt 8,17% van de flatruntime, maar makeSlice (), waar vandaan wordt geroepen calcProduct (), neemt 72% (cumulatief omdat het andere functies oproept). Dit geeft een vrij goede indicatie van wat we moeten optimaliseren. Wat doet de code? Voor elke reeks van 13 aangrenzende getallen wijst het een segment toe:

func calcProduct (series string) int64 digits: = make ([] int64, len (series)) ... 

Dat is bijna 1.000 keer per run en we lopen 100.000 keer. Geheugentoewijzingen zijn traag. In dit geval is het echt niet nodig om elke keer een nieuw segment toe te wijzen. Eigenlijk is het niet nodig om een ​​segment toe te wijzen. We kunnen gewoon de invoerarray scannen. 

Het volgende codefragment laat zien hoe het lopende product te berekenen door simpelweg te delen door het eerste cijfer van de vorige reeks en te vermenigvuldigen met de cur cijfer. 

if cur == 1 currProduct / = old continue if old == 1 currProduct * = cur else currProduct = currProduct / old * cur if currProduct> largestProduct largestProduct = currProduct 

Hier is een korte lijst van enkele van de algoritmische optimalisaties:

  • Een lopend product berekenen. Stel dat we het product hebben berekend op index N ... N + 13 en dit P (N) noemen. Nu moeten we het product berekenen bij index N + 1 ... N + 13. P (N + 1) is gelijk aan P (N) behalve dat het eerste nummer bij index N weg is en we moeten rekening houden met het nieuwe nummer bij index N + 14T. Dit kan gedaan worden door het vorige product te delen door zijn eerste nummer en te vermenigvuldigen met het nieuwe nummer. 
  • Geen enkele reeks van 13 getallen berekenen die 0 bevatten (het product zal altijd nul zijn).
  • Splitsing of vermenigvuldiging met 1 voorkomen.

Het complete programma is hier. Er is een netelige logica om rond de nullen te werken, maar verder is het vrij eenvoudig. Het belangrijkste is dat we aan het begin slechts één array van 1000 bytes toewijzen, en we geven deze door aanwijzer (dus geen kopie) door aan de  findLargestProductInSeries () functie met een reeks indexen.

pakket scan functie vindLargestProductInSeries (cijfers * [1000] byte, start, einde int) int64 if (einde - begin) < 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 retourneer largestProduct func FindLargestProduct (tekstreeks) int64 var digits [1000] byte digIndex: = 0 voor _, c: = bereiktekst if c == 10 continue digits [digIndex] = byte (c) - 48 digIndex ++ start: = -1 end: = -1 findStart: = true var largestProduct int64 voor 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 largestProduct = result findStart = true retourneer largestProduct

De test is hetzelfde. Laten we eens kijken hoe we het hebben gedaan met het profiel:

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

Direct uit de vleermuis, kunnen we zien dat de looptijd daalde van meer dan 13 seconden naar minder dan een seconde. Dat is best goed. Tijd om naar binnen te gluren. Laten we gewoon gebruiken top 10, welke sorteert op tijd.

(pprof) top10 Knopen tonen die 560ms vertegenwoordigen, 100% van 560ms totaal vlak flat% som% 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% runtime .usleep 0 0% 100% 540ms 96,43% TestFindLarsteProduct 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 

Dit is geweldig. Vrijwel de volledige runtime wordt doorgebracht in onze code. Geen geheugentoewijzingen. We kunnen dieper duiken en kijken naar het statement-niveau met de lijstopdracht:

(pprof) lijst FindLargestProduct Totaal: 560ms ROUTINE ======================== scan.FindLargestProduct 250ms 540ms (vlak, cum) 96,43% van Total ... 44: ... 45: ... 46: func FindLargestProduct (t string) int64 ... 47: var digits [1000] byte ... 48: digIndex: = 0 70ms 70ms 49: for _, c: = range text ... 50: if c == 10 ... 51: verder ... 52: ... 53: cijfers [digIndex] = byte (c) - 48 10ms 10ms 54: digIndex ++ ... 55: ... 56: ... 57: start: = -1 ... 58: end: = -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 > largestProduct ... 75: largestProduct = resultaat ... 76: ... 77: findStart = true ... 78: ... 79:

Dit is behoorlijk verbluffend. Je krijgt een verklaring door de timing van de verklaring van alle belangrijke punten. Merk op dat de oproep op lijn 73 naar functie f () is eigenlijk een oproep aan findLargestProductInSeries (), die ik vanwege ruimtebeperkingen in het profiel hernoemd heb. Deze oproep duurt 20 ms. Misschien kunnen we, door de functiecode in te bedden, de functieaanroep opslaan (inclusief het toewijzen van stack- en kopieerargumenten) en die 20 ms opslaan. Er kunnen andere waardevolle optimalisaties zijn die deze weergave kan helpen lokaliseren.

visualisatie

Het bekijken van deze tekstprofielen kan moeilijk zijn voor grote programma's. Go geeft je veel visualisatie-opties. Je zult Graphviz voor de volgende sectie moeten installeren.

De pprof-tool kan uitvoer in vele formaten genereren. Een van de eenvoudigste manieren (svg-uitvoer) is eenvoudigweg 'web' typen via de interactieve prompt van pprof en uw browser geeft een mooie grafiek weer met het warme pad gemarkeerd in roze.

Vlamgrafieken

De ingebouwde grafieken zijn leuk en nuttig, maar met grote programma's kunnen zelfs deze grafieken moeilijk te ontdekken zijn. Een van de meest populaire hulpmiddelen voor het visualiseren van prestatieresultaten is de vlamgrafiek. De pprof-tool ondersteunt het nog niet uit de doos, maar je kunt spelen met vlamgrafieken die al gebruik maken van de go-torch-tool van Uber. Er is voortdurend werk aan het toevoegen van ingebouwde ondersteuning voor vlamgrafieken aan pprof.

Conclusie

Go is een systeem programmeertaal die wordt gebruikt om high-performance gedistribueerde systemen en data stores te bouwen. Go wordt geleverd met uitstekende ondersteuning die steeds beter wordt voor het profileren van uw programma's, het analyseren van hun prestaties en het visualiseren van de resultaten. 

Het Go-team en de community leggen veel nadruk op het verbeteren van de tooling rondom prestaties. De volledige broncode met drie verschillende algoritmen is te vinden op GitHub.