Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Consigli generali di ottimizzazione
Introduzione
In un mondo ideale, i computer funzionerebbero a velocità infinita. L'unico limite a ciò che potremmo realizzare sarebbe la nostra immaginazione. Tuttavia, nel mondo reale, è fin troppo facile produrre software che metterebbe in ginocchio anche il computer più veloce.
Pertanto, progettare giochi e altri software è un compromesso tra ciò che vorremmo fosse possibile e ciò che possiamo realisticamente ottenere mantenendo buone prestazioni.
Per ottenere i migliori risultati, abbiamo due approcci:
Lavora più rapidamente.
Lavora più intelligentemente.
E preferibilmente, useremo un misto dei due.
Fumo e niente arrosto
Parte di lavorare più intelligentemente è riconoscere che, nei videogiochi, spesso possiamo far credere al giocatore di trovarsi in un mondo molto più complesso, interattivo e graficamente avvincente di quanto non sia in realtà. Un buon programmatore è un prestigiatore e dovrebbe impegnarsi a imparare i trucchi del mestiere mentre cerca di inventarne di nuovi.
La natura della lentezza
Per un osservatore esterno, i problemi di prestazioni sono spesso considerati un tutt'uno. Ma in realtà, esistono diversi tipi di problemi di prestazioni:
Un processo lento che avviene a ogni frame, portando a una frequenza di frame costantemente bassa.
Un processo intermittente che provoca "picchi" di lentezza, portando a blocchi.
Un processo lento che avviene al di fuori del normale gameplay, ad esempio durante il caricamento di un livello.
Ognuno di questi è fastidioso per l'utente, ma in modi diversi.
Misurare le prestazioni
Probabilmente il mezzo più importante per l'ottimizzazione è la capacità di misurare le prestazioni, ovvero di individuare dove si trovano i colli di bottiglia e di misurare il successo dei nostri tentativi di accelerarli.
Esistono diversi metodi per misurare le prestazioni, tra cui:
Inserire un timer di avvio/arresto attorno al codice di interesse.
Utilizzo del profiler Godot.
Utilizzare profiler di CPU esterni.
Utilizzare profiler/debugger di GPU esterni come NVIDIA Nsight Graphics, Radeon GPU Profiler, PIX (solo Direct3D 12), Xcode (solo Metal) o Arm Performance Studio.
Verificare la frequenza dei frame (con V-Sync disattivato). Anche utilità di terze parti come RivaTuner Statistics Server (Windows), Special K (Windows) o MangoHud (Linux) possono essere utili.
Utilizzare un componente aggiuntivo non ufficiale per il menu di debug.
Si faccia molta attenzione che le prestazioni relative di diverse aree possono variare a seconda dell'hardware. Spesso è consigliabile misurare i tempi su più dispositivi. Questo è particolarmente il caso se si puntano i dispositivi mobili.
Limitazioni
I profiler di CPU sono spesso il metodo di riferimento per misurare le prestazioni. Tuttavia, non raccontano sempre l'intera storia.
I colli di bottiglia avvengono spesso sulla GPU, "come risultato" delle istruzioni fornite dalla CPU.
I picchi possono avvenire nei processi del sistema operativo (fuori da Godot) "come risultato" delle istruzioni utilizzate in Godot (ad esempio, l'allocazione dinamica della memoria).
Potrebbe non essere sempre possibile profilare un dispositivo specifico, come un telefono cellulare, a causa della configurazione iniziale richiesta.
Potrebbe essere necessario risolvere problemi di prestazioni che occorrono su hardware a cui non si ha accesso.
A causa di queste limitazioni, spesso è necessario indagare come un detective per scoprire dove si trovano i colli di bottiglia.
Lavoro investigativo
Il lavoro investigativo è un'abilità cruciale per gli sviluppatori (in termini sia di prestazioni sia di correzione dei bug). Questo può includere valutazione delle ipotesi e ricerca binaria.
Valutazione delle ipotesi
Supponiamo, ad esempio, che si creda che gli sprite rallentino il gioco. È possibile verificare questa ipotesi:
Misurando le prestazioni quando si aggiungono o si rimuovono più sprite.
Ciò potrebbe portare a un'ulteriore ipotesi: la dimensione dello sprite determina il calo delle prestazioni?
È possibile verificarlo mantenendo tutto lo stesso, ma cambiando le dimensioni dello sprite e misurando le prestazioni.
Ricerca binaria
Se è noto che i frame impiegano molto più tempo del previsto, ma non si è sicuri di dove si trovi il collo di bottiglia, si potrebbe iniziare commentando circa la metà delle routine che avvengono su un frame normale. Le prestazioni sono migliorate più o meno del previsto?
Una volta individuata la metà che contiene il collo di bottiglia, è possibile ripetere questo processo finché non si è individuata l'area problematica.
Profiler
Profilers allow you to time your program while running it. Profilers then provide results telling you what percentage of time was spent in different functions and areas, and how often functions were called.
This can be very useful both to identify bottlenecks and to measure the results of your improvements. Sometimes, attempts to improve performance can backfire and lead to slower performance. Always use profiling and timing to guide your efforts.
Per ulteriori informazioni su come utilizzare il profiler integrato di Godot, consultare Il Profiler.
Principi
Donald Knuth ha detto:
I programmatori sprecano enormi quantità di tempo a pensare, o a preoccuparsi, della velocità di parti non critiche dei loro programmi, e questi tentativi di efficienza hanno in realtà un forte impatto negativo quando si considerano il debug e la manutenzione. Dovremmo dimenticare le piccole efficienze, diciamo nel 97% dei casi: l'ottimizzazione prematura è la radice di tutti i mali. Eppure non dovremmo lasciarci sfuggire le opportunità in quel 3% critico.
I messaggi sono molto importanti:
Il tempo a disposizione degli sviluppatori è limitato. Invece di cercare ciecamente di velocizzare tutti gli aspetti di un programma, dovremmo concentrare i nostri sforzi sugli aspetti che contano davvero.
Gli sforzi di ottimizzazione spesso finiscono per produrre codice più difficile da leggere e correggere rispetto al codice non ottimizzato. È nel nostro interesse limitare questi sforzi alle aree che ne trarranno realmente beneficio.
Soltanto perché possiamo ottimizzare una particolare porzione di codice, non significa necessariamente che dobbiamo farlo. Sapere quando ottimizzare e quando no è un'abilità fondamentale da sviluppare.
Un aspetto ingannoso della citazione è che le persone tendono a concentrarsi sulla sotto-citazione "l'ottimizzazione prematura è la radice di tutti i mali". Mentre l'ottimizzazione prematura è (per definizione) indesiderabile, un software performante è il risultato di una progettazione performante.
Design performante
Il pericolo nell'incoraggiare le persone a ignorare l'ottimizzazione finché non è necessario è che si ignora convenientemente che il momento più importante per considerare le prestazioni è la fase di progettazione, prima ancora che un tasto sia premuto sulla tastiera. Se la progettazione o gli algoritmi di un programma sono inefficienti, nessuna quantità di perfezionamento dei dettagli in seguito lo renderà veloce. Potrebbe funzionare più veloce, ma non sarà mai veloce quanto un programma progettato per le prestazioni.
Questo aspetto tende ad essere molto più importante nella programmazione di videogiochi o di grafica che nella programmazione in generale. Un progetto performante, anche senza ottimizzazione di basso livello, spesso funzionerà molto più velocemente di un progetto mediocre con ottimizzazione di basso livello.
Design incrementale
Naturalmente, in pratica, a meno che non si abbiano conoscenze pregresse, è improbabile che si riesca a trovare la progettazione migliore al primo tentativo. Invece, spesso si realizzano una serie di versioni di una particolare area di codice, ognuna delle quali adotta un approccio diverso al problema, fino a raggiungere una soluzione soddisfacente. È importante non dedicare troppo tempo ai dettagli in questa fase, finché non si è finalizzato la progettazione complessiva. Altrimenti, gran parte del lavoro verrà sprecato.
È difficile dare linee guida generali per una progettazione performante, perché questa dipende molto dal problema specifico. Un punto che vale la pena menzionare, tuttavia, per quanto riguarda la CPU, è che le CPU moderne sono quasi sempre limitate dalla larghezza di banda della memoria. Ciò ha portato a una rinascita della progettazione orientata ai dati, che consiste a progettare strutture dati e algoritmi per la località della cache dei dati e l'accesso lineare, piuttosto che salti nella memoria.
Il processo di ottimizzazione
Supponendo di avere una progettazione ragionevole e prendendo spunto da Knuth, il nostro primo passo nell'ottimizzazione dovrebbe essere quello di identificare i colli di bottiglia più grandi, ovvero le funzioni più lente, i frutti più facili da raggiungere.
Una volta migliorata la velocità dell'area più lenta, potrebbe non essere più il collo di bottiglia. Quindi dovremmo testare/profilare nuovamente e individuare il prossimo collo di bottiglia su cui concentrarci.
Il processo è quindi:
Profilare / Indentificare i colli di bottiglia.
Ottimizzare il collo di bottiglia.
Ritornare al passo 1.
Ottimizzazione dei colli di bottiglia
Alcuni profiler diranno addirittura quale parte di una funzione (quali accessi ai dati, calcoli) sta rallentando il tutto.
As with design, you should concentrate your efforts first on making sure the algorithms and data structures are the best they can be. Data access should be local (to make best use of CPU cache), and it can often be better to use compact storage of data (again, always profile to test results). Often, you precalculate heavy computations ahead of time. This can be done by performing the computation when loading a level, by loading a file containing precalculated data, or by storing the results of complex calculations into a script constant and reading its value.
Una volta che gli algoritmi e i dati vanno bene, spesso è possibile apportare piccole modifiche alle routine per migliorarne le prestazioni. Ad esempio, è possibile spostare alcuni calcoli all'esterno dei cicli o trasformare cicli for annidati in cicli non annidati. (Questo dovrebbe essere fattibile se si conosce in anticipo la larghezza o l'altezza di un array 2D.)
Ritestare sempre i tempi/colli di bottiglia dopo ogni modifica. Alcune modifiche aumenteranno la velocità, altre potrebbero avere un effetto negativo. A volte, un piccolo effetto positivo sarà controbilanciato dagli aspetti negativi di un codice più complesso, e si potrebbe scegliere di tralasciare quell'ottimizzazione.
Appendice
Matematica dei colli di bottiglia
Il proverbio "una catena è forte quanto il suo anello più debole" si applica direttamente all'ottimizzazione delle prestazioni. Se il progetto trascorre il 90% del tempo nella funzione A, allora ottimizzare A può avere un impatto enorme sulle prestazioni.
A: 9 ms
Everything else: 1 ms
Total frame time: 10 ms
A: 1 ms
Everything else: 1ms
Total frame time: 2 ms
In questo esempio, migliorare il collo di bottiglia A per un fattore di 9x riduce il tempo complessivo di frame di 5x, allo stesso tempo aumentando i frame al secondo di 5x.
Tuttavia, se qualcos'altro si sta eseguendo lentamente e crea un collo di bottiglia nel progetto, lo stesso miglioramento può portare a guadagni meno drastici:
A: 9 ms
Everything else: 50 ms
Total frame time: 59 ms
A: 1 ms
Everything else: 50 ms
Total frame time: 51 ms
In questo esempio, nonostante la funzione A sia stata notevolmente ottimizzata, il guadagno effettivo in termini di frame rate è piuttosto ridotto.
Nei giochi, le cose si complicano ulteriormente perché la CPU e la GPU funzionano indipendentemente l'una dall'altra. Il frame time totale è determinato dalla più lenta delle due.
CPU: 9 ms
GPU: 50 ms
Total frame time: 50 ms
CPU: 1 ms
GPU: 50 ms
Total frame time: 50 ms
In questo esempio abbiamo ottimizzato ancora una volta notevolmente la CPU, ma il frame time non è migliorato perché siamo limitati dalla GPU.