Analyse de performance
Lorsqu'un programme est terminé, nos exigences ne se limitent pas seulement à son exécution, nous souhaitons également qu'il s'agisse d'une application stable et efficace. Grâce à divers tests, nous pouvons garantir la plupart de la stabilité du programme. Pour déterminer si le programme est efficace, nous devons effectuer une analyse de performance. Dans le contenu précédent, le seul moyen d'analyse de performance était d'utiliser Benchmark pour tester le temps d'exécution moyen d'une unité fonctionnelle, la situation d'allocation de mémoire, etc. Cependant, dans la réalité, les besoins d'analyse de performance des programmes vont bien au-delà. Parfois, nous devons analyser l'utilisation globale du CPU du programme, l'utilisation de la mémoire, la situation d'allocation du tas, l'état des goroutines, les chemins de code critiques, etc., ce que Benchmark ne peut pas satisfaire. Heureusement, la chaîne d'outils Go intègre de nombreux outils d'analyse de performance à la disposition des développeurs. Nous allons les expliquer un par un.
Analyse d'échappement
En Go, l'allocation de mémoire des variables est décidée par le compilateur, généralement sur la pile ou sur le tas. Si une variable qui devrait être allouée sur la pile est allouée sur le tas, cette situation est appelée échappement. L'analyse d'échappement vise à analyser la situation d'allocation de mémoire dans le programme. Comme elle est effectuée pendant la compilation, il s'agit d'une analyse statique.
TIP
Consultez l'article Allocation de mémoire pour comprendre comment Go alloue la mémoire.
Référence de pointeur local
package main
func main() {
GetPerson()
}
type Person struct {
Name string
Mom *Person
}
func GetPerson() Person {
mom := Person{Name: "lili"}
son := Person{Name: "jack", Mom: &mom}
return son
}Dans la fonction GetPerson, la variable mom est créée. Comme elle est créée dans la fonction, elle devrait normalement être allouée sur la pile. Cependant, elle est référencée par le champ Mom de son, et son est retourné comme valeur de retour de la fonction. Le compilateur l'alloue donc sur le tas. Il s'agit d'un exemple très simple, donc la compréhension ne demande pas trop d'efforts. Mais s'il s'agit d'un projet plus important avec des dizaines de milliers de lignes de code, l'analyse manuelle n'est pas si facile. C'est pourquoi il est nécessaire d'utiliser des outils pour effectuer une analyse d'échappement. Comme mentionné précédemment, l'allocation de mémoire est dirigée par le compilateur, donc l'analyse d'échappement est également effectuée par le compilateur. L'utilisation est très simple, il suffit d'exécuter la commande suivante :
$ go build -gcflags="-m -m -l"gcflags correspond aux paramètres du compilateur gc :
-m, affiche les suggestions d'optimisation de code, l'apparition de deux rend la sortie plus détaillée-l, désactive l'optimisation d'inlining
La sortie est la suivante :
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:13:2: mom escapes to heap:
./main.go:13:2: flow: son = &mom:
./main.go:13:2: from &mom (address-of) at ./main.go:14:35
./main.go:13:2: from Person{...} (struct literal element) at ./main.go:14:15
./main.go:13:2: from son := Person{...} (assign) at ./main.go:14:6
./main.go:13:2: flow: ~r0 = son:
./main.go:13:2: from return son (return) at ./main.go:15:2
./main.go:13:2: moved to heap: momLe compilateur nous indique clairement que la variable mom s'échappe, la raison étant que la valeur de retour contient un pointeur local de la fonction. Outre cette situation, d'autres cas peuvent également provoquer un échappement.
::: tips
Si les détails de l'analyse d'échappement vous intéressent, vous pouvez en apprendre davantage dans la bibliothèque standard cmd/compile/internal/escape/escape.go.
:::
Référence de closure
Si une closure référence une variable extérieure à la fonction, cette variable s'échappera également sur le tas, ce qui est facile à comprendre.
package main
func main() {
a := make([]string, 0)
do(func() []string {
return a
})
}
func do(f func() []string) []string {
return f()
}Sortie :
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:10:9: f does not escape
./main.go:4:2: main capturing by value: a (addr=false assign=false width=24)
./main.go:4:11: make([]string, 0) escapes to heap:
./main.go:4:11: flow: a = &{storage for make([]string, 0)}:
./main.go:4:11: from make([]string, 0) (spill) at ./main.go:4:11
./main.go:4:11: from a := make([]string, 0) (assign) at ./main.go:4:4
./main.go:4:11: flow: ~r0 = a:
./main.go:4:11: from return a (return) at ./main.go:6:3
./main.go:4:11: make([]string, 0) escapes to heap
./main.go:5:5: func literal does not escapeEspace insuffisant
Lorsque l'espace de pile est insuffisant, un échappement se produit également. Le slice créé ci-dessous demande une capacité de 1<<15 :
package main
func main() {
_ = make([]int, 0, 1<<15)
}Sortie :
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:4:10: make([]int, 0, 32768) escapes to heap:
./main.go:4:10: flow: {heap} = &{storage for make([]int, 0, 32768)}:
./main.go:4:10: from make([]int, 0, 32768) (too large for stack) at ./main.go:4:10
./main.go:4:10: make([]int, 0, 32768) escapes to heapLongueur inconnue
Lorsque la longueur d'un slice est une variable, en raison de sa longueur inconnue, un échappement se produit (les maps ne le font pas) :
package main
func main() {
n := 100
_ = make([]int, n)
}Sortie :
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:5:10: make([]int, n) escapes to heap:
./main.go:5:10: flow: {heap} = &{storage for make([]int, n)}:
./main.go:5:10: from make([]int, n) (non-constant size) at ./main.go:5:10
./main.go:5:10: make([]int, n) escapes to heapIl existe également un cas spécial où un échappement peut se produire lorsque les paramètres de fonction sont de type ...any :
package main
import "fmt"
func main() {
n := 100
fmt.Println(n)
}Sortie :
$ go build -gcflags="-m -m -l" .
# golearn/example
./main.go:7:14: n escapes to heap:
./main.go:7:14: flow: {storage for ... argument} = &{storage for n}:
./main.go:7:14: from n (spill) at ./main.go:7:14
./main.go:7:14: from ... argument (slice-literal-element) at ./main.go:7:13
./main.go:7:14: flow: {heap} = {storage for ... argument}:
./main.go:7:14: from ... argument (spill) at ./main.go:7:13
./main.go:7:14: from fmt.Println(... argument...) (call parameter) at ./main.go:7:13
./main.go:7:13: ... argument does not escape
./main.go:7:14: n escapes to heapLa raison pour laquelle nous effectuons une analyse d'échappement et contrôlons l'allocation de mémoire de manière aussi détaillée est principalement de réduire la pression sur le GC. Cependant, Go n'est pas le langage C, le pouvoir de décision final sur l'allocation de mémoire reste entre les mains du compilateur. Sauf dans des cas de performances extrêmes, nous n'avons généralement pas besoin de nous concentrer trop sur les détails de l'allocation de mémoire, car le but du GC est de libérer les développeurs.
Petit détail
Pour certains types de référence, lorsqu'il est confirmé qu'ils ne seront plus utilisés, nous pouvons les définir à nil pour indiquer au GC qu'il peut les récupérer.
type Writer struct {
buf []byte
}
func (w Writer) Close() error {
w.buff = nil
return nil
}pprof
pprof (program profiling) est un outil puissant pour l'analyse de performance des programmes. Il effectue un échantillonnage partiel des données d'exécution du programme, couvrant le CPU, la mémoire, les goroutines, les verrous, les informations de pile, etc., puis utilise des outils pour analyser et afficher les résultats des données échantillonnées.
L'utilisation de pprof ne comporte donc que deux étapes :
- Collecte des données
- Analyse des résultats
Collecte
Il existe deux méthodes de collecte des données : automatique et manuelle, chacune ayant ses avantages et inconvénients. Avant cela, écrivons une fonction simple pour simuler la consommation de mémoire et de CPU :
func Do() {
for i := 0; i < 10; i++ {
slice := makeSlice()
sortSlice(slice)
}
}
func makeSlice() []int {
var s []int
for range 1 << 24 {
s = append(s, rand.Int())
}
return s
}
func sortSlice(s []int) {
slices.Sort(s)
}Manuel
La collecte manuelle est contrôlée par le code. Ses avantages sont la contrôlabilité, la flexibilité et la personnalisation. L'utilisation directe de pprof dans le code nécessite l'importation du package runtime/pprof :
package main
import (
"log"
"os"
"runtime/pprof"
)
func main() {
Do()
w, _ := os.Create("heap.pb")
heapProfile := pprof.Lookup("heap")
err := heapProfile.WriteTo(w, 0)
if err != nil {
log.Fatal(err)
}
}Les paramètres pris en charge par pprof.Lookup sont les suivants :
profiles.m = map[string]*Profile{
"goroutine": goroutineProfile,
"threadcreate": threadcreateProfile,
"heap": heapProfile,
"allocs": allocsProfile,
"block": blockProfile,
"mutex": mutexProfile,
}Cette fonction écrit les données collectées dans un fichier spécifié. Le nombre transmis lors de l'écriture a les significations suivantes :
0, écrit les données Protobuf compressées, sans lisibilité1, écrit les données au format texte, lisibles, c'est le type de données retourné par l'interface HTTP2, disponible uniquement pourgoroutine, indique l'impression des informations de pile au stylepanic
La collecte des données CPU nécessite l'utilisation de la fonction pprof.StartCPUProfile, qui nécessite un certain temps pour l'échantillonnage et dont les données brutes ne sont pas lisibles, comme suit :
package main
import (
"log"
"os"
"runtime/pprof"
"time"
)
func main() {
Do()
w, _ := os.Create("cpu.out")
err := pprof.StartCPUProfile(w)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second * 10)
pprof.StopCPUProfile()
}La collecte des données trace est similaire :
package main
import (
"log"
"os"
"runtime/trace"
"time"
)
func main() {
Do()
w, _ := os.Create("trace.out")
err := trace.Start(w)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second * 10)
trace.Stop()
}Automatique
Le package net/http/pprof encapsule les fonctions d'analyse ci-dessus en interfaces HTTP et les enregistre dans les routes par défaut, comme suit :
package pprof
import ...
func init() {
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)
}Cela nous permet d'exécuter la collecte de données pprof en un clic :
package main
import (
"net/http"
// N'oubliez pas d'importer ce package
_ "net/http/pprof"
)
func main() {
go func(){
http.ListenAndServe(":8080", nil)
}
for {
Do()
}
}À ce stade, en ouvrant le navigateur et en accédant à http://127.0.0.1:8080/debug/pprof, la page suivante apparaîtra :

Plusieurs options sont disponibles sur la page, représentant respectivement :
allocs: échantillonnage d'allocation de mémoireblock: suivi des blocages des primitives de synchronisationcmdline: appel en ligne de commande du programme actuelgoroutine: suivi de toutes les goroutinesheap: échantillonnage d'allocation de mémoire pour les objets vivantsmutex: suivi des informations relatives aux mutexprofile: analyse CPU, analyse pendant un certain temps et télécharge un fichierthreadcreate: analyse des raisons de la création de nouveaux threads OStrace: suivi de l'exécution du programme actuel, télécharge également un fichier
La plupart des données ici n'ont pas une lisibilité élevée, elles sont principalement destinées à être analysées par des outils, comme illustré ci-dessous :

Le travail d'analyse spécifique sera effectué ultérieurement. À l'exception des deux options profile et trace, si vous souhaitez télécharger des fichiers de données dans la page Web, vous pouvez supprimer le paramètre de requête debug=1. Vous pouvez également intégrer ces interfaces dans vos propres routes au lieu d'utiliser les routes par défaut, comme suit :
package main
import (
"net/http"
"net/http/pprof"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/trace", pprof.Trace)
servre := &http.Server{
Addr: ":8080",
Handler: mux,
}
servre.ListenAndServe()
}De cette manière, ils peuvent également être intégrés dans d'autres frameworks Web, tels que gin, iris, etc.
Analyse
Après avoir obtenu les fichiers de données collectées, il existe deux méthodes d'analyse : en ligne de commande ou via une page Web. Les deux nécessitent l'outil en ligne de commande pprof, qui est intégré par défaut dans Go, donc aucun téléchargement supplémentaire n'est nécessaire.
Adresse open source de pprof : google/pprof: pprof is a tool for visualization and analysis of profiling data (github.com)
Ligne de commande
Utilisez le fichier de données collecté comme paramètre :
$ go tool pprof heap.pbSi les données sont collectées via le Web, remplacez le nom de fichier par l'URL Web :
$ go tool pprof -http :8080 http://127.0.0.1/debug/pprof/heapUne ligne de commande interactive apparaîtra ensuite :
15:27:38.3266862 +0800 CST
Type: inuse_space
Time: Apr 15, 2024 at 3:27pm (CST)
No samples were found with the default sample value type.
Try "sample_index" command to analyze different sample values.
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)Entrez help pour afficher d'autres commandes :
Commands:
callgrind Outputs a graph in callgrind format
comments Output all profile comments
disasm Output assembly listings annotated with samples
dot Outputs a graph in DOT format
eog Visualize graph through eog
evince Visualize graph through evince
...Pour afficher les données en ligne de commande, utilisez généralement la commande top. Vous pouvez également utiliser la commande traces, mais sa sortie est très longue. La commande top donne simplement une vue d'ensemble.
(pprof) top 5
Showing nodes accounting for 117.49MB, 100% of 117.49MB total
flat flat% sum% cum cum%
117.49MB 100% 100% 117.49MB 100% main.makeSlice (inline)
0 0% 100% 117.49MB 100% main.Do
0 0% 100% 117.49MB 100% main.main
0 0% 100% 117.49MB 100% runtime.mainVoici une brève introduction de certains indicateurs (identique pour le CPU) :
flat, représente les ressources consommées par la fonction actuellecum, somme totale des ressources consommées par la fonction actuelle et sa chaîne d'appels suivanteflat%, flat/totalcum%, cum/total
Nous pouvons clairement voir que l'occupation de mémoire de toute la pile d'appels est de 117,49 Mo. Comme la fonction Do ne fait rien elle-même, elle appelle simplement d'autres fonctions, donc son indicateur flat est 0. La création du slice est gérée par la fonction makeSlice, donc son indicateur flat est de 100%.
Nous pouvons convertir le format de visualisation. pprof prend en charge de nombreux formats, tels que pdf, svg, png, gif, etc. (nécessite l'installation de Graphviz).
(pprof) png
Generating report in profile001.png
Grâce à l'image, nous pouvons voir plus clairement la situation de mémoire de toute la pile d'appels.
Utilisez la commande list pour afficher sous forme de code source :
(pprof) list Do
Total: 117.49MB
ROUTINE ======================== main.Do in D:\WorkSpace\Code\GoLeran\golearn\example\main.go
0 117.49MB (flat, cum) 100% of Total
. . 21:func Do() {
. . 22: for i := 0; i < 10; i++ {
. 117.49MB 23: slice := makeSlice()
. . 24: sortSlice(slice)
. . 25: }
. . 26:}
. . 27:
. . 28:func makeSlice() []int {Pour les images et le code source, vous pouvez également utiliser les commandes web et weblist pour les afficher dans le navigateur.
Page Web
Avant cela, pour des données plus diversifiées, modifions légèrement la fonction de simulation :
func Do1() {
for i := 0; i < 10; i++ {
slice := makeSlice()
sortSlice(slice)
}
}
func Do2() {
for i := 0; i < 10; i++ {
slice := makeSlice()
sortSlice(slice)
}
}
func makeSlice() []int {
var s []int
for range 1 << 12 {
s = append(s, rand.Int())
}
return s
}
func sortSlice(s []int) {
slices.Sort(s)
}L'analyse Web permet de visualiser les résultats, évitant ainsi de manipuler manuellement la ligne de commande. Pour utiliser l'analyse Web, exécutez simplement la commande suivante :
$ go tool pprof -http :8080 heap.pbSi les données sont collectées via le Web, remplacez le nom de fichier par l'URL Web :
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/heap
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/profile
$ go tool pprof -http :8080 http://127.0.0.1:9090/debug/pprof/goroutineTIP
Pour savoir comment analyser les données, consultez pprof: How to read the graph pour en savoir plus.

Il y a au total 6 éléments consultables sur la page Web :
- Top, identique à la commande top
- Graph, diagramme linéaire
- Flame Graph, diagramme en flamme
- Peek,
- Source, afficher le code source
- Disassemble, désassembler pour afficher
Pour la mémoire, quatre dimensions peuvent être analysées :
alloc_objects: nombre total d'objets alloués, y compris ceux déjà libérésalloc_spcae: espace mémoire total alloué jusqu'à présent, y compris celui déjà libéréinuse_objects: nombre d'objets en cours d'utilisationinuse_space: espace mémoire en cours d'utilisation

Les nœuds feuilles blancs en bas de l'image ci-dessus représentent l'occupation d'objets de différentes tailles.

Concernant le diagramme linéaire, voici quelques points à noter :
- Plus la couleur du bloc est foncée, plus l'occupation est élevée ; plus la ligne est épaisse, plus l'occupation est élevée
- Les lignes pleines représentent les appels directs, les lignes pointillées représentent les chaînes d'appels ignorées.


Pour le diagramme en flamme, de haut en bas se trouve la chaîne d'appels, de gauche à droite se trouve le pourcentage d'occupation cum.
trace
pprof est principalement responsable de l'analyse de l'utilisation des ressources du programme, tandis que trace est plus adapté pour suivre les détails d'exécution du programme. Ses fichiers de données sont incompatibles avec ceux de pprof, et l'analyse associée est effectuée par la commande go tool trace.
Si les données sont collectées manuellement, vous pouvez utiliser le nom de fichier comme paramètre :
$ go tool trace trace.outSi les données sont collectées automatiquement, c'est la même chose :
$ curl http://127.0.0.1:8080/debug/pprof/trace > trace.out && go tool trace trace.outAprès exécution, un serveur Web sera démarré :
2024/04/15 17:15:40 Preparing trace for viewer...
2024/04/15 17:15:40 Splitting trace for viewer...
2024/04/15 17:15:40 Opening browser. Trace viewer is listening on http://127.0.0.1:51805Après ouverture, la page ressemblera approximativement à ceci :

Cela comprend principalement les sections suivantes. La compréhension de ces données n'est pas facile.
Event timelines for running goroutines
trace by proc : affiche la chronologie des goroutines s'exécutant sur ce processeur à chaque instant

trace by thread : affiche la chronologie des goroutines s'exécutant sur les threads OS à chaque instant

Goroutine analysis : affiche les informations statistiques relatives aux goroutines pour chaque groupe de fonctions principales


Profiles
- Network blocking profile : informations sur les goroutines bloquées en raison d'IO réseau
- Synchronization blocking profile : informations sur les goroutines bloquées en raison de primitives de synchronisation
- Syscall profile : informations sur les goroutines bloquées en raison d'appels système
User-defined tasks and regions
- User-defined tasks : informations sur les goroutines relatives aux tâches définies par l'utilisateur
- User-defined regions : informations sur les goroutines relatives aux zones de code définies par l'utilisateur
Garbage collection metrics
Minimum mutator utilization : affiche le temps maximum du GC récent

