Guia prático da mentalidade orientada ao desempenho associada a John Carmack: profiling, orçamentos de tempo por frame, trade-offs e como entregar sistemas complexos em tempo real.

John Carmack costuma ser tratado como uma lenda dos motores de jogo, mas a parte útil não é a mitologia — são os hábitos repetíveis. Não se trata de copiar o estilo de uma pessoa ou presumir “movimentos de gênio”. Trata-se de princípios práticos que levam, de forma confiável, a software mais rápido e suave, especialmente quando prazos e complexidade se acumulam.
Engenharia de desempenho significa fazer o software atingir uma meta de velocidade em hardware real, sob condições reais — sem quebrar a corretude. Não é “deixar rápido a qualquer custo”. É um loop disciplinado:
Essa mentalidade aparece no trabalho do Carmack repetidas vezes: discuta com dados, mantenha mudanças explicáveis e prefira abordagens que você consiga manter.
Gráficos em tempo real são implacáveis porque há um prazo a cada frame. Se você o perde, o usuário sente imediatamente como stutter, latência de entrada ou movimento irregular. Outros softwares podem esconder ineficiências atrás de filas, telas de carregamento ou trabalho em background. Um renderer não negocia: ou você termina a tempo, ou não.
É por isso que as lições se generalizam além dos jogos. Qualquer sistema com requisitos rígidos de latência — UI, áudio, AR/VR, trading, robótica — se beneficia de pensar em orçamentos, entender gargalos e evitar picos-surpresa.
Você receberá checklists, heurísticas e padrões de decisão aplicáveis ao seu trabalho: como definir orçamentos de tempo por frame (ou latência), como perfilar antes de otimizar, como escolher a “uma coisa” para corrigir e como prevenir regressões para que desempenho vire rotina — não um pânico de última hora.
O pensamento de desempenho ao estilo Carmack começa com uma troca simples: pare de falar de “FPS” como unidade primária e comece a falar sobre tempo por frame.
FPS é um recíproco (“60 FPS” soa bem, “55 FPS” soa próximo), mas a experiência do usuário é guiada por quanto tempo cada frame demora — e, igualmente importante, quão consistentes são esses tempos. Um salto de 16.6 ms para 33.3 ms é visível instantaneamente, mesmo que sua média de FPS ainda pareça respeitável.
Um produto em tempo real tem múltiplos orçamentos, não apenas “renderizar mais rápido”:
Esses orçamentos interagem. Economizar tempo de GPU adicionando batchs pesados na CPU pode sair pela culatra, e reduzir memória pode aumentar custos de streaming ou descompressão.
Se sua meta é 60 FPS, seu orçamento total é 16.6 ms por frame. Uma divisão aproximada pode ser:
Se CPU ou GPU ultrapassarem o orçamento, você perde o frame. Por isso equipes falam em ser “CPU-bound” ou “GPU-bound” — não como rótulos, mas como forma de decidir de onde realisticamente virá o próximo milissegundo.
O ponto não é perseguir uma métrica de vaidade como “maior FPS num PC topo de linha”. O ponto é definir o que é rápido o suficiente para seu público — alvos de hardware, resolução, limites de bateria, térmicas e responsividade de entrada — e então tratar o desempenho como orçamentos explícitos que você pode gerenciar e defender.
O movimento padrão do Carmack não é “otimizar”, é “verificar”. Problemas de desempenho em tempo real estão cheios de histórias plausíveis — pausas de GC, “shaders lentos”, “muitos draw calls” — e a maioria delas está errada na sua build no seu hardware. Perfilar é como substituir intuição por evidência.
Trate o profiling como uma feature de primeira classe, não como ferramenta de resgate de última hora. Capture tempos por frame, timelines de CPU e GPU, e as contagens que os explicam (triângulos, draw calls, mudanças de estado, alocações, faltas de cache se puder). O objetivo é responder uma pergunta: onde o tempo está realmente indo?
Um modelo útil: em todo frame lento, uma coisa é o fator limitante. Talvez seja a GPU presa em um passe pesado, a CPU presa na atualização de animação, ou a thread principal travada em sincronização. Encontre essa restrição primeiro; o resto é ruído.
Um loop disciplinado evita que você fique girando em falso:
Se a melhoria não for clara, presuma que não ajudou — porque provavelmente não vai sobreviver ao próximo conteúdo.
Trabalho de desempenho é especialmente vulnerável à autoenganação:
Perfilar primeiro mantém seu esforço focado, suas trocas justificadas e suas mudanças mais fáceis de defender em revisão.
Problemas de desempenho em tempo real parecem bagunçados porque tudo acontece ao mesmo tempo: gameplay, render, streaming, animação, UI, física. O instinto do Carmack é cortar o ruído e identificar o limitador dominante — a única coisa que está definindo seu tempo por frame no momento.
A maioria das lentidões cai em alguns grupos:
O objetivo não é rotular para um relatório — é puxar a alavanca certa.
Alguns experimentos rápidos podem dizer o que está realmente no controle:
Raramente você vence aparando 1% em dez sistemas. Encontre o maior custo que se repete a cada frame e ataque isso primeiro. Remover um único ofensor de 4 ms vale mais que semanas de micro-otimizações.
Depois de consertar a pedra grande, a próxima pedra grande fica visível. Isso é normal. Trate o trabalho de desempenho como um loop: medir → mudar → re-medir → re-priorizar. O objetivo não é um perfil perfeito; é progresso constante rumo a tempo por frame previsível.
A média do tempo por frame pode parecer boa enquanto a experiência ainda é ruim. Gráficos em tempo real são julgados pelos piores momentos: o frame perdido durante uma grande explosão, o travamento ao entrar em uma nova sala, o stutter súbito quando um menu abre. Isso é latência da cauda — frames lentos, raros, mas frequentes o suficiente para os usuários notarem.
Um jogo rodando a 16.6 ms a maior parte do tempo (60 FPS) mas pulando para 60–120 ms a cada poucos segundos parecerá “quebrado”, mesmo que a média ainda seja 20 ms. Humanos são sensíveis ao ritmo. Um único frame longo quebra a previsibilidade da entrada, o movimento da câmera e o sync áudio/visual.
Picos frequentemente vêm de trabalho que não é espalhado uniformemente:
O objetivo é tornar o trabalho caro previsível:
Não apenas trace uma linha de FPS média. Registre tempos por frame e visualize:
Se você não consegue explicar seus piores 1% de frames, você não explicou realmente o desempenho.
O trabalho de desempenho fica mais fácil no momento em que você para de fingir que pode ter tudo ao mesmo tempo. O estilo do Carmack empurra equipes a nomear a troca em voz alta: o que estamos comprando, o que estamos pagando e quem sente a diferença?
A maioria das decisões se situa em alguns eixos:
Se uma mudança melhora um eixo mas taxa silenciosamente três outros, documente isso. “Isso adiciona 0.4 ms de GPU e 80 MB de VRAM para ganhar sombras mais suaves” é uma declaração utilizável. “Fica melhor” não é.
Gráficos em tempo real não tratam de perfeição; tratam de atingir uma meta com consistência. Concorde com limites como:
Uma vez que a equipe concorda que, digamos, 16.6 ms a 1080p na GPU base é a meta, as discussões ficam concretas: essa feature nos mantém dentro do orçamento ou força um downgrade em outro lugar?
Quando estiver incerto, escolha opções que você possa desfazer:
A reversibilidade protege o cronograma. Você pode lançar o caminho seguro e deixar o ambicioso atrás de um toggle.
Evite overengineering por ganhos invisíveis. Uma melhoria de 1% na média raramente vale um mês de complexidade — a menos que remova stutter, corrija latência de entrada ou previna um crash por memória. Priorize mudanças que os jogadores notam imediatamente e deixe o resto para depois.
O trabalho de desempenho fica dramaticamente mais fácil quando o programa está correto. Uma quantidade surpreendente de tempo gasto em “otimização” é na verdade perseguir bugs de corretude que apenas parecem problemas de desempenho: um loop O(N²) acidental por trabalho duplicado, um passe de render rodando duas vezes porque uma flag não foi resetada, um vazamento de memória que aumenta gradualmente o tempo por frame, ou uma condição de corrida que vira stutter aleatório.
Um engine estável e previsível dá medidas limpas. Se o comportamento muda entre execuções, você não pode confiar nos profiles e acabará otimizando ruído.
Práticas de engenharia disciplinadas ajudam a acelerar:
Muitos picos de tempo por frame são “Heisenbugs”: desaparecem quando você adiciona logging ou depura passo a passo. O antídoto é reprodução determinística.
Construa um pequeno harness de teste controlado:
Quando um hitch aparece, você quer um botão que o reproduza 100 vezes — não um relato vago de que “às vezes acontece depois de 10 minutos”.
Trabalho de performance se beneficia de mudanças pequenas e revisáveis. Grandes refactors criam múltiplos modos de falha de uma vez: regressões, novas alocações e trabalho extra oculto. Diffs menores facilitam responder à única pergunta que importa: o que mudou no tempo por frame, e por quê?
Disciplina aqui não é burocracia — é como manter as medições confiáveis para que otimização vire algo direto e não superstição.
Desempenho em tempo real não é só sobre “código mais rápido”. É sobre organizar o trabalho para que CPU e GPU possam executá-lo eficientemente. Carmack enfatizou repetidamente uma verdade simples: a máquina é literal. Ela ama dados previsíveis e odeia overhead evitável.
CPUs modernas são incrivelmente rápidas — até esperarem por memória. Se seus dados estão espalhados por muitos objetos pequenos, a CPU passa tempo perseguindo ponteiros em vez de fazer matemática.
Um modelo mental útil: não faça dez viagens ao mercado para dez itens. Coloque-os em um carrinho e percorra os corredores uma vez. Em código, isso significa manter valores usados frequentemente próximos (frequentemente em arrays ou structs compactos) para que cada fetch de cache line traga dados que você realmente vai usar.
Alocações frequentes criam custos escondidos: overhead do alocador, fragmentação de memória e pausas imprevisíveis quando o sistema precisa arrumar as coisas. Mesmo que cada alocação seja “pequena”, um fluxo constante pode se tornar um imposto pago por frame.
Correções comuns são propositalmente sem graça: reutilize buffers, faça pools de objetos e prefira alocações de longa vida para caminhos quentes. O objetivo não é esperteza — é consistência.
Uma quantidade surpreendente de tempo por frame pode desaparecer em bookkeeping: mudanças de estado, draw calls, trabalho do driver, syscalls e coordenação de threads.
Batching é a versão “um grande carrinho” do rendering e da simulação. Em vez de emitir muitas operações minúsculas, agrupe trabalho similar para cruzar fronteiras caras menos vezes. Muitas vezes, cortar overhead vence micro-otimizar um shader ou loop interior — porque a máquina passa menos tempo se preparando para trabalhar e mais tempo realmente trabalhando.
Trabalho de desempenho não é só código mais rápido — é também ter menos código. Complexidade tem um custo que você paga todo dia: bugs demoram mais para isolar, correções exigem testes mais cuidadosos, iteração desacelera porque cada mudança toca mais peças, e regressões surgem por caminhos raramente usados. Essa complexidade não só consome tempo de desenvolvedor; frequentemente adiciona overhead em tempo de execução (branches extras, alocações, faltas de cache, sincronização) que é difícil de ver até que seja tarde.
Um sistema “inteligente” pode parecer elegante até você estar num prazo e um pico de frame aparecer apenas em um mapa, uma GPU ou uma combinação de configurações. Cada flag extra, caminho de fallback e caso especial multiplica o número de comportamentos que você precisa entender e medir. Essa complexidade não só desperdiça tempo de dev; muitas vezes adiciona overhead de runtime difícil de detectar.
Uma boa regra: se você não consegue explicar o modelo de desempenho para um colega em poucas frases, provavelmente não consegue otimizá-lo de forma confiável.
Soluções simples têm duas vantagens:
Às vezes, o caminho mais rápido é remover uma feature, cortar uma opção ou colapsar variantes múltiplas em uma só. Menos features = menos caminhos de código, menos combinações de estado e menos lugares onde o desempenho pode degradar silenciosamente.
Deletar código também é um movimento de qualidade: o melhor bug é o que você elimina removendo o módulo que poderia gerá-lo.
Patch (correção cirúrgica) quando:
Refactor (simplificar estrutura) quando:
Simplicidade não é “menos ambição”. É escolher designs que continuam compreensíveis sob pressão — quando desempenho importa mais.
O trabalho de desempenho só permanece se você consegue dizer quando ele escorrega. É isso que é teste de regressão de desempenho: uma maneira repetível de detectar quando uma mudança torna o produto mais lento, menos suave ou mais pesado em memória.
Diferente dos testes funcionais (que respondem “funciona?”), testes de regressão respondem “continua com a mesma sensação de velocidade?”. Uma build pode estar 100% correta e ainda ser uma má release se acrescentar 4 ms de tempo por frame ou dobrar o tempo de carregamento.
Você não precisa de um laboratório para começar — apenas consistência.
Escolha um pequeno conjunto de cenas base que representem uso real: uma visão pesada na GPU, uma visão pesada na CPU e uma cena de stress “pior caso”. Mantenha-as estáveis e roteirize a câmera e inputs para que cada execução seja idêntica.
Rode os testes em hardware fixo (um PC/console/devkit conhecido). Se você mudar drivers, OS ou configurações de clock, registre isso. Trate a combinação hardware/software como parte do fixture de teste.
Armazene resultados em um histórico versionado: hash do commit, configuração do build, ID da máquina e métricas medidas. O objetivo não é um número perfeito — é uma linha de tendência confiável.
Prefira métricas difíceis de discutir:
Defina limiares simples (por exemplo: p95 não deve regredir mais que 5%).
Trate regressões como bugs: atribua um dono e um prazo.
Primeiro, bisecte para achar a mudança que a introduziu. Se a regressão bloquear uma release, reverta rápido e re-lande com a correção.
Ao consertar, adicione guardrails: mantenha o teste, deixe uma nota no código e documente o orçamento esperado. O hábito é a vitória — desempenho vira algo que você mantém, não algo que você “faz depois”.
“Shippar” não é um evento no calendário — é um requisito de engenharia. Um sistema que só roda bem no laboratório, ou que só atinge tempo por frame depois de uma semana de ajustes manuais, não está pronto. A mentalidade do Carmack trata restrições do mundo real (variedade de hardware, conteúdo bagunçado, comportamento imprevisível dos jogadores) como parte da especificação desde o dia um.
Quando você está perto do release, perfeição vale menos que previsibilidade. Defina os não negociáveis em termos claros: FPS alvo, picos de tempo por frame, limites de memória e tempos de carregamento. Então trate qualquer coisa que viole isso como bug, não “polish”. Isso reframeia trabalho de desempenho de otimização opcional para trabalho de confiabilidade.
Nem todas as lentidões importam igualmente. Conserte primeiro os problemas visíveis ao usuário:
A disciplina de profiling paga aqui: você não está adivinhando qual problema “parece grande”, você escolhe com base em impacto medido.
Trabalho de desempenho no ciclo tardio é arriscado porque “correções” podem introduzir novos custos. Use rollouts em etapas: primeiro registre instrumentação, depois ative a mudança atrás de um toggle, depois amplie a exposição. Prefira padrões seguros de desempenho — configurações que protejam o tempo por frame mesmo que reduzam um pouco a qualidade visual — especialmente para configurações auto-detectadas.
Se você lançar múltiplas plataformas ou tiers, trate defaults como uma decisão de produto: é melhor parecer um pouco menos fancy do que parecer instável.
Traduza trocas em resultados: “Este efeito custa 2 ms por frame em GPUs de nível médio, o que nos arrisca cair abaixo de 60 FPS durante lutas.” Ofereça opções, não palestras: reduzir resolução, simplificar o shader, limitar taxa de spawn, ou aceitar um alvo menor. Restrições são mais fáceis de aceitar quando enquadradas como escolhas concretas com impacto claro no usuário.
Você não precisa de um engine novo ou de um rewrite para adotar pensamento de desempenho ao estilo Carmack. Precisa de um loop repetível que torne desempenho visível, testável e difícil de quebrar acidentalmente.
Medir: capture uma baseline (média, p95, pior spike) para tempo por frame e subsistemas chave.
Orçar: defina um orçamento por frame para CPU e GPU (e memória se estiver apertado). Escreva o orçamento ao lado da meta da feature.
Isolar: reproduza o custo em uma cena mínima ou teste. Se não conseguir reproduzir, não consegue consertar de forma confiável.
Otimizar: mude uma coisa por vez. Prefira mudanças que reduzam trabalho, não apenas “deixem mais rápido”.
Validar: re-profile, compare deltas e cheque regressões de qualidade e corretude.
Documentar: registre o que mudou, por que ajudou e o que observar no futuro.
Se você quer operacionalizar esses hábitos numa equipe, a chave é reduzir atrito: experimentos rápidos, harnesses repetíveis e rollbacks fáceis.
Koder.ai pode ajudar quando você está construindo as ferramentas ao redor — não o engine em si. Como plataforma vibe-coding que gera código real exportável (web apps em React; backends em Go com PostgreSQL; mobile em Flutter), você pode rapidamente criar dashboards internos para percentis de tempo por frame, histórico de regressões e checklists de “revisão de desempenho”, então iterar via chat conforme requisitos evoluem. Snapshots e rollback também combinam com o loop “mude uma coisa, re-meça”.
Se quiser mais orientação prática, navegue por /blog ou veja como equipes operacionalizam isso em /pricing.
O tempo de frame é o tempo por quadro em milissegundos (ms) e mapeia diretamente quanto trabalho a CPU/GPU fez.
Escolha uma meta (por exemplo, 60 FPS) e converta-a para um prazo rígido (16.6 ms). Em seguida, divida esse prazo em orçamentos explícitos.
Exemplo de ponto de partida:
Trate esses valores como requisitos de produto e ajuste com base na plataforma, resolução, térmicas e metas de latência de entrada.
Comece tornando seus testes repetíveis e, só então, meça antes de mudar qualquer coisa.
Só depois de saber onde o tempo está indo você deve decidir o que otimizar.
Faça experimentos rápidos e focados para isolar o limitador:
Porque os usuários sentem os piores frames, não a média.
Rastreie:
Uma build que tenha média de 16.6 ms mas estoure para 80 ms ainda parecerá quebrada.
Torne o trabalho caro previsível e agendado:
Também registre os picos para que você possa reproduzi-los e corrigi-los, em vez de apenas “esperar que sumam”.
Torne a troca explícita em números e impacto no usuário.
Use frases do tipo:
Então decida com base em limiares acordados:
Porque correção instável torna os dados de desempenho pouco confiáveis.
Passos práticos:
Se o comportamento mudar de execução para execução, você vai acabar otimizando ruído em vez de gargalos.
Muito do trabalho “código rápido” é, na verdade, trabalho em memória e overhead.
Foque em:
Frequentemente, cortar overhead traz ganhos maiores que ajustar um loop interior.
Torne o desempenho mensurável, repetível e difícil de quebrar acidentalmente.
Evite reescrever sistemas até conseguir nomear o custo dominante em milissegundos.
Se estiver em dúvida, prefira decisões reversíveis (feature flags, níveis de qualidade escaláveis).
Quando aparecer uma regressão: bisecte, atribua um dono e reverta rapidamente se bloquear a release.