Golang

Artigo 43 — Profiling e otimização de performance em Go Já leu

10 min de leitura

Artigo 43 — Profiling e otimização de performance em Go
Artigo 43 — Profiling e otimização de performance em Go Otimizar sem medir é adivinhar A regra mais importante de otimização de performance é também a mais

Artigo 43 — Profiling e otimização de performance em Go

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 7 — Ferramentas, CLI e Qualidade


Otimizar sem medir é adivinhar

A regra mais importante de otimização de performance é também a mais frequentemente ignorada: nunca otimize sem antes medir. Código que parece lento pode estar em um caminho raramente executado. Código que parece eficiente pode estar alocando memória em escala invisível a olho nu.

Go oferece um ecossistema completo de profiling integrado ao toolchain — sem dependências externas, sem instrumentação manual invasiva. O pprof captura CPU, memória, goroutines, contention de locks e muito mais. O trace captura a execução em linha do tempo. Juntos, eles tornam a otimização uma atividade baseada em evidências.


Benchmarks: a base da otimização

Antes de perfilar um sistema completo, isole a função suspeita em um benchmark:

package processamento

import (
    "strings"
    "testing"
)

// Função candidata a otimização
func concatenarStrings(partes []string) string {
    resultado := ""
    for _, p := range partes {
        resultado += p // alocação a cada iteração
    }
    return resultado
}

func concatenarComBuilder(partes []string) string {
    var sb strings.Builder
    sb.Grow(estimarTamanho(partes))
    for _, p := range partes {
        sb.WriteString(p)
    }
    return sb.String()
}

func estimarTamanho(partes []string) int {
    total := 0
    for _, p := range partes {
        total += len(p)
    }
    return total
}

// Benchmarks
func BenchmarkConcatenarStrings(b *testing.B) {
    partes := []string{"Go", " é", " uma", " linguagem", " eficiente"}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        concatenarStrings(partes)
    }
}

func BenchmarkConcatenarComBuilder(b *testing.B) {
    partes := []string{"Go", " é", " uma", " linguagem", " eficiente"}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        concatenarComBuilder(partes)
    }
}
# Executar benchmarks com métricas de memória
go test -bench=. -benchmem -count=5 ./processamento/

# Saída típica
# BenchmarkConcatenarStrings-8      3000000    450 ns/op    128 B/op    4 allocs/op
# BenchmarkConcatenarComBuilder-8  10000000    120 ns/op     48 B/op    1 allocs/op

pprof: profiling de CPU

Integração em servidores HTTP

package main

import (
    "net/http"
    _ "net/http/pprof" // importação em branco registra os handlers automaticamente
)

func main() {
    // Servidor principal
    go func() {
        mux := http.NewServeMux()
        mux.HandleFunc("/", handlerPrincipal)
        http.ListenAndServe(":8080", mux)
    }()

    // Servidor de profiling — nunca exponha em produção sem autenticação
    http.ListenAndServe(":6060", nil) // usa DefaultServeMux com pprof registrado
}

Coletando perfis via HTTP

# Perfil de CPU por 30 segundos
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Perfil de alocações de memória
go tool pprof http://localhost:6060/debug/pprof/heap

# Goroutines ativas
go tool pprof http://localhost:6060/debug/pprof/goroutine

# Contention de mutex
go tool pprof http://localhost:6060/debug/pprof/mutex

# Contention de channel (block profile)
go tool pprof http://localhost:6060/debug/pprof/block

Profiling em benchmarks

# Gera perfil de CPU durante benchmark
go test -bench=BenchmarkConcatenarStrings \
    -cpuprofile=cpu.prof \
    -memprofile=mem.prof \
    -benchmem \
    ./processamento/

# Analisa o perfil de CPU
go tool pprof cpu.prof

# Analisa o perfil de memória
go tool pprof mem.prof

Comandos interativos do pprof

(pprof) top           # top N funções por tempo de CPU
(pprof) top10 -cum    # top 10 por tempo acumulado (inclui subfunções)
(pprof) list NomeFuncao  # detalha linha a linha uma função específica
(pprof) web           # abre visualização gráfica no navegador (requer graphviz)
(pprof) png           # gera imagem PNG do grafo de chamadas
(pprof) svg           # gera SVG interativo

Exemplo de sessão interativa:

$ go tool pprof cpu.prof

Type: cpu
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top10
Showing nodes accounting for 2.3s, 92.00% of 2.5s total
      flat  flat%   sum%        cum   cum%
     1.20s 48.00% 48.00%      1.20s 48.00%  runtime.mallocgc
     0.50s 20.00% 68.00%      0.50s 20.00%  runtime.memmove
     0.30s 12.00% 80.00%      1.80s 72.00%  processamento.concatenarStrings
     ...

(pprof) list concatenarStrings
Total: 2.5s
ROUTINE ======================== processamento.concatenarStrings
     300ms      1.80s (flat, cum) 72.00% of Total
         .          .     10: func concatenarStrings(partes []string) string {
         .          .     11:     resultado := ""
      50ms      1.50s     12:     for _, p := range partes {
     250ms       300ms    13:         resultado += p
         .          .     14:     }
         .          .     15:     return resultado
         .          .     16: }

pprof: visualização web integrada

# Interface web completa — mais útil que o modo interativo
go tool pprof -http=:8090 cpu.prof

Isso abre um servidor web com: - Graph — grafo de chamadas com pesos visuais - Flame Graph — visualização de flamegraph interativa - Top — tabela ordenada por custo - Source — código anotado com custos por linha - Disasm — assembly anotado

O flamegraph é especialmente útil: o eixo X representa proporção de tempo, o eixo Y representa profundidade da pilha de chamadas. Barras largas no topo são onde o tempo é gasto.


Profiling de memória em profundidade

package main

import (
    "os"
    "runtime"
    "runtime/pprof"
)

func perfilarMemoria(caminho string) func() {
    f, _ := os.Create(caminho)
    return func() {
        runtime.GC() // força GC para perfil mais preciso
        pprof.WriteHeapProfile(f)
        f.Close()
    }
}

func main() {
    encerrar := perfilarMemoria("mem.prof")
    defer encerrar()

    // Código a perfilar
    processar()
}

Tipos de perfil de memória:

# Objetos alocados que ainda estão vivos (heap inuse)
go tool pprof -alloc_objects mem.prof

# Espaço total alocado desde o início (inclui objetos já coletados)
go tool pprof -alloc_space mem.prof

# Objetos em uso no momento da captura
go tool pprof -inuse_objects mem.prof

# Espaço em uso no momento da captura
go tool pprof -inuse_space mem.prof

Trace: visualização temporal da execução

O trace captura eventos com timestamps precisos — goroutines criadas, GC pausas, chamadas de sistema, preempções:

package main

import (
    "os"
    "runtime/trace"
)

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()

    trace.Start(f)
    defer trace.Stop()

    // Código a analisar
    executarTarefas()
}
# Analisar o trace
go tool trace trace.out

O trace viewer exibe: - Goroutine Analysis — tempo em cada estado (running, runnable, blocked, sleeping) - GC pausas — duração e frequência - Scheduler latency — tempo esperando para ser escalonado - Syscall duration — tempo em chamadas de sistema

Trace em benchmarks

go test -bench=BenchmarkProcessar -trace=trace.out ./...
go tool trace trace.out

Identificando e corrigindo problemas comuns

Alocações excessivas: escape analysis

// Descobre o que escapa para o heap
go build -gcflags="-m" ./...
go build -gcflags="-m -m" ./... # mais detalhado
// PROBLEMA: struct escapa para o heap desnecessariamente
func criarPonto() *Ponto {
    return &Ponto{X: 1, Y: 2} // escapa para heap
}

// MELHOR: para funções frequentemente chamadas, prefira retorno por valor
func criarPontoValor() Ponto {
    return Ponto{X: 1, Y: 2} // fica na stack
}

// PROBLEMA: interface causa boxing e escape para heap
func processar(v interface{}) {
    _ = v
}

// MELHOR: tipo concreto quando possível
func processarInt(v int) {
    _ = v
}

Slices: pré-alocação

// PROBLEMA: múltiplas realocações
func coletarIDs(usuarios []Usuario) []int {
    var ids []int
    for _, u := range usuarios {
        ids = append(ids, u.ID) // realoca várias vezes
    }
    return ids
}

// MELHOR: pré-aloca com capacidade conhecida
func coletarIDsOtimizado(usuarios []Usuario) []int {
    ids := make([]int, 0, len(usuarios))
    for _, u := range usuarios {
        ids = append(ids, u.ID) // zero realocações
    }
    return ids

}

Maps: pré-alocação

// PROBLEMA: map cresce incrementalmente
func indexar(itens []Item) map[string]Item {
    idx := make(map[string]Item) // começa vazio
    for _, item := range itens {
        idx[item.ID] = item
    }
    return idx
}

// MELHOR: hint de tamanho reduz rehashing
func indexarOtimizado(itens []Item) map[string]Item {
    idx := make(map[string]Item, len(itens))
    for _, item := range itens {
        idx[item.ID] = item
    }
    return idx
}

sync.Pool: reutilização de objetos

var poolBuffers = sync.Pool{
    New: func() any {
        return bytes.NewBuffer(make([]byte, 0, 4096))
    },
}

func processarRequisicao(dados []byte) string {
    buf := poolBuffers.Get().(*bytes.Buffer)
    buf.Reset()
    defer poolBuffers.Put(buf)

    // usa buf sem alocar novo buffer a cada requisição
    buf.Write(dados)
    buf.WriteString(" processado")
    return buf.String()
}

Benchmark comparativo: antes e depois

package cache_test

import (
    "testing"
)

// Versão 1: sem pré-alocação, com concatenação
func buscarChavesV1(m map[string]int) []string {
    var chaves []string
    for k := range m {
        chaves = append(chaves, k)
    }
    return chaves
}

// Versão 2: com pré-alocação
func buscarChavesV2(m map[string]int) []string {
    chaves := make([]string, 0, len(m))
    for k := range m {
        chaves = append(chaves, k)
    }
    return chaves
}

// Versão 3: reutilizando slice passado como argumento
func buscarChavesV3(m map[string]int, destino []string) []string {
    destino = destino[:0]
    for k := range m {
        destino = append(destino, k)
    }
    return destino
}

func setupMap(n int) map[string]int {
    m := make(map[string]int, n)
    for i := 0; i < n; i++ {
        m[fmt.Sprintf("chave_%d", i)] = i
    }
    return m
}

func BenchmarkBuscarChavesV1(b *testing.B) {
    m := setupMap(1000)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        buscarChavesV1(m)
    }
}

func BenchmarkBuscarChavesV2(b *testing.B) {
    m := setupMap(1000)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        buscarChavesV2(m)
    }
}

func BenchmarkBuscarChavesV3(b *testing.B) {
    m := setupMap(1000)
    destino := make([]string, 0, 1000)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        buscarChavesV3(m, destino)
    }
}

benchstat: comparando resultados estatisticamente

go install golang.org/x/perf/cmd/benchstat@latest

# Captura resultados antes da otimização
go test -bench=. -benchmem -count=10 ./... > antes.txt

# Faz a otimização...

# Captura resultados depois
go test -bench=. -benchmem -count=10 ./... > depois.txt

# Compara estatisticamente
benchstat antes.txt depois.txt

Saída:

goos: linux
goarch: amd64

           │   antes.txt   │          depois.txt           │
           │    sec/op     │   sec/op     vs base           │
BuscarV1-8   450ns ± 2%    120ns ± 1%   -73.33% (p=0.000)
BuscarV2-8   130ns ± 1%    120ns ± 1%    -7.69% (n.s.)

           │  antes.txt   │         depois.txt           │
           │    B/op      │    B/op      vs base          │
BuscarV1-8   8192 ± 0%     4096 ± 0%   -50.00% (p=0.000)
BuscarV2-8   4096 ± 0%     4096 ± 0%      ~ (n.s.)

O p=0.000 indica que a diferença é estatisticamente significativa. n.s. (not significant) indica que a diferença pode ser ruído.


Expondo métricas de runtime

package main

import (
    "encoding/json"
    "net/http"
    "runtime"
    "time"
)

type MetricasRuntime struct {
    Goroutines    int    `json:"goroutines"`
    HeapAllocMB   uint64 `json:"heap_alloc_mb"`
    HeapSysMB     uint64 `json:"heap_sys_mb"`
    HeapObjects   uint64 `json:"heap_objects"`
    NumGC         uint32 `json:"num_gc"`
    PausaTotalMs  uint64 `json:"pausa_total_ms"`
    UltimaGCMs    int64  `json:"ultima_gc_ms"`
}

func handlerMetricas(w http.ResponseWriter, r *http.Request) {
    var stats runtime.MemStats
    runtime.ReadMemStats(&stats)

    m := MetricasRuntime{
        Goroutines:   runtime.NumGoroutine(),
        HeapAllocMB:  stats.HeapAlloc / 1024 / 1024,
        HeapSysMB:    stats.HeapSys / 1024 / 1024,
        HeapObjects:  stats.HeapObjects,
        NumGC:        stats.NumGC,
        PausaTotalMs: stats.PauseTotalNs / 1e6,
        UltimaGCMs:   time.Unix(0, int64(stats.LastGC)).UnixMilli(),
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(m)
}

Regras práticas de performance em Go

1. Meça antes de otimizar — use benchmarks e pprof
2. Reduza alocações — é a otimização mais impactante em Go
3. Pré-aloque slices e maps quando o tamanho é conhecido
4. Use sync.Pool para objetos frequentemente alocados e descartados
5. Prefira valores a ponteiros para structs pequenas
6. Use strings.Builder para concatenação de strings em loops
7. Evite interface{}/any em hot paths — causa boxing e alocações
8. Leia o escape analysis — go build -gcflags="-m"
9. Use benchstat para comparações estatisticamente válidas
10. O GC é seu amigo — reduzir alocações beneficia automaticamente

Resumo do que foi coberto

Este artigo apresentou profiling e otimização de performance em Go de forma completa: benchmarks com métricas de memória, coleta de perfis de CPU e memória com pprof via HTTP e benchmarks, a interface web do pprof com flamegraph, trace para análise temporal, escape analysis para entender alocações de heap, padrões de otimização — pré-alocação de slices e maps, sync.Pool, preferência por valores — benchstat para comparação estatística e exposição de métricas de runtime. Com este artigo o Módulo 7 está completo.


Referências e leituras complementares

  • Documentação pprof — Referência completa do pacote net/http/pprof. https://pkg.go.dev/net/http/pprof

  • Go Blog: Profiling Go Programs — Tutorial oficial de profiling. https://go.dev/blog/pprof

  • Go Blog: Diagnostics — Visão geral de todas as ferramentas de diagnóstico. https://go.dev/doc/diagnostics

  • benchstat — Ferramenta de comparação estatística de benchmarks. https://pkg.go.dev/golang.org/x/perf/cmd/benchstat

  • Go execution tracer — Documentação do runtime/trace. https://pkg.go.dev/runtime/trace

  • High Performance Go Workshop — Dave Cheney — Workshop completo sobre performance. https://dave.cheney.net/high-performance-go-workshop/gopherchina-2019.html


Próximo artigo: Artigo 44 — Clean Architecture em Go: camadas e dependências**


you asked

Sim


claude response

Comentários

Mais em Golang

Operadores, expressões e conversão de tipo
Operadores, expressões e conversão de tipo

Se vari&aacute;veis s&atilde;o os substantivos de um programa, operadores s&a...

Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi
Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi

Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi Quando a biblioteca pa...

Estruturas de controle: if, for e switch
Estruturas de controle: if, for e switch

Todo programa &uacute;til precisa tomar decis&otilde;es e repetir opera&ccedi...