Go wird häufig zum Schreiben verteilter Systeme, fortschrittlicher Datenspeicher und Mikrodienste verwendet. In diesen Bereichen ist Leistung der Schlüssel.
In diesem Lernprogramm erfahren Sie, wie Sie Ihre Programme profilieren, um sie blitzschneller zu machen (die CPU besser zu nutzen) oder Federleichter zu verwenden (weniger Speicher verwenden). Ich werde CPU- und Speicherprofile behandeln, mit dem pprof (dem Go-Profiler) die Profile und sogar Flammengraphen visualisieren.
Profiling misst die Leistung Ihres Programms in verschiedenen Dimensionen. Go bietet großartige Unterstützung für die Profilerstellung und kann die folgenden Abmessungen aus der Verpackung heraus profilieren:
Sie können sogar benutzerdefinierte Profile erstellen, wenn Sie möchten. Beim Go-Profiling wird eine Profildatei erstellt und anschließend mit der analysiert pprof
Werkzeug gehen.
Es gibt verschiedene Möglichkeiten, eine Profildatei zu erstellen.
Der einfachste Weg ist die Verwendung test gehen
. Es verfügt über mehrere Flags, mit denen Sie Profildateien erstellen können. So erstellen Sie im aktuellen Verzeichnis eine CPU-Profildatei und eine Speicherprofildatei für den Test: go test -cpuprofile cpu.prof -memprofile mem.prof -bench .
Wenn Sie einen langlebigen Web-Service erstellen möchten, können Sie die integrierte HTTP-Schnittstelle zum Bereitstellen von Profildaten verwenden. Fügen Sie irgendwo die folgende Importanweisung hinzu:
Import _ "net / http / pprof"
Jetzt können Sie Live-Profildaten aus dem herunterladen / debug / pprof /
URL. Weitere Informationen finden Sie in der Dokumentation zum net / http / pprof-Paket.
Sie können auch direktes Profiling in Ihren Code einfügen, um vollständige Kontrolle zu erhalten. Zuerst müssen Sie importieren Laufzeit / pprof
. Die CPU-Profilerstellung wird durch zwei Aufrufe gesteuert:
pprof.StartCPUProfile ()
pprof.StopCPUProfile ()
Die Speicherprofilerstellung erfolgt durch Aufrufen runtime.GC ()
gefolgt von pprof.WriteHeapProfile ()
.
Alle Profiling-Funktionen akzeptieren ein Dateihandle, für das Sie das entsprechende Öffnen und Schließen verantwortlich sind.
Um den Profiler in Aktion zu sehen, verwende ich ein Programm, das Project Eulers Problem 8 löst. Das Problem ist: Geben Sie bei einer 1.000-stelligen Nummer die 13 nebenstehenden Ziffern in dieser Nummer an, die das größte Produkt haben.
Hier ist eine triviale Lösung, die alle 13-stelligen Sequenzen durchläuft und für jede dieser Sequenzen alle 13 Ziffern multipliziert und das Ergebnis zurückgibt. Das größte Ergebnis wird gespeichert und schließlich zurückgegeben:
trivialer Import von Paketen ("Zeichenfolgen") func calcProduct (Serienzeichenfolge) int64 digit: = make ([] int64, len (series)) für i, c: = range series Ziffern [i] = int64 (c) - 48 product: = int64 (1) für i: = 0; ich < 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) Ende = Len (Text) Reihe: = Text [i: Ende] Ergebnis: = calcProduct (Reihe) wenn Ergebnis> größtesProdukt größteProdukt = Ergebnis größtes Produkt zurückgeben
Später, nach dem Profiling, werden wir einige Möglichkeiten sehen, die Leistung mit einer anderen Lösung zu verbessern.
Lassen Sie uns die CPU unseres Programms profilieren. Ich verwende die Go-Test-Methode mit diesem Test:
import ( "Prüfung") 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) für i: = 0; ich < 100000; i++ res := FindLargestProduct(text) expected := int64(23514624000) if res != expected t.Errorf("Wrong!")
Beachten Sie, dass ich den Test 100.000 Mal durchführe, da der go-Profiler ein Sampling-Profiler ist, für den der Code tatsächlich einige Zeit (mehrere Millisekunden kumulativ) für jede Codezeile benötigt. Hier ist der Befehl, um das Profil vorzubereiten:
go test -cpuprofile cpu.prof -bench. ok _ / github.com / the-gigi / project-euler / 8 / go / trivial 13.243s
Es dauerte etwas mehr als 13 Sekunden (für 100.000 Iterationen). Um das Profil anzuzeigen, verwenden Sie das Tool pprof go, um zur interaktiven Eingabeaufforderung zu gelangen. Es gibt viele Befehle und Optionen. Der grundlegendste Befehl ist topN. Mit der Option -cum werden die Top-N-Funktionen angezeigt, deren Ausführung die meiste Zeit in Anspruch nahm (eine Funktion, deren Ausführung sehr wenig Zeit in Anspruch nimmt, die jedoch oft aufgerufen wird, kann ganz oben stehen). Damit fange ich normalerweise an.
> go tool pprof cpu.prof Typ: cpu Zeit: 23. Oktober 2017 um 08:05 Uhr (PDT) Dauer: 13.22s, Samples gesamt = 13.10s (99.06%) Eingabe des interaktiven Modus (geben Sie "help" für Befehle ein) (pprof ) top5 -cum Zeigt Knoten an, die 1,23s ausmachen, 9,39% von 13,10s. Insgesamt 76 Knoten (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
Verstehen wir die Ausgabe. Jede Zeile repräsentiert eine Funktion. Ich habe den Pfad zu jeder Funktion aufgrund von Platzbeschränkungen ausgewählt, aber er wird in der tatsächlichen Ausgabe als letzte Spalte angezeigt.
Flat bedeutet die Zeit (oder den Prozentsatz), die innerhalb der Funktion verbracht wurde, und Cum steht für kumulativ - die Zeit, die innerhalb der Funktion und aller aufgerufenen Funktionen verbracht wird. In diesem Fall, testing.tRunner
ruft tatsächlich an TestFindLargestProduct ()
, was ruft FindLargestProduct ()
, Da dort jedoch praktisch keine Zeit aufgewendet wird, zählt der Sampling-Profiler seine pausenlose Zeit als 0.
Die Speicherprofilerstellung ist ähnlich, mit der Ausnahme, dass Sie ein Speicherprofil erstellen:
go test -memprofile mem.prof -bench. PASS ok _ / github.com / the-gigi / project-euler / 8 / go / trivial
Sie können Ihre Speichernutzung mit demselben Tool analysieren.
Mal sehen, was wir tun können, um das Problem schneller zu lösen. Wenn wir das Profil betrachten, sehen wir das calcProduct ()
nimmt 8,17% der flachen Laufzeit, aber makeSlice ()
, welches von aufgerufen wird calcProduct ()
, nimmt 72% (kumulativ, weil es andere Funktionen aufruft). Dies gibt einen ziemlich guten Hinweis darauf, was wir optimieren müssen. Was macht der Code? Für jede Folge von 13 benachbarten Zahlen wird ein Slice zugewiesen:
func calcProduct (serielle Zeichenfolge) int64 Ziffern: = make ([] int64, len (series))…
Das sind fast 1.000 Mal pro Lauf, und wir laufen 100.000 Mal. Die Speicherzuordnung ist langsam. In diesem Fall muss wirklich nicht jedes Mal ein neues Segment zugewiesen werden. Tatsächlich besteht keine Notwendigkeit, überhaupt ein Segment zuzuordnen. Wir können das Eingabe-Array einfach scannen.
Das folgende Codefragment zeigt, wie das laufende Produkt berechnet wird, indem einfach durch die erste Ziffer der vorherigen Sequenz dividiert und mit multipliziert wird cur
Ziffer.
wenn cur == 1 currProduct / = old fortsetzen wenn old == 1 currProduct * = cur else currProduct = currProduct / old * cur wenn currProduct> größteProdukt größteProdukt = currProduct
Hier ist eine kurze Liste einiger algorithmischer Optimierungen:
Das komplette Programm ist hier. Es gibt eine heikle Logik, um die Nullen zu umgehen, aber ansonsten ist es ziemlich unkompliziert. Die Hauptsache ist, dass wir am Anfang nur ein Array von 1000 Bytes zuweisen und es per Zeiger (also keine Kopie) an das übergeben findLargestProductInSeries ()
Funktion mit einer Reihe von Indizes.
Paket-Scan-Funktion findLargestProductInSeries (Ziffern * [1000] Byte, Start, Ende Int) int64 if (Ende - 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 > größteProdukt größteProdukt = currProduct gibt größteProdukt zurück func FindLargestProduct (Textzeichenfolge) int64 var Ziffern [1000] Byte digIndex: = 0 für _, c: = Bereichstext if c == 10 continue Ziffern [digIndex] = Byte (c) - 48 digIndex ++ start: = -1 Ende: = -1 findStart: = true var greatestProduct 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 > größtenProdukt größteProdukt = Ergebnis findStart = true Rückgabe der größten Produkte
Der Test ist derselbe. Mal sehen, wie wir mit dem Profil gearbeitet haben:
> go-test -cpuprofile cpu.prof -bench. PASS ok _ / github.com / the-gigi / project-euler / 8 / go / scan 0.816s
Sofort sieht man, dass die Laufzeit von mehr als 13 Sekunden auf weniger als eine Sekunde gesunken ist. Das ist sehr gut. Zeit, hinein zu schauen. Lass uns einfach verwenden Top 10
, was durch pünktliche Zeit sortiert.
(pprof) top10 Knoten werden angezeigt, die 560 ms ausmachen, 100% von 560ms insgesamt pauschal% sum% cum cum% 290ms 51.79% 51.79% 290ms 51.79% findLargestProductInSeries 250ms 44.64% 96.43% 540ms 96.43% FindLargestProduct 20ms 3.57% 100ms 207% Laufzeit .usleep 0 0% 100% 540ms 96.43% TestFindLargestProduct 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
Das ist toll. So ziemlich die gesamte Laufzeit wird in unserem Code verbracht. Keine Speicherzuweisungen. Wir können tiefer tauchen und die Anweisungsebene mit dem Befehl list betrachten:
(pprof) list FindLargestProduct Total: 560ms ROUTINE ========================================================================================= … 45:… 46: func FindLargestProduct (t string) int64 … 47: var digit [1000] byte… 48: digIndex: = 0 70ms 70ms 49: für _, c: = Bereichstext … 50: if c == 10 … 51: weiter… 52:… 53: Ziffern [digIndex] = Byte (c) - 48 10ms 10ms 54: digIndex ++… 55:… 56:… 57: start: = -1… 58: end: = -1… 59: findStart: = true… 60: var greatestProduct 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 > größteProdukt … 75: größteProdukt = Ergebnis… 76:… 77: findStart = true… 78:… 79:
Das ist ziemlich erstaunlich. Sie erhalten eine Aussage zum zeitlichen Ablauf aller wichtigen Punkte. Beachten Sie, dass der Anruf auf Leitung 73 an Funktion f ()
ist eigentlich ein anruf findLargestProductInSeries ()
, die ich im Profil aus Platzgründen umbenannt habe. Dieser Anruf dauert 20 ms. Durch das Einbetten des Funktionscodes können wir den Funktionsaufruf (einschließlich der Zuweisung von Stapel- und Kopierargumenten) und die 20 ms speichern. Es kann andere lohnenswerte Optimierungen geben, die diese Ansicht bei der Suche unterstützen kann.
Das Betrachten dieser Textprofile kann für große Programme schwierig sein. Go bietet Ihnen viele Visualisierungsoptionen. Sie müssen Graphviz für den nächsten Abschnitt installieren.
Das pprof-Tool kann Ausgaben in vielen Formaten generieren. Eine der einfachsten Methoden (svg-Ausgabe) besteht darin, einfach "web" über die interaktive Eingabeaufforderung von pprof einzugeben, und Ihr Browser zeigt ein schönes Diagramm mit dem Hot-Pfad in Rosa an.
Die integrierten Diagramme sind nett und hilfreich, aber bei großen Programmen kann es schwierig sein, auch diese Diagramme zu untersuchen. Eines der beliebtesten Werkzeuge zur Visualisierung von Leistungsergebnissen ist der Flammengraph. Das pprof-Tool unterstützt es noch nicht sofort, aber Sie können bereits mit Flammgraphen spielen, wenn Sie das Uber-Go-Torch-Tool verwenden. Derzeit wird daran gearbeitet, die integrierte Unterstützung für Flammengraphen zu pprof hinzuzufügen.
Go ist eine Systemprogrammiersprache, mit der hochleistungsfähige verteilte Systeme und Datenspeicher erstellt werden. Go bietet eine hervorragende Unterstützung, die es Ihnen ermöglicht, Ihre Programme besser zu profilieren, ihre Leistung zu analysieren und die Ergebnisse zu visualisieren.
Das Go-Team und die Community legen großen Wert auf die Verbesserung der Tools rund um die Leistung. Den vollständigen Quellcode mit drei verschiedenen Algorithmen finden Sie auf GitHub.