Em dezembro de 1974, na revista Computing Surveys,
Donald Knuth publicou um ensaio chamado Structured
Programming with go to Statements. O texto era uma
defesa cuidadosa de quando o uso de goto ainda
tinha lugar em código estruturado, contra a maré que
Dijkstra havia inaugurado em 1968 com a famosa carta
Go To Statement Considered Harmful. Em meio a essa
discussão técnica, Knuth escreveu uma frase que, fora de
contexto, virou folclore: "premature optimization is
the root of all evil". Em quase todo lugar onde a
frase aparece, ela aparece truncada — sem o restante do
parágrafo, e portanto sem o argumento.
O parágrafo completo dizia: "Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%". Knuth não está dizendo que otimização é má. Está dizendo que otimização sem medição é má, porque o tempo humano gasto raciocinando sobre o que parece lento é tipicamente desperdiçado em código que não é o gargalo, e o ônus de manutenção do código "otimizado" persiste para sempre. A frase canônica é a defesa do empirismo, não da preguiça.
Cinquenta e dois anos depois, a frase continua vital — e
continua mal compreendida. A tendência humana de raciocinar
sobre o que é lento é tão forte que mesmo engenheiros
experientes a aplicam sem perceber. "Vou usar
StringBuilder em vez de
+= aqui porque é mais rápido." "Vou trocar o
ORM por SQL cru porque ORM é lento." "Vou cachear esse
cálculo porque parece pesado." Cada uma dessas decisões
pode ser correta. A maioria, sem medição, não é. O ônus de
manutenção é certo; o ganho é especulativo.
Este conceito articula performance como disciplina profissional, não como instinto. Há um workflow canônico, há ferramentas que funcionam, e há um conjunto pequeno mas consequente de exceções onde a regra de "medir primeiro" cede espaço para "decidir antes". Os onze conceitos seguintes do módulo destrincham as ferramentas e a camada física; aqui o foco é a postura — o jeito de pensar — que torna o resto utilizável.
O workflow canônico — medir, encontrar, mexer, medir
A disciplina de performance, depois de Knuth, foi progressivamente formalizada por engenheiros como Brendan Gregg (Netflix, depois Intel), Martin Thompson (LMAX, Disruptor pattern, "Mechanical Sympathy"), e Andrey Akinshin (autor de Pro .NET Benchmarking). O workflow que emergiu cabe em quatro passos repetidos.
Primeiro: medir o estado atual. Antes de pensar em otimizar qualquer coisa, você precisa saber qual é o número que importa — latência P99 do endpoint, throughput do batch, alocações por request, RSS de memória — e qual é o valor atual. Sem essa baseline, você não tem como saber depois se a mudança ajudou, prejudicou, ou foi neutra.
Segundo: encontrar o gargalo. Profiler em mãos, identifique onde o tempo (ou memória, ou alocações) está sendo gasto. Brendan Gregg chama isso de "encontrar a árvore na floresta": flame graph mostra o que domina o tempo, e quase sempre não é onde a intuição apostou. O conceito 05 deste módulo cobre profiling em profundidade.
Terceiro: mexer no gargalo. Apenas no gargalo, e com mudança mínima isolada. Se duas mudanças são feitas ao mesmo tempo, qualquer melhora não pode ser atribuída a uma delas. Mude uma coisa, meça, decida se ficou; mude a outra, meça, decida.
Quarto: medir o estado pós-mudança e comparar com a baseline. A pergunta não é "ficou mais rápido?", é "ficou mais rápido por uma margem estatisticamente significativa, sob carga representativa, sem regredir outras métricas?". Esse rigor é o que separa otimização real de "sensação de mais rápido".
O ciclo é simples e, na prática, muito difícil de sustentar — porque exige paciência. Equipes pressionadas pulam para o passo três (mexer) sem o um (medir), e passam meses fazendo "otimizações" que ninguém consegue provar que ajudaram. Esse padrão é tão recorrente que tem nome no folclore: shotgun debugging, ou cargo cult performance.
Por que a intuição erra — e quando ela acerta
A intuição humana sobre performance é notoriamente ruim, e há razões mensuráveis. Hardware moderno tem várias camadas de cache (L1/L2/L3, mais o page cache do SO), pipelines de execução superscalar, branch predictors, e compiladores que reorganizam código de forma agressiva. O "raciocínio mecânico" — quanto custa cada instrução — diverge tanto da execução real que estimativas a olho nu erram por uma a duas ordens de magnitude rotineiramente.
A intuição também é míope. O programador olha o código que
escreveu e acha "deve ser rápido"; raramente olha o
código chamado pelo seu, que pode ser onde o tempo
real está. Allocação de objeto pequeno em loop quente parece
barata; o GC que ela alimenta não é. String.format
parece neutro; o boxing de argumentos primitivos não é.
Logging em Debug parece grátis; o formatador
que executa antes do filtro de nível, não.
Há, no entanto, três classes de decisões onde a intuição acerta com frequência alta o bastante para ser usada como guia, mesmo sem medição completa. Conhecer essas exceções é parte da disciplina — porque às vezes não há tempo de medir.
Decisões arquiteturais caras de reverter. Escolha de modelo de execução (sync vs async vs goroutine), layout de dados crítico (struct of arrays vs array of structs, conceito 07), protocolo de rede (REST vs gRPC vs binário custom), modelo de banco (relacional vs documento vs key-value). Reverter qualquer uma dessas dói muito; vale aplicar conhecimento prévio (medições de outros sistemas, ordem de magnitude da pilha de latência — conceito 02) para escolher informadamente desde o início.
Padrões reconhecidos como armadilhas estruturais. N+1 query (módulo 03 e conceito 11 deste), retry storm (módulo 05 conceito 9), cache stampede (módulo 05 conceito 10), false sharing (conceito 07), alocação em loop quente, conexão sem pool. Esses padrões têm sintomatologia conhecida e quase sempre custam caro em produção. Evitá-los desde o design não é "otimização prematura" — é higiene.
Constraints físicas duras. Velocidade da luz (conceito 02 mostra os números), tamanho do payload de rede vs MTU, tempo de seek de disco rotativo (em sistemas que ainda têm). Decisões que ignoram essas constantes físicas falham por design — nenhuma otimização compensa uma chamada cross-region em loop síncrono.
Os números de magnitude — calibrando intuição
A maior falha da intuição é não conhecer ordens de magnitude. Quando engenheiros sêniores conversam sobre otimização, eles operam num modelo mental em que cada operação tem um custo aproximado em ciclos ou nanosegundos, e onde transições entre camadas (cache, RAM, disco, rede) cobram preços bem conhecidos. O conceito 02 sistematiza esses números (a tabela canônica de Jeff Dean), mas vale antecipar a régua mental aqui:
Operação em CPU em registrador: ~1 nanosegundo (10⁻⁹s). Acesso a L1 cache: ~1ns. Acesso a L3 cache: ~10ns. Acesso a RAM: ~100ns. Leitura de SSD NVMe: ~100µs (10⁵ns). Leitura de SSD SATA: ~250µs. Round-trip TCP local: ~500µs (em Linux, ~50µs com tuning agressivo). Round-trip dentro do datacenter: ~500µs–1ms. Round-trip cross-region (mesmo continente): ~50ms. Round-trip cross-continent: ~150–300ms.
A consequência prática: uma operação que cabe em RAM e em poucos ciclos roda em microssegundos; uma que envolve disco, em centenas de microssegundos; uma que envolve rede local, em milissegundos; uma que envolve cross-region, em dezenas de milissegundos. Trocar uma operação por outra sem entender essas magnitudes é onde decisões erram em ordens de grandeza.
Métricas que importam — e métricas que enganam
Performance é multidimensional, e medir só uma dimensão é armadilha. Há quatro métricas canônicas que toda equipe sênior monitora simultaneamente.
Latência — quanto tempo leva uma operação individual. Sempre como distribuição (P50, P95, P99, P99.9), nunca como média. O conceito 03 do módulo detalha por quê.
Throughput — quantas operações por segundo. Tipicamente reportado a um nível de latência fixo ("X RPS sustentável a P99 < 100ms"); throughput sem latência associada não significa nada útil.
Utilização de recurso — CPU, memória, I/O, rede, conexões de banco. Brendan Gregg formulou o USE method em 2012: para cada recurso, monitorar Utilization, Saturation, Errors. Recurso saturado é o gargalo; recurso pouco utilizado mas com latência alta indica problema em outro lugar.
Custo — performance medida em dólares. Em cloud, infraestrutura é elástica mas cobrada por hora, por giga, por requisição. Otimização que dobra a latência mas reduz o custo em 80% pode ser o trade-off certo. Articular custo como métrica de performance é maturidade; ignorar é como engenheiros que entregam sistemas que escalam financeiramente para o zero.
Métricas que enganam, em contraste, são sintomas: "CPU em 30%" não significa que o sistema está saudável (pode estar gargalado em I/O); "memória em 70%" não significa que tem 30% de folga (pode haver vazamento crescendo a cada hora); "latência média 50ms" não significa que o usuário está feliz (P99 pode estar em 5s). Sêniores olham distribuições e correlações, não só pontos isolados.
Profiling-driven em três linguagens
A disciplina materializa-se em código no profiling. A forma idiomática varia, mas o resultado é o mesmo: você roda a aplicação sob carga representativa, captura um perfil, lê o flame graph, identifica o hot path, mexe, e compara.
# captura perfil em produção (5 segundos)
dotnet-trace collect --process-id $PID --duration 00:00:05 \
--providers Microsoft-DotNETCore-SampleProfiler
# converte para flame graph (via PerfView ou speedscope)
dotnet-trace convert trace.nettrace --format speedscope
# counters em tempo real (utilização)
dotnet-counters monitor --process-id $PID \
System.Runtime Microsoft.AspNetCore.Hosting
# benchmark micro com BenchmarkDotNet
[MemoryDiagnoser]
public class StringConcatBench
{
private readonly string[] parts = Enumerable.Range(0, 100)
.Select(i => $"part_{i}").ToArray();
[Benchmark(Baseline = true)]
public string PlusEquals()
{
var s = "";
foreach (var p in parts) s += p;
return s;
}
[Benchmark]
public string StringBuilder()
{
var sb = new StringBuilder();
foreach (var p in parts) sb.Append(p);
return sb.ToString();
}
}
dotnet-trace + speedscope
rendem flame graph em segundos. BenchmarkDotNet
aplica statistical analysis, warmup, e
[MemoryDiagnoser] mede alocações além de
tempo. Pyroscope expõe profiling contínuo em
produção.
# py-spy: profiler sampling sem precisar mexer no código
py-spy record -o profile.svg --pid $PID --duration 30
# top ao vivo
py-spy top --pid $PID
# cProfile dentro do código
import cProfile, pstats
profiler = cProfile.Profile()
profiler.enable()
do_work()
profiler.disable()
pstats.Stats(profiler).sort_stats('cumulative').print_stats(20)
# pytest-benchmark
def test_concat_plus(benchmark):
parts = [f"part_{i}" for i in range(100)]
def run():
s = ""
for p in parts: s += p
return s
benchmark(run)
def test_concat_join(benchmark):
parts = [f"part_{i}" for i in range(100)]
benchmark(lambda: "".join(parts))
py-spy (Ben Frederickson, 2018) é o
profiler de produção em Python — sampling, baixíssimo
overhead, sem patch no código. scalene
(Emery Berger, 2020) vai além e mede alocações por
linha.
package main
import (
_ "net/http/pprof" // expõe /debug/pprof/* em http
"net/http"
)
func main() {
go http.ListenAndServe(":6060", nil)
// ... resto da aplicação
}
// captura perfil de CPU (30s)
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30
// captura heap
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap
// benchmark (gera CPU + mem profile)
func BenchmarkConcat(b *testing.B) {
parts := make([]string, 100)
for i := range parts { parts[i] = fmt.Sprintf("part_%d", i) }
b.Run("plus", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s := ""
for _, p := range parts { s += p }
_ = s
}
})
b.Run("builder", func(b *testing.B) {
b.ReportAllocs()
var sb strings.Builder
for i := 0; i < b.N; i++ {
sb.Reset()
for _, p := range parts { sb.WriteString(p) }
_ = sb.String()
}
})
}
Go é a linguagem mainstream com a melhor experiência
de profiling fora da caixa. pprof embutido
no net/http entrega CPU, heap, mutex,
block, goroutine profiles via HTTP.
go test -bench com -benchmem
mede tempo e alocações.
O ônus do código "otimizado"
Toda otimização tem custo de manutenção. Código mais rápido é, em quase todos os casos, código menos legível — tira clareza, adiciona indireção, força conhecimento de detalhes de runtime. Esse custo persiste pela vida do sistema; o ganho de performance, se não foi mensurado, pode ser ilusório.
A pergunta de senior antes de qualquer otimização é: "esse ganho compensa o ônus pelos próximos cinco anos?". Se a resposta envolve "depende de quanto a gente economiza em CPU vs quanto a equipe perde em legibilidade", a resposta correta exige número, não opinião. Sistemas que recebem otimização sem medição acumulam complexidade que não retorna; sistemas que recebem otimização medida acumulam complexidade que paga.
Otimização que ajuda o sistema sob teste e prejudica em
produção. Cenário recorrente: o engenheiro identifica
"alocação em hot path" via benchmark sintético, troca
por ArrayPool ou sync.Pool, e
commita. Em benchmark, melhora 30%. Em produção, o pool
cresce para refletir o pico, e em horário de baixa
retém memória que não voltaria com alocação normal —
uso de RSS sobe 3x. O bug fica invisível por meses até a
infraestrutura cobrar o custo. Defesa: profile sob carga
real, não sob microbenchmark; soak test (12+ horas) que
revela vazamento e crescimento; monitoring de RSS além
de heap.
Os três tipos de otimização que valem o esforço
Depois de medir, três classes de otimização entregam retorno consistente — e vale conhecer cada uma para reconhecer quando aplicá-la.
Eliminação. Remover trabalho que não precisa acontecer. Cache (não recalcular o que já foi calculado), lazy loading (não computar o que não é consumido), batching (não fazer N chamadas quando uma cabe), short-circuit (não percorrer estrutura inteira quando primeira resposta basta). Eliminação é a otimização de maior retorno; sempre vale pensar primeiro.
Realocação. Mover trabalho para onde é mais barato. Mover serialização para borda HTTP (CDN ao invés de origin), pré-computar em background o que vai ser pedido em tempo crítico (materialized views), mover validação para borda em vez de domínio (não chega ao banco). Realocação não reduz o trabalho total — apenas o caminho que ele atravessa.
Substituição algorítmica ou estrutural. Trocar O(n²) por O(n log n), trocar lista por hash quando lookup é dominante, trocar JSON por binário quando volume de payload domina. É a otimização mais perigosa porque muda contratos; vale para hot paths bem identificados, em situações onde a magnitude do ganho compensa a complicação.
Otimizações fora dessas três categorias —
"trocar += por StringBuilder em
caminho frio", "prealocar lista de tamanho 10" — são as
que Knuth catalogava como prematuras. Não somam, e o ônus
persiste.
Antes de mexer em código por motivo de performance, responda quatro perguntas. "Qual a métrica concreta que eu quero melhorar — P99 do endpoint X? Throughput do job Y?". Se vaga, está cedo demais. "Qual o valor atual e qual o valor-alvo?". Se você não sabe, está cedo demais. "Onde o profiler diz que o tempo está sendo gasto?". Se você não rodou o profiler, está cedo demais. "Qual classe de otimização eu vou aplicar — eliminar trabalho, realocar trabalho, ou substituir algoritmo?". Se a resposta é "deixar mais rápido", está cedo demais. As quatro perguntas filtram 90% do que parecia urgente.
Por que importa para a sua carreira
A disciplina de performance é uma das que mais separa sêniores juniores em entrevistas e em entregas. Em entrevista de design, "como você melhoraria a performance desse sistema?" é convite direto para mostrar postura — a resposta forte cita o workflow (medir antes de mexer), pergunta sobre a métrica relevante (latência ou throughput? P99 ou P50?), articula em qual camada provavelmente está o gargalo (banco? rede? CPU?), e menciona ferramentas de profiling antes de propor mudança. Em revisão de código, ver PR com "otimização" não-mensurada e devolver "qual benchmark mostra essa melhora?" é serviço ao time. Em pos-mortem, articular "não medimos antes, então não sabemos se mexer aqui ajuda" é honestidade que evita iteração de seis meses por gargalo errado. Performance é tema onde o vocabulário maduro vale mais que o talento bruto — quem aprendeu a cabeça leva o time inteiro junto.
Como praticar
- Workflow profiling-driven em projeto seu. Pegue um endpoint de algum projeto que você considere "lento" (ou rápido, na verdade — o exercício funciona em qualquer caso). Antes de tocar no código, capture baseline de latência (P50/P95/P99 sob 100 req/s por 2 minutos). Capture flame graph com a ferramenta da linguagem (dotnet-trace, py-spy, pprof). Identifique a função que domina. Sem mudar nada, escreva uma hipótese de otimização. Mude. Capture flame graph e latência de novo. Compare. Se ficou pior ou neutro, reverta. Se ficou melhor, articule por quê. Esse exercício, feito três vezes em projetos diferentes, calibra intuição permanentemente.
-
Benchmark sério com BenchmarkDotNet ou
equivalente. Implemente o benchmark do
lang-compare em sua linguagem principal, executando
comparação entre
+=,StringBuilder(ou equivalente), eString.Joinpara 100, 1000, e 10000 strings. Plote os resultados. Agora repita com strings de tamanho variado (10 chars, 100 chars, 1000 chars). Note onde o cruzamento ocorre. Esse gráfico — o que custa o quê em qual escala — é conhecimento que sustenta decisão de design por anos. - Audit de "otimizações" antigas. Em um projeto que você conhece, identifique trechos de código com aspecto de "alguém otimizou aqui": pré-alocação explícita, troca por structs em vez de classes, loop manual em vez de LINQ/list comprehension. Para cada um, pergunte: "tem benchmark que comprova o ganho?". Se não tem, considere reverter para a forma idiomática e rodar benchmark depois — frequentemente o ganho era ilusório, e a reversão melhora legibilidade sem custo.
Referências para aprofundar
- paper Structured Programming with go to Statements — Donald Knuth (Computing Surveys, 1974).
- livro Systems Performance: Enterprise and the Cloud (2ª ed.) — Brendan Gregg (Pearson, 2020).
- livro Pro .NET Benchmarking — Andrey Akinshin (Apress, 2019).
- livro The Art of Computer Programming, vol. 1 — Donald Knuth (Addison-Wesley, 4ª ed. 2011).
- livro Site Reliability Engineering — Betsy Beyer et al., Google (O'Reilly, 2016).
- livro Database Internals — Alex Petrov (O'Reilly, 2019).
- artigo Mature Optimization Handbook — Carlos Bueno (Facebook, 2013).
- artigo The USE Method — Brendan Gregg (brendangregg.com, 2012).
- artigo How NOT to Measure Latency — Gil Tene (Strange Loop, 2015).
- artigo Mechanical Sympathy — Martin Thompson (blog series, 2011+).
- docs Brendan Gregg — Performance Tools.
- vídeo Computer Latency at a Human Scale — Carlos Bueno (Velocity, 2014).