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 é:

  1. Receber requisitos
  2. Começar a otimizar imediatamente (antes de escrever código)
  3. Escrever código otimizado (pular o rascunho não-otimizado)
  4. Adicionar três camadas de cache (mesmo para valores estáticos)
  5. Fazer profiling da otimização (confirmar que é mais rápido que código que nunca foi escrito)
  6. Adicionar documentação explicando por que a abordagem óbvia estava errada
  7. Fazer deploy em produção com 47 variáveis de ambiente
  8. 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.