Guida pratica alla mentalità “performance-first” associata a John Carmack: profiling, budget per tempo per frame, compromessi e come consegnare sistemi reali in tempo reale.

John Carmack è spesso trattato come una leggenda dei motori di gioco, ma la parte utile non è la mitologia—sono le abitudini ripetibili. Non si tratta di copiare lo stile di una persona o di attribuire tutto alla “mossa da genio”. Si tratta di principi pratici che portano, con costanza, a software più veloci e fluidi, specialmente quando scadenze e complessità si accumulano.
L’ingegneria delle prestazioni significa far sì che il software rispetti un obiettivo di velocità sull’hardware reale, in condizioni reali—senza rompere la correttezza. Non è “rendilo veloce a ogni costo”. È un ciclo disciplinato:
Questa mentalità ricorre nel lavoro di Carmack: discutere con i dati, mantenere le modifiche spiegabili e preferire approcci sostenibili.
La grafica in tempo reale è spietata perché ha una scadenza ogni frame. Se la fallisci, l’utente lo percepisce subito come stutter, input lag o movimento irregolare. Altri software possono nascondere inefficienze dietro code, schermate di caricamento o lavoro in background. Un renderer non può trattare: o finisci in tempo, o non finisci.
Per questo le lezioni si generalizzano oltre i giochi. Qualsiasi sistema con requisiti di latenza stretti—UI, audio, AR/VR, trading, robotica—beneficia dal pensare per budget, capire i colli di bottiglia ed evitare picchi a sorpresa.
Avrai checklist, euristiche e schemi decisionali applicabili al tuo lavoro: come impostare budget per tempo per frame (o latenza), come fare profiling prima di ottimizzare, come scegliere la “cosa unica” da risolvere e come prevenire regressioni affinché la performance diventi routine—non un panico dell’ultimo minuto.
Il pensiero sulle prestazioni in stile Carmack parte da una semplice inversione: smetti di parlare di “FPS” come unità primaria e comincia a parlare del tempo per frame.
L’FPS è un reciproco (“60 FPS” suona bene, “55 FPS” suona vicino), ma l’esperienza utente è guidata da quanto dura ogni frame—e, altrettanto importante, da quanto sono coerenti quei tempi. Un salto da 16.6 ms a 33.3 ms è immediatamente visibile anche se l’FPS medio sembra rispettabile.
Un prodotto in tempo reale ha più budget, non solo “render più veloce”:
Questi budget interagiscono. Risparmiare tempo GPU aggiungendo batching pesante sulla CPU può ritorcersi contro, e ridurre memoria può aumentare i costi di streaming o decompressione.
Se il tuo target è 60 FPS, il budget totale è 16.6 ms per frame. Una ripartizione grezza potrebbe essere:
Se CPU o GPU superano il budget, perdi il frame. Ecco perché i team parlano di essere “CPU-bound” o “GPU-bound”: non sono etichette, ma modi per decidere dove può realisticamente venire il prossimo millisecondo.
Non si tratta di inseguire una metrica di vanità come “massimo FPS su una macchina top”. Si tratta di definire cosa è abbastanza veloce per il tuo pubblico—target hardware, risoluzione, limiti di batteria, termiche e reattività in input—e poi trattare la performance come budget espliciti che puoi gestire e difendere.
La mossa predefinita di Carmack non è “ottimizza”, è “verifica”. I problemi di performance in tempo reale sono pieni di storie plausibili—pause GC, “shader lenti”, “troppe draw call”—e la maggior parte di queste spiegazioni è sbagliata nella tua build sul tuo hardware. Il profiling è come sostituire l’intuizione con l’evidenza.
Tratta il profiling come una feature di prima classe, non uno strumento di salvataggio all’ultimo minuto. Cattura i tempi per frame, le timeline CPU e GPU e i conteggi che li spiegano (triangoli, draw call, cambi di stato, allocazioni, cache miss se puoi ottenerli). L’obiettivo è rispondere a una domanda: dove sta andando realmente il tempo?
Un modello utile: in ogni frame lento, una cosa è il fattore limitante. Forse è la GPU bloccata su un pass pesante, la CPU bloccata nell’update delle animazioni o il thread principale fermo sulla sincronizzazione. Trova prima quel vincolo; tutto il resto è rumore.
Un loop disciplinato ti evita di oscillare:
Se il miglioramento non è chiaro, presumilo non utile—perché probabilmente non sopravviverà al prossimo drop di contenuti.
Il lavoro sulle prestazioni è particolarmente vulnerabile all’auto-illusione:
Profilare prima mantiene lo sforzo concentrato, i compromessi giustificati e le modifiche più facili da difendere in revisione.
I problemi di performance in tempo reale sembrano disordinati perché tutto accade insieme: gameplay, rendering, streaming, animazione, UI, fisica. L’istinto di Carmack è tagliare il rumore e identificare il limitante dominante—la cosa che sta attualmente fissando il tempo per frame.
La maggior parte dei rallentamenti rientra in pochi insiemi:
Lo scopo non è etichettare per un report—è tirare la leva giusta.
Alcuni esperimenti veloci possono dirti cosa è realmente in controllo:
Raramente si vince raschiando l’1% su dieci sistemi. Trova il costo più grande che si ripete ogni frame e attaccalo per primo. Rimuovere un singolo colpevole da 4 ms batte settimane di micro-ottimizzazioni.
Dopo aver risolto la grossa roccia, la prossima diventa visibile. È normale. Tratta il lavoro di performance come un loop: misura → cambia → rimisura → riprioritizza. L’obiettivo non è un profilo perfetto; è un progresso costante verso tempi per frame prevedibili.
Il tempo medio per frame può sembrare ok mentre l’esperienza è comunque scadente. La grafica in tempo reale è giudicata dai momenti peggiori: il frame perso durante un’esplosione, il blocco entrando in una stanza nuova, il brusco stutter quando si apre un menu. Quella è latenza di coda—frame lenti rari ma non così rari da essere ignorati.
Un gioco che gira a 16.6 ms la maggior parte del tempo (60 FPS) ma che spike a 60–120 ms ogni pochi secondi sembrerà “rotto”, anche se la media stampa ancora come 20 ms. Gli esseri umani sono sensibili al ritmo. Un singolo frame lungo rompe la prevedibilità dell’input, il movimento della camera e la sincronizzazione audio/visiva.
Gli spike spesso provengono da lavoro che non è distribuito uniformemente:
L’obiettivo è rendere il lavoro costoso prevedibile:
Non tracciare solo una linea di FPS media. Registra i tempi per frame e visualizza:
Se non puoi spiegare i tuoi peggiori 1% dei frame, non hai davvero spiegato le prestazioni.
Il lavoro sulle prestazioni diventa più semplice nel momento in cui smetti di fingere di poter avere tutto insieme. Lo stile di Carmack spinge i team a nominare il compromesso ad alta voce: cosa stiamo comprando, cosa stiamo pagando e chi ne sente la differenza?
La maggior parte delle decisioni si trova su pochi assi:
Se una modifica migliora un asse ma grava discretamente su tre altri, documentalo. “Questo aggiunge 0.4 ms GPU e 80 MB VRAM per ottenere ombre più morbide” è una frase utilizzabile. “Sembra meglio” non lo è.
La grafica in tempo reale non è perfezione; è centrare un target con coerenza. Concorda soglie come:
Una volta che il team è d’accordo che, per esempio, 16.6 ms a 1080p su GPU di riferimento è l’obiettivo, le discussioni diventano concrete: questa feature ci mantiene sotto budget o forza un declassamento altrove?
Quando sei incerto, scegli opzioni che puoi annullare:
La reversibilità protegge il programma. Puoi spedire la via sicura e tenere l’ambiziosa dietro un toggle.
Evita l’overengineering per vittorie invisibili. Un miglioramento medio dell’1% raramente vale un mese di complessità—a meno che non rimuova stutter, risolva la latenza di input o prevenga un crash per memoria. Prioritizza i cambiamenti che i giocatori notano immediatamente, e lascia il resto in attesa.
Il lavoro sulle prestazioni diventa molto più semplice quando il programma è giusto. Una quantità sorprendente di tempo d’ottimizzazione è in realtà spesa a inseguire bug di correttezza che sembrano problemi di performance: un ciclo O(N²) accidentale dovuto a lavoro duplicato, un pass di render eseguito due volte perché una flag non si è resettata, una perdita di memoria che aumenta lentamente il tempo per frame, o una race che diventa stuttering casuale.
Un motore stabile e prevedibile ti dà misurazioni pulite. Se il comportamento cambia tra le esecuzioni, non puoi fidarti dei profili e finirai per ottimizzare il rumore.
Le pratiche disciplinate aiutano la velocità:
Molti spike sono “Heisenbugs”: scompaiono quando aggiungi logging o usi il debugger. L’antidoto è la riproduzione deterministica.
Costruisci un piccolo harness di test controllato:
Quando compare un hitch, vuoi un pulsante che lo ripeta 100 volte—non un generico rapporto che dice “succede a volte dopo 10 minuti”.
Il lavoro sulle prestazioni beneficia di cambi piccoli e revisionabili. I grandi refactor creano molteplici modalità di errore insieme: regressioni, nuove allocazioni e lavoro extra nascosto. Diff ristretti rendono più facile rispondere all’unica domanda che conta: cosa è cambiato nel tempo per frame, e perché?
La disciplina non è burocrazia—è come mantenere le misurazioni affidabili così l’ottimizzazione diventa lineare invece che superstiziosa.
La performance in tempo reale non riguarda solo “codice più veloce”. È organizzare il lavoro in modo che CPU e GPU possano svolgerlo efficacemente. Carmack sottolineava spesso una verità semplice: la macchina è letterale. Ama dati prevedibili e odia overhead evitabile.
Le CPU moderne sono incredibilmente veloci—finché non aspettano la memoria. Se i tuoi dati sono sparsi in tanti piccoli oggetti, la CPU passa il tempo a inseguire puntatori invece che fare i calcoli.
Un modello mentale utile: non fare dieci piccoli viaggi per dieci articoli. Mettili in un carrello e attraversa i corridoi una volta sola. In codice, vuol dire tenere valori usati frequentemente vicini (spesso in array o struct compatti) così ogni fetch di cache porta dati che userai davvero.
Allocazioni frequenti creano costi nascosti: overhead dell’allocatore, frammentazione della memoria e pause imprevedibili quando il sistema deve riordinare. Anche se ogni allocazione è “piccola”, un flusso costante può diventare una tassa pagata ogni frame.
I fix comuni sono intenzionalmente noiosi: riusa buffer, poola oggetti e preferisci allocazioni di lunga durata per i percorsi caldi. L’obiettivo non è l’ingegnosità—è la coerenza.
Una quantità sorprendente di tempo per frame può sparire in bookkeeping: cambi di stato, draw call, lavoro del driver, syscall e coordinazione tra thread.
Il batching è la versione del “grande carrello” per rendering e simulazione. Invece di emettere molte piccole operazioni, raggruppa lavoro simile così attraversi confini costosi meno volte. Spesso ridurre l’overhead batte micro-ottimizzare uno shader o un loop interno—perché la macchina passa meno tempo a prepararsi e più tempo a lavorare davvero.
Il lavoro sulle prestazioni non riguarda solo codice più veloce—riguarda anche avere meno codice. La complessità ha un costo che paghi ogni giorno: i bug richiedono più tempo per essere isolati, le correzioni richiedono test più attenti, l’iterazione rallenta perché ogni cambiamento tocca più parti e le regressioni si insinuano attraverso percorsi poco usati.
Un sistema “geniale” può sembrare elegante fino a quando sei in scadenza e uno spike appare solo su una mappa, una GPU o una combinazione di impostazioni. Ogni feature flag in più, percorso di fallback e caso speciale moltiplica i comportamenti che devi capire e misurare. Quella complessità non solo sprecaa tempo degli sviluppatori; spesso aggiunge overhead in esecuzione (ramificazioni in più, allocazioni, cache miss, sincronizzazioni) difficile da vedere finché non è troppo tardi.
Una buona regola: se non riesci a spiegare il modello di performance a un collega in poche frasi, probabilmente non puoi ottimizzarlo in modo affidabile.
Le soluzioni semplici hanno due vantaggi:
A volte la strada più veloce è rimuovere una feature, eliminare un’opzione o unificare varianti multiple in una sola. Meno feature significa meno percorsi di codice, meno combinazioni di stato e meno punti dove la performance può degradare silenziosamente.
Cancellare codice è anche una mossa di qualità: il miglior bug è quello che non può più verificarsi perché il modulo che lo generava è stato rimosso.
Patch (fix chirurgico) quando:
Refactor (semplificare la struttura) quando:
La semplicità non è “meno ambiziosa”. È scegliere design che restano comprensibili sotto pressione—quando la performance conta di più.
Il lavoro sulle prestazioni resta solo se puoi sapere quando scivola. Questo è ciò che significa testing per regressioni di prestazione: un modo ripetibile per rilevare quando una nuova modifica rallenta il prodotto, peggiora la fluidità o aumenta l’uso di memoria.
A differenza dei test funzionali (che rispondono “funziona?”), i test di regressione rispondono “è ancora veloce come prima?” Una build può essere corretta al 100% eppure essere una cattiva release se aggiunge 4 ms al tempo per frame o raddoppia i tempi di caricamento.
Non serve un laboratorio—solo coerenza.
Scegli un piccolo set di scene baseline che rappresentino l’uso reale: una vista GPU-heavy, una vista CPU-heavy e una scena di stress “worst case”. Tienile stabili e scriptate così percorso camera e input sono identici esecuzione dopo esecuzione.
Esegui i test su hardware fisso (un PC/console/devkit noto). Se cambi driver, OS o impostazioni di clock, registralo. Tratta la combinazione hardware/software come parte del fixture di test.
Conserva i risultati in una storia versionata: hash di commit, config build, ID macchina e metriche misurate. L’obiettivo non è un numero perfetto—è una linea di tendenza affidabile.
Preferisci metriche difficili da discutere:
Definisci soglie semplici (per esempio: p95 non deve regredire oltre il 5%).
Tratta le regressioni come bug con un proprietario e una scadenza.
Prima, bisecta per trovare la modifica che l’ha introdotta. Se la regressione blocca il rilascio, reverti rapidamente e poi rilanda con la correzione.
Quando risolvi, aggiungi guardrail: conserva il test, aggiungi una nota nel codice e documenta il budget atteso. L’abitudine è la vittoria—la performance diventa qualcosa da mantenere, non da fare “più tardi”.
“Ship” non è un evento in calendario—è un requisito ingegneristico. Un sistema che funziona bene solo in laboratorio, o che arriva a tempi per frame accettabili solo dopo una settimana di tuning manuale, non è finito. La mentalità di Carmack tratta vincoli reali (varietà hardware, contenuti disordinati, comportamento imprevedibile dei giocatori) come parte della specifica fin dal giorno uno.
Quando sei vicino al rilascio, la prevedibilità vale più della perfezione. Definisci i non negoziabili in termini chiari: target FPS, spike massimi per frame, limiti di memoria e tempi di caricamento. Poi considera tutto ciò che li viola come bug, non “polish”. Questo rimette il lavoro sulle prestazioni nella categoria affidabilità invece che ottimizzazione opzionale.
Non tutti i rallentamenti hanno lo stesso peso. Risolvi prima i problemi maggiormente visibili dagli utenti:
La disciplina del profiling ripaga qui: non stai indovinando quale problema è “grande”, scegli in base all’impatto misurato.
Il lavoro di performance a ciclo tardivo è rischioso perché le “correzioni” possono introdurre nuovi costi. Usa rollout a stadi: landa prima l’instrumentazione, poi la modifica dietro un toggle, poi amplia l’esposizione. Prediligi default sicuri—impostazioni che proteggono il tempo per frame anche se riducono leggermente la qualità visiva—specialmente per configurazioni auto-detect.
Se esci con più piattaforme o tier, tratta i default come una decisione di prodotto: è meglio apparire un po’ meno ricco che risultare instabile.
Traduci i compromessi in risultati: “Questo effetto costa 2 ms ogni frame su GPU mid-tier, il che rischia di scendere sotto i 60 FPS durante i combattimenti.” Offri opzioni, non lezioni: riduci la risoluzione, semplifica lo shader, limita il rate di spawn o accetta un target più basso. I vincoli sono più facili da accettare se presentati come scelte concrete con chiaro impatto utente.
Non ti serve un nuovo motore o una riscrittura per adottare il pensiero sulle prestazioni in stile Carmack. Ti serve un loop ripetibile che renda la performance visibile, testabile e difficile da rompere per errore.
Misura: cattura una baseline (media, p95, peggior spike) per il tempo per frame e i sottosistemi chiave.
Budget: imposta un budget per frame per CPU e GPU (e memoria se sei tirato). Scrivi il budget accanto all’obiettivo della feature.
Isola: riproduci il costo in una scena minima o test. Se non lo puoi riprodurre, non puoi fissarlo in modo affidabile.
Ottimizza: cambia una cosa alla volta. Preferisci modifiche che riducono il lavoro, non solo “lo rendono più veloce”.
Valida: riprofilare, confronta i delta e controlla regressioni di qualità e problemi di correttezza.
Documenta: registra cosa è cambiato, perché ha aiutato e cosa monitorare in futuro.
Se vuoi operationalizzare queste abitudini in un team, la chiave è ridurre l’attrito: esperimenti rapidi, harness ripetibili e rollback semplici.
Koder.ai può aiutare quando costruisci il tooling di contorno—non il motore in sé. Essendo una piattaforma vibe-coding che genera codice sorgente reale esportabile (web app in React; backend in Go con PostgreSQL; mobile in Flutter), puoi creare rapidamente cruscotti interni per percentili del tempo per frame, cronologia delle regressioni e moduli di “performance review”, poi iterare via chat man mano che i requisiti evolvono. Snapshot e rollback si allineano bene con il loop “cambia una cosa, rimisura”.
Se vuoi più guida pratica, sfoglia /blog o guarda come i team concretizzano questo su /pricing.
Il tempo per frame è il tempo impiegato per ogni frame in millisecondi (ms) e corrisponde direttamente a quanto lavoro ha fatto CPU/GPU.
Scegli un target (es., 60 FPS) e converti in una scadenza hard (16.6 ms). Poi dividi quella scadenza in budget espliciti.
Esempio di punto di partenza:
Trattali come requisiti di prodotto e adatta in base a piattaforma, risoluzione, termiche e obiettivi di latenza di input.
Inizia rendendo i test ripetibili, poi misura prima di cambiare qualcosa.
Solo dopo aver capito dove va il tempo decidi cosa ottimizzare.
Esegui esperimenti rapidi e mirati che isolino il limitatore:
Perché gli utenti avvertono i frame peggiori, non la media.
Traccia:
Una build che fa in media 16.6 ms ma spike a 80 ms sembrerà comunque rotta.
Rendi il lavoro costoso prevedibile e pianificato:
Inoltre registra gli spike così da poterli riprodurre e correggere, non solo sperare che spariscano.
Rendi espliciti i compromessi in numeri e impatto utente.
Usa frasi come:
Poi decidi in base a soglie concordate:
Perché uno stato non corretto rende i dati di prestazione inaffidabili.
Passi pratici:
Gran parte del “codice veloce” è in realtà lavoro su memoria e overhead.
Concentrati su:
Spesso ridurre l’overhead dà vincite maggiori rispetto a rifinire un inner loop.
Rendi la performance misurabile, ripetibile e difficile da rompere accidentalmente.
Evita di riscrivere sistemi finché non puoi quantificare il costo dominante in millisecondi.
Se non sei sicuro, preferisci decisioni reversibili (feature flag, livelli di qualità scalabili).
Se il comportamento cambia da esecuzione a esecuzione, finirai per ottimizzare rumore invece dei colli di bottiglia.
Quando appare una regressione: bisecta, assegna un proprietario e reverti rapidamente se blocca il rilascio.