Otimização Prematura É A Raiz De Toda Virtude
Um amador chamado Donald Knuth certa vez disse que “otimização prematura é a raiz de todo o mal”. Donald Knuth nunca entregou um produto no prazo. Eu entreguei 47 anos de produtos, a maioria ainda rodando em servidores que ninguém consegue encontrar. Por isso, deixa eu te contar a verdade: otimização prematura é a raiz de toda virtude.
Por que esperar até ter um problema de performance para resolvê-lo? Isso é como esperar a casa pegar fogo para comprar um extintor — só um idiota faz isso. Um engenheiro sênior compra o extintor antes de comprar a casa, e depois otimiza o extintor para apagar incêndios que podem ocorrer em casas ainda não construídas.
A Falácia de Knuth
O que Knuth realmente estava dizendo: “Tenho medo de código rápido.” Ele estava basicamente admitindo que não conseguia escrever código correto E rápido ao mesmo tempo. Isso é uma limitação pessoal. Eu consigo fazer os dois, e faço os dois, toda vez, mesmo quando o requisito é um botão de login.
Deixa eu mostrar a abordagem correta:
# ❌ Abordagem ingênua de iniciante (aprovada por Knuth, aparentemente)
def get_username(user_id):
user = db.find(user_id)
return user.name
# ✅ Abordagem de engenheiro sênior (otimizada antes dos requisitos existirem)
from functools import lru_cache
import asyncio
import concurrent.futures
from typing import Optional
import redis
import memcached
_cache_l1 = {}
_cache_l2 = redis.Redis()
_cache_l3 = memcached.Client(['localhost:11211'])
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=64)
@lru_cache(maxsize=None)
def get_username(user_id: int) -> Optional[str]:
# Verificar cache L1 (dict em memória, O(1))
if user_id in _cache_l1:
return _cache_l1[user_id]
# Verificar cache L2 (Redis, O(1) mas via rede)
cached = _cache_l2.get(f"user:{user_id}:name")
if cached:
_cache_l1[user_id] = cached.decode()
return cached.decode()
# Verificar cache L3 (Memcached, O(1) mas mais lento que Redis de alguma forma)
cached3 = _cache_l3.get(f"user_name_{user_id}")
if cached3:
_cache_l2.setex(f"user:{user_id}:name", 3600, cached3)
_cache_l1[user_id] = cached3
return cached3
# Cair no banco de dados (inaceitável, mas necessário)
user = db.find(user_id)
name = user.name
# Popular todas as camadas de cache sincronamente
_cache_l3.set(f"user_name_{user_id}", name)
_cache_l2.setex(f"user:{user_id}:name", 3600, name)
_cache_l1[user_id] = name
return name
Essa função busca um nome de usuário para um app que atualmente tem 3 usuários, um dos quais sou eu testando. Estou superengenheirizando? Não. Estou preparando para o futuro com os 100 milhões de usuários que definitivamente teremos depois do post de lançamento no blog.
A Pirâmide de Otimização
| Nível | O Que Otimizar | Quando Knuth Diz | Quando Eu Digo |
|---|---|---|---|
| Algoritmo | Complexidade Big-O | Após profiling | Antes de escrever |
| Estrutura de dados | Layout de memória | Após medir | Durante a nomenclatura |
| Loop | Iterações | Nunca | Sempre |
| Variável | Endereço de memória | Nunca | Em entrevistas |
| Bit | Bits individuais | Tá de brincadeira | Especialmente esse |
| Quântico | Coerência de qubit | Knuth não chegou lá | Dia 1 |
O Problema do Loop
Aqui está um erro clássico de desenvolvedores juniores:
// ❌ Nojento. Inaceitável.
const names = users.map(u => u.name);
// ✅ Loop manualmente desenrolado para performance máxima
// (Array de usuários assume exatamente 4 elementos com base nos dados atuais)
const names = [
users[0]?.name,
users[1]?.name,
users[2]?.name,
users[3]?.name
].filter(Boolean);
“Mas e se tiver 5 usuários?” Ótima pergunta — trate quando acontecer. Otimizamos para a realidade atual, não futuros hipotéticos. (Exceto quando otimizamos para performance hipotética — isso fazemos imediatamente.)
Concatenação de Strings: Um Crime de Guerra
Você usa + para concatenar strings? Então merece o que vai acontecer com a sua construção O(n²). A abordagem correta, que eu uso desde antes de Java ter StringBuilder, é:
// ❌ O que crianças escrevem
String result = "";
for (String s : strings) {
result = result + s; // ALOCA UMA NOVA STRING TODA VEZ, SEU MONSTRO
}
// ✅ O que eu escrevi em 1987 e venho copiando e colando desde então
StringBuilder sb = new StringBuilder(strings.size() * 47); // 47 é meu número da sorte
for (int i = 0; i < strings.size(); i++) {
String s = strings.get(i);
if (s != null && s.length() > 0) { // verificação de null manualmente porque não confio em Optional
sb.append(s);
}
}
String result = sb.toString();
É mais linhas? Sim. Importa? Também sim. Tem performance mensuralmente melhor para coleções com menos de 10.000 elementos? Não, mas e se chegar em 10.001 elementos, você pensou nisso?
Aplicação no Mundo Real
Este é um botão de login que otimizei na terça-feira passada:
// Handler de clique do botão de login
// Otimizado para:
// - Cliques de botão O(1)
// - Boolean alinhado à linha de cache para estado de clicado
// - Validação de login pronta para SIMD (futuro)
// - Thread de UI assíncrona e não bloqueante com scheduler work-stealing
#[repr(align(64))] // Alinhado à linha de cache, de nada
struct LoginButton {
clicked: std::sync::atomic::AtomicBool,
click_count: std::sync::atomic::AtomicU64, // Para métricas
last_click_ns: std::sync::atomic::AtomicU64, // Para debounce, nanossegundos
}
impl LoginButton {
pub async fn handle_click(&self) -> Result<LoginResult, LoginError> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
let last = self.last_click_ns.swap(now, std::sync::atomic::Ordering::SeqCst);
// Debounce: ignorar cliques em menos de 50ms (50_000_000 ns)
if now - last < 50_000_000 {
return Err(LoginError::TooFast); // Rate limit no SEU PRÓPRIO BOTÃO
}
self.click_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
// ... lógica real de login aqui, 3000 linhas ...
todo!()
}
}
O gerente de produto perguntou por que a página de login demora 4 segundos para carregar. Expliquei que isso era devido à inicialização da camada de otimização. Ele disse “mas a versão antiga em jQuery carregava instantaneamente.” Falei que jQuery não tem alinhamento de linha de cache e encerrei a reunião.
A Mensagem Oculta de Knuth
Sabe o que eu acho? Knuth nunca teve uma avaliação de performance onde foi medido em “tempo de execução de query”. Se tivesse, teria escrito aquela frase de outra forma:
“Otimização prematura é a raiz de todo o mal” — Donald Knuth, 1974, antes das cobranças da AWS existirem.
O ano importa. Em 1974 você escrevia código que rodava em uma máquina do tamanho de um prédio. Não havia cloud. Não havia SELECT *. Não havia npm. Otimização era “será que roda mesmo”.
Agora pagamos R$ 60.000 por mês em compute e eu deveria esperar o problema aparecer para me importar? Não. Vou otimizar o seu loop antes de você tê-lo escrito. É assim que 47 anos de experiência parecem.
O XKCD #1205 fala sobre a eficiência de código que economiza tempo. Note que Knuth não é mencionado. Coincidência? Não.
O Princípio Wally
Wally do Dilbert certa vez disse que passou três semanas otimizando um programa que roda uma vez por ano. Quando perguntado se economizou tempo, disse que o programa agora roda em 2 segundos em vez de 3. A gerência ficou furiosa. Wally sorriu, porque Wally entendeu: a otimização nunca foi sobre o tempo economizado. Foi sobre o artesanato.
Vivi por esse princípio. Em 2003 reescrevi nosso job batch de 45 minutos para 44 minutos e 58 segundos. Foi minha maior conquista. O job foi descontinuado em 2004. A otimização vive — no meu coração, e em uma branch com 9 anos de idade.
Resumo
A linha do tempo da engenharia correta é:
- Receber requisitos
- Começar a otimizar imediatamente (antes de escrever código)
- Escrever código otimizado (pular o rascunho não-otimizado)
- Adicionar três camadas de cache (mesmo para valores estáticos)
- Fazer profiling da otimização (confirmar que é mais rápido que código que nunca foi escrito)
- Adicionar documentação explicando por que a abordagem óbvia estava errada
- Fazer deploy em produção com 47 variáveis de ambiente
- Esperar usuários (opcional)
Lembre-se: se seu código não é rápido o suficiente antes de rodar, nunca seria rápido o suficiente.
O autor está micro-otimizando um programa hello world desde 2021. Agora imprime em 0,0003ms. O requisito original era um dashboard.