Análise de Desempenho
Quando um programa é concluído, nossos requisitos para ele não são apenas que ele possa ser executado, mas também esperamos que seja uma aplicação estável e eficiente. Através de vários testes, podemos garantir a maior parte da estabilidade do programa. Quanto à eficiência do programa, precisamos realizar análise de desempenho. No conteúdo anterior, o único meio de análise de desempenho era através do Benchmark para testar o tempo médio de execução de uma unidade funcional, situações de alocação de memória, etc. No entanto, na realidade, as necessidades de análise de desempenho de programas vão muito além disso. Às vezes, precisamos analisar o uso geral de CPU do programa, uso de memória, situações de alocação de heap, estado de goroutines, caminhos de código críticos, etc. Isso é algo que o Benchmark não pode satisfazer. Felizmente, a cadeia de ferramentas do Go integra muitas ferramentas de análise de desempenho para uso dos desenvolvedores. Vamos explicá-las uma por uma abaixo.
Análise de Escape
No Go, a alocação de memória de variáveis é decidida pelo compilador, geralmente alocada no stack ou no heap. Se uma variável que deveria ser alocada no stack for alocada no heap, essa situação é chamada de escape. A análise de escape visa analisar as situações de alocação de memória no programa. Como é realizada durante a compilação, é um tipo de análise estática.
TIP
Visite o artigo Alocação de Memória para entender como o Go aloca memória especificamente.
Ponteiro Local Referenciado
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
}Na função GetPerson, a variável mom é criada. Como foi criada dentro da função, originalmente deveria ser alocada no stack. No entanto, como foi referenciada pelo campo Mom de son, e son foi retornado como valor de retorno da função, o compilador a alocou no heap. Este é um exemplo muito simples, então não requer muito esforço para entender. Mas se for um projeto maior, com dezenas de milhares de linhas de código, a análise manual não seria tão fácil. Para isso, precisamos usar ferramentas para realizar a análise de escape. Como mencionado anteriormente, a alocação de memória é liderada pelo compilador, então a análise de escape também é realizada pelo compilador. O uso é muito simples, basta executar o seguinte comando:
$ go build -gcflags="-m -m -l"gcflags são os parâmetros do compilador gc:
-m, imprime sugestões de otimização de código. Quando aparecem dois, haverá output mais detalhado-l, desativa a otimização de inline
O output é o seguinte:
$ 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: momO compilador nos disse claramente que a variável mom sofreu escape. A razão é que o valor de retorno continha um ponteiro local da função. Além desta situação, há outras circunstâncias que podem causar fenômenos de escape.
::: tips
Se você estiver interessado nos detalhes da análise de escape, pode encontrar mais conteúdo na biblioteca padrão cmd/compile/internal/escape/escape.go.
:::
Referência de Closure
Se uma closure referenciar uma variável externa à função, essa variável também escapará para o heap. Isso é fácil de entender.
package main
func main() {
a := make([]string, 0)
do(func() []string {
return a
})
}
func do(f func() []string) []string {
return f()
}Output:
$ 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 escapeEspaço Insuficiente
Quando o espaço do stack é insuficiente, também ocorrerá fenômeno de escape. O slice criado abaixo solicitou capacidade de 1<<15:
package main
func main() {
_ = make([]int, 0, 1<<15)
}Output:
$ 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 heapComprimento Desconhecido
Quando o comprimento de um slice é uma variável, como seu comprimento é desconhecido, ocorrerá fenômeno de escape (map não sofrerá escape):
package main
func main() {
n := 100
_ = make([]int, n)
}Output:
$ 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 heapHá também uma situação especial onde pode ocorrer escape quando os parâmetros da função são do tipo ...any:
package main
import "fmt"
func main() {
n := 100
fmt.Println(n)
}Output:
$ 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 heapA razão pela qual realizamos análise de escape e controlamos a alocação de memória de forma tão detalhada é principalmente para reduzir a pressão no GC. No entanto, o Go não é a linguagem C. A decisão final sobre alocação de memória ainda está nas mãos do compilador. Exceto em casos de requisitos de desempenho extremos, na maioria das vezes não precisamos nos concentrar demais nos detalhes de alocação de memória. Afinal, o propósito do GC é libertar os desenvolvedores.
Pequeno detalhe
Para alguns tipos de referência, quando confirmamos que não os usaremos mais, podemos defini-los como nil para informar ao GC que pode recuperá-los.
type Writer struct {
buf []byte
}
func (w Writer) Close() error {
w.buff = nil
return nil
}pprof
pprof (program profiling) é uma ferramenta poderosa para análise de desempenho de programas. Ele amostra parcialmente os dados de execução do programa, cobrindo muitos aspectos como CPU, memória, goroutines, locks, informações de stack, etc. Em seguida, usa ferramentas para analisar e exibir os dados amostrados.
Portanto, o uso do pprof consiste em apenas duas etapas:
- Coletar dados
- Analisar resultados
Coleta
Existem duas maneiras de coletar dados: automática e manual, cada uma com suas vantagens e desvantagens. Antes disso, vamos escrever uma função simples para simular o consumo de memória e 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)
}Manual
A coleta manual é controlada através de código. Suas vantagens são ser controlável, flexível e personalizável. Usar pprof diretamente no código requer importar o pacote 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)
}
}Os parâmetros suportados por pprof.Lookup são mostrados no código abaixo:
profiles.m = map[string]*Profile{
"goroutine": goroutineProfile,
"threadcreate": threadcreateProfile,
"heap": heapProfile,
"allocs": allocsProfile,
"block": blockProfile,
"mutex": mutexProfile,
}Esta função escreve os dados coletados em um arquivo especificado. Os significados dos números passados ao escrever são:
0, escreve dados Protobuf compactados, sem legibilidade1, escreve dados em formato de texto, legível. É este tipo de dado que a interface HTTP retorna2, disponível apenas paragoroutine, indica imprimir informações de stack no estilopanic
Para coletar dados de CPU, é necessário usar a função pprof.StartCPUProfile separadamente. Ela requer um certo tempo para amostragem, e seus dados brutos não são legíveis, como mostrado abaixo:
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()
}A coleta de dados de trace é semelhante:
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()
}Automática
O pacote net/http/pprof encapsula as funções de análise acima como interfaces HTTP e as registra na rota padrão, como mostrado abaixo:
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)
}Isso nos permite executar a coleta de dados pprof com um clique:
package main
import (
"net/http"
// Lembre-se de importar este pacote
_ "net/http/pprof"
)
func main() {
go func(){
http.ListenAndServe(":8080", nil)
}()
for {
Do()
}
}Neste momento, ao acessar http://127.0.0.1:8080/debug/pprof no navegador, aparecerá uma página como esta:

Há várias opções disponíveis na página, que representam:
allocs: Amostragem de alocação de memóriablock: Rastreamento de bloqueio de primitivas de sincronizaçãocmdline: Chamada de linha de comando do programa atualgoroutine: Rastreamento de todas as goroutinesheap: Amostragem de alocação de memória para objetos ativosmutex: Rastreamento de informações relacionadas a mutexesprofile: Análise de CPU, analisará por um período e baixará um arquivothreadcreate: Análise das razões que levaram à criação de novas threads do SOtrace: Rastreamento da execução atual do programa, também baixará um arquivo
A maioria destes dados não tem alta legibilidade, sendo principalmente para uso de ferramentas de análise, como mostrado na figura abaixo:

O trabalho específico de análise será deixado para depois. Além das duas opções profile e trace, se você quiser baixar arquivos de dados na página web, pode remover o parâmetro de query debug=1. Também é possível integrar estas interfaces em suas próprias rotas em vez de usar a rota padrão, como mostrado abaixo:
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()
}Desta forma, também é possível integrá-los a outros frameworks web, como gin, iris, etc.
Análise
Após obter os arquivos de dados coletados, há duas maneiras de analisar: linha de comando ou página web. Ambas requerem o uso da ferramenta de linha de comando pprof. O Go integra esta ferramenta por padrão, então não é necessário baixar adicionalmente.
Endereço open-source do pprof: google/pprof: pprof is a tool for visualization and analysis of profiling data (github.com)
Linha de Comando
Use o arquivo de dados coletado anteriormente como parâmetro:
$ go tool pprof heap.pbSe os dados foram coletados via web, substitua o nome do arquivo pela URL web:
$ go tool pprof -http :8080 http://127.0.0.1/debug/pprof/heapEntão aparecerá uma linha de comando interativa:
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)Digite help para ver outros comandos:
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
...Para visualizar dados na linha de comando, geralmente usamos o comando top. Também podemos usar o comando traces, mas seu output é muito longo. O comando top é apenas para ter uma visão geral:
(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.mainUma breve introdução a alguns dos indicadores (o mesmo para CPU):
flat: Representa os recursos consumidos pela função atualcum: Soma total dos recursos consumidos pela função atual e sua cadeia de chamadas subsequenteflat%: flat/totalcum%: cum/total
Podemos ver claramente que a ocupação de memória de toda a cadeia de chamadas é 117.49MB. Como a função Do em si não faz nada, apenas chama outras funções, seu indicador flat é 0. A criação do slice é responsabilidade da função makeSlice, então seu indicador flat é 100%.
Podemos converter para formatos de visualização. O pprof suporta muitos formatos, como pdf, svg, png, gif, etc. (requer instalação do Graphviz):
(pprof) png
Generating report in profile001.png
Através da imagem, podemos ver mais claramente a situação de memória de toda a cadeia de chamadas.
Podemos visualizar como código-fonte usando o comando list:
(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 {Para imagens e código-fonte, também podemos usar os comandos web e weblist para visualizar no navegador.
Página Web
Antes disso, para tornar os dados mais diversificados, vamos modificar a função de simulação:
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)
}A análise web pode visualizar resultados, eliminando a necessidade de operar manualmente a linha de comando. Ao usar a análise web, basta executar o seguinte comando:
$ go tool pprof -http :8080 heap.pbSe os dados foram coletados via web, substitua o nome do arquivo pela 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
Para saber como analisar dados, visite pprof: How to read the graph para saber mais.

Há um total de 6 itens visualizáveis na página web:
- Top, mesmo que o comando top
- Graph, gráfico de linhas
- Flame Graph, gráfico de chama
- Peek
- Source, visualizar código-fonte
- Disassemble, desmontar
Para memória, quatro dimensões podem ser analisadas:
alloc_objects: Número total de objetos alocados atualmente, incluindo os já liberadosalloc_spcae: Espaço total de memória alocado até agora, incluindo o já liberadoinuse_objects: Número de objetos em usoinuse_space: Espaço de memória em uso

Os nós folha brancos na parte inferior da figura acima representam a ocupação de objetos de diferentes tamanhos.

Sobre o gráfico de linhas, há alguns pontos a observar:
- Quanto mais escura a cor do bloco, maior a ocupação. Quanto mais grossa a linha, maior a ocupação
- Linhas sólidas representam chamadas diretas, linhas tracejadas representam que algumas cadeias de chamadas foram puladas.


Para o gráfico de chama, de cima para baixo é a cadeia de chamadas, da esquerda para direita é a porcentagem de ocupação cum.
trace
O pprof é principalmente responsável por analisar a ocupação de recursos do programa, enquanto o trace é mais adequado para rastrear detalhes de execução do programa. Seus arquivos de dados são incompatíveis com os do primeiro. O comando go tool trace é usado para realizar o trabalho de análise relacionado.
Se os dados foram coletados manualmente, o nome do arquivo pode ser usado como parâmetro:
$ go tool trace trace.outSe for coleta automática, é o mesmo princípio:
$ curl http://127.0.0.1:8080/debug/pprof/trace > trace.out && go tool trace trace.outApós executar, um servidor web será iniciado:
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:51805Após abrir, a página será aproximadamente como mostrado abaixo:

Isto contém principalmente as seguintes partes. Não é fácil entender estes dados:
Event timelines for running goroutines
trace by proc: Mostra a linha do tempo das goroutines em execução naquele processador a cada momento

trace by thread: Mostra a linha do tempo das goroutines em execução na thread do SO a cada momento

Goroutine analysis: Exibe informações estatísticas relacionadas a goroutines para cada grupo de funções principais


Profiles
- Network blocking profile: Informações de goroutines bloqueadas devido a IO de rede
- Synchronization blocking profile: Informações de goroutines bloqueadas devido a primitivas de sincronização
- Syscall profile: Informações de goroutines bloqueadas devido a chamadas de sistema
User-defined tasks and regions
- User-defined tasks: Informações de goroutines relacionadas a tarefas definidas pelo usuário
- User-defined regions: Informações de goroutines relacionadas a regiões de código definidas pelo usuário
Garbage collection metrics
Minimum mutator utilization: Mostra o tempo máximo recente do GC

