DevGuideTK

Profiling

Como medir onde o tempo e a memória são gastos antes de otimizar.

O código desta página foi executado nas cinco linguagens, num núcleo de CPU reservado, e cada versão lenta foi medida com o profiler da sua linguagem.

O relatório, lento e corrigido, em cada linguagem

Cada versão gera 100 mil pedidos de 20 mil clientes e monta uma linha por cliente, com o total. A diferença entre as duas versões está numa função: a lenta confere se o cliente já está numa lista percorrendo a lista; a corrigida usa um conjunto. Com a variável SO=lento, o programa roda só a versão lenta, para o profiler.

type Pedido = { clienteId: number; total: number };

function gerarPedidos(n: number): Pedido[] {
  return Array.from({ length: n }, (_, i) => ({ clienteId: (i * 7919) % 20_000, total: i % 500 }));
}

function formatarLinha(clienteId: number, total: number): string {
  return `cliente ${clienteId}: R$ ${total.toFixed(2)}`;
}

// Versão lenta: includes percorre a lista inteira a cada pedido (O(n²))
function clientesUnicosLento(pedidos: Pedido[]): number[] {
  const clientes: number[] = [];
  for (const p of pedidos) if (!clientes.includes(p.clienteId)) clientes.push(p.clienteId);
  return clientes;
}

// Versão corrigida: o Set confere a presença em tempo constante
function clientesUnicos(pedidos: Pedido[]): number[] {
  return [...new Set(pedidos.map((p) => p.clienteId))];
}

function relatorio(pedidos: Pedido[], unicos: (p: Pedido[]) => number[]): string[] {
  const totais = new Map<number, number>();
  for (const p of pedidos) totais.set(p.clienteId, (totais.get(p.clienteId) ?? 0) + p.total);
  return unicos(pedidos).map((c) => formatarLinha(c, totais.get(c)!));
}

const pedidos = gerarPedidos(100_000);
const versoes = [["lento", clientesUnicosLento], ["corrigido", clientesUnicos]] as const;
for (const [nome, f] of versoes) {
  if (process.env.SO === "lento" && nome !== "lento") continue;
  const inicio = performance.now();
  const linhas = relatorio(pedidos, f);
  const ms = (performance.now() - inicio).toFixed(0);
  console.log(`${nome}: ${linhas.length} clientes em ${ms} ms`);
}

O resultado, nas cinco linguagens

Versão lentaVersão corrigidaQuantas vezes mais rápida
TypeScript894 ms28 ms32
Python11.374 ms30 ms379
Go286 ms12 ms24
Java1.011 ms177 ms6
C#172 ms10 ms17

As cinco versões encontraram os mesmos 20 mil clientes. A versão corrigida em Java ficou mais lenta que as outras. Os candidatos são a compilação inicial da JVM, que ainda não otimizou o código numa execução tão curta, e o String.format das 20 mil linhas; o teste não mediu qual dos dois pesa, e é exatamente o que um novo perfil diria.

O que muda de uma linguagem para outra

Busca linear (lenta)Conjunto (corrigida)
TypeScriptArray.includesSet
Pythonin numa listdict.fromkeys, que guarda a ordem
Goslices.Containsmap[int]bool
JavaArrayList.containsLinkedHashSet, que guarda a ordem
C#List.ContainsHashSet com Where(vistos.Add)

A busca linear é rápida o bastante em todas as linguagens com poucos itens. O problema é o número de itens multiplicado pelo número de buscas: 100 mil pedidos procurando numa lista que chega a 20 mil.

Os comandos de profiling

Os comandos usados no teste, um por linguagem.

Node: grava um arquivo .cpuprofile, que abre no painel de desempenho do Chrome ou no Speedscope:

SO=lento node --cpu-prof relatorio.ts

Python: o cProfile, ordenado pelo tempo próprio de cada função:

SO=lento python3 -m cProfile -s tottime relatorio.py

Go: o programa grava o perfil quando recebe PERFIL (o trecho com runtime/pprof no exemplo), e o pprof lista as funções:

SO=lento PERFIL=cpu.prof ./rel
go tool pprof -top -nodecount=5 rel cpu.prof
go tool pprof -http=:8080 rel cpu.prof   # gráfico de chama no navegador

Java: uma gravação do JDK Flight Recorder e as amostras de execução:

SO=lento java -XX:StartFlightRecording=filename=rec.jfr,settings=profile Relatorio.java
jfr print --events jdk.ExecutionSample rec.jfr

C#: o dotnet-trace (instalado com dotnet tool install -g dotnet-trace), que já amostra a CPU no perfil padrão:

SO=lento dotnet-trace collect -o rel.nettrace -- ./app
dotnet-trace report rel.nettrace topN -n 6

No teste, a primeira tentativa no C# usou --profile cpu-sampling, o nome de versões anteriores, e a versão 10 do dotnet-trace recusou: The specified profile 'cpu-sampling' does not apply. Sem --profile, o padrão incluiu a amostragem de CPU.

E a memória no Python, com o tracemalloc:

import tracemalloc

tracemalloc.start()
# ... o código medido ...
for estatistica in tracemalloc.take_snapshot().statistics("lineno")[:3]:
    print(estatistica)

Lista de revisão

Antes de otimizar

  • Existe uma medida do problema, com dados e carga realistas?
  • O sintoma é CPU alta, espera, memória crescendo ou pausas do coletor? O profiler escolhido mede esse sintoma?

Lendo o perfil

  • A análise olha o tempo acumulado, e não só o próprio?
  • O caminho até o código da aplicação foi seguido na pilha?
  • A conclusão leva em conta funções embutidas pelo compilador?

Depois de corrigir

  • O ganho foi confirmado medindo de novo, sem o profiler?
  • Há um benchmark ou teste de desempenho que impede o gargalo de voltar?

Em produção

  • O time sabe como coletar um perfil de uma instância em produção?
  • Há profiling contínuo, ou ao menos um caminho rápido para ativá-lo?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Fazer profiling na minha linguagem

    Quero aprender a usar o profiler da minha linguagem na prática.
    
    Pergunte qual linguagem eu uso.
    
    Vamos em etapas:
    1. Um programa de exemplo com um gargalo escondido.
    2. Coletar um perfil de CPU, com os comandos exatos.
    3. Ler o resultado: tempo próprio, acumulado e gráfico de chama.
    4. Corrigir e medir de novo, sem o profiler.
    5. Coletar um perfil de memória e encontrar a maior alocação.
  • Proteger o desempenho com benchmarks

    Corrigi um gargalo e quero garantir que ele não volte.
    
    Pergunte qual linguagem eu uso e qual foi o gargalo.
    
    Me ajude a escrever um benchmark com a ferramenta padrão da linguagem (testing.B, pytest-benchmark, JMH, BenchmarkDotNet, tinybench ou equivalente), com dados realistas, e a rodá-lo na integração contínua com um limite que falhe se o desempenho piorar demais.
  • Revisar código em busca de gargalos

    Vou colar abaixo um trecho de código que está lento, e se possível a saída de um profiler. Revise.
    
    Verifique, e para cada item diga se encontrou o problema, citando o trecho:
    1. Buscas lineares dentro de laços.
    2. Consultas ao banco ou chamadas de rede dentro de laços.
    3. Serialização, cópia ou ordenação repetidas.
    4. Expressões regulares ou objetos caros criados a cada chamada.
    5. Alocações desnecessárias em caminhos muito usados.
    
    Para cada problema, estime o impacto, mostre a correção e diga como confirmar com um profiler.
    
    Código e perfil:
    [cole aqui]