In generale, quanto spesso e quando devo ottimizzare il mio codice?


13

Nella fase di "normale" ottimizzazione della programmazione aziendale viene spesso lasciato fino a quando non è strettamente necessario. Ciò significa che non dovresti optare per l'ottimizzazione fino a quando non sarà veramente necessario.

Ricorda cosa ha detto Donald Knuth "Dovremmo dimenticare le piccole efficienze, diciamo circa il 97% delle volte: l'ottimizzazione prematura è la radice di tutti i mali"

Quando è il momento di ottimizzare per assicurarsi che non stia sprecando sforzi. Dovrei farlo a livello di metodo? Livello di classe? Livello del modulo?

Inoltre, quale dovrebbe essere la mia misurazione dell'ottimizzazione? Le zecche? Frequenza dei fotogrammi? Tempo totale?

Risposte:


18

Dove ho lavorato, utilizziamo sempre più livelli di profilazione; se vedi un problema, scorri un po 'più in basso l'elenco fino a capire cosa sta succedendo:

  • Il "profiler umano", noto anche come gioco ; ti senti lento o "agganciato" di tanto in tanto? Notare animazioni a scatti? (Come sviluppatore, tieni presente che sarai più sensibile ad alcuni tipi di problemi di prestazioni e ignaro di altri. Pianifica di conseguenza ulteriori test.)
  • Attiva il display FPS , che è un FPS medio di 5 secondi a finestra scorrevole. Spese generali minime da calcolare e visualizzare.
  • Attiva le barre del profilo , che sono solo una serie di quadranti (colori ROYGBIV) che rappresentano diverse parti del fotogramma (ad esempio vblank, preframe, update, collision, render, postframe) usando un semplice timer "cronometro" attorno a ciascuna sezione del codice . Per enfatizzare ciò che vogliamo, impostiamo una larghezza di barra dello schermo che sia rappresentativa di un frame di destinazione a 60Hz, quindi è davvero facile vedere se hai ad esempio il 50% di budget (solo una mezza barra) o il 50% di oltre ( la barra si avvolge e diventa una e mezza). È anche abbastanza facile dire cosa generalmente mangia la maggior parte del frame: rosso = rendering, giallo = aggiornamento, ecc ...
  • Costruisci una speciale build strumentata che inserisce "cronometro" come il codice attorno a ciascuna funzione. (Tieni presente che potresti fare grandi prestazioni, dcache e icache quando lo fai, quindi è decisamente invadente. Ma se ti manca un profiler di campionamento adeguato o un supporto decente sulla CPU, questa è un'opzione accettabile. Puoi anche essere intelligente sulla registrazione di un minimo di dati sulla funzione enter / exit e sulla ricostruzione di calltraces in seguito.) Quando abbiamo creato il nostro, abbiamo imitato gran parte del formato di output di gprof .
  • Soprattutto, eseguire un profiler di campionamento ; VTune e CodeAnalyst sono disponibili per x86 e x64, hai vari ambienti di simulazione o emulazione che potrebbero fornirti dati qui.

(C'è una storia divertente del GDC dell'anno scorso di un programmatore grafico che ha scattato quattro foto di se stesso - felice, indifferente, infastidito e arrabbiato - e ha mostrato un'immagine appropriata nell'angolo delle build interne basata sul framerate. i creatori di contenuti impararono rapidamente a non attivare shader complicati per tutti i loro oggetti e ambienti: avrebbero fatto arrabbiare il programmatore. Ecco il potere del feedback.)

Nota che puoi anche fare cose divertenti come rappresentare graficamente le "barre del profilo" in modo continuo, in modo da poter vedere modelli di spike ("stiamo perdendo un frame ogni 7 frame") o simili.

Per rispondere direttamente alla tua domanda, però: nella mia esperienza, mentre è allettante (e spesso gratificante - di solito imparo qualcosa) a riscrivere singole funzioni / moduli per ottimizzare il numero di istruzioni o le prestazioni di icache o dcache, e in realtà dobbiamo fare questo a volte quando abbiamo un problema di prestazioni particolarmente odioso, la stragrande maggioranza dei problemi di prestazione che affrontiamo su base regolare si riduce alla progettazione . Per esempio:

  • Dovremmo memorizzare nella cache RAM o ricaricare dal disco i frame di animazione dello stato "attacco" per il lettore? Che ne dici di ogni nemico? Non abbiamo RAM per eseguirli tutti, ma i carichi del disco sono costosi! Puoi vedere l'autostop se 5 o 6 nemici diversi saltano fuori contemporaneamente! (Okay, che ne dici di spawning sbalorditivo?)
  • Stiamo eseguendo un solo tipo di operazione su tutte le particelle o tutte le operazioni su una singola particella? (Questo è un compromesso icache / dcache e la risposta non è sempre chiara.) Che ne dici di separare tutte le particelle e memorizzare le posizioni insieme (la famosa "struttura di matrici") rispetto a mantenere tutti i dati delle particelle in un unico posto (" matrice di strutture ").

Lo senti fino a quando non diventa odioso in qualsiasi corso di informatica a livello universitario, ma: si tratta davvero di strutture di dati e algoritmi. Trascorrere un po 'di tempo in algoritmo e progettazione del flusso di dati ti farà guadagnare di più per il dollaro in generale. (Assicurati di aver letto le eccellenti insidie ​​delle diapositive sulla programmazione orientata agli oggetti di un collega dei Servizi per gli sviluppatori Sony per avere un'idea qui.) Questo non "sembra" come ottimizzazione; è principalmente tempo impiegato con una lavagna o uno strumento UML o con la creazione di molti prototipi, piuttosto che rendere il codice corrente più veloce. Ma in genere è molto più utile.

E un'altra utile euristica: se sei vicino al "core" del tuo motore, potrebbe valere la pena di dedicare qualche sforzo e sperimentazione in più per l'ottimizzazione (es. Vettorializzare quei moltiplicatori di matrici!). Più lontano dal core, meno dovresti preoccuparti a meno che uno dei tuoi strumenti di profilazione non ti dica diversamente.


6
  1. Utilizzare le giuste strutture dati e algoritmi in anticipo.
  2. Non micro-ottimizzare fino a quando non si profila e non si conosce esattamente dove sono i punti caldi.
  3. Non preoccuparti di essere intelligente. Il compilatore fa già tutti i piccoli trucchi a cui stai pensando ("Oh! Devo moltiplicare per quattro! Sposterò a sinistra due!")
  4. Prestare attenzione ai mancati cache.

1
Affidarsi al compilatore è intelligente solo fino a un certo punto. Sì, farà alcune ottimizzazioni di spioncino a cui non penseresti (e non potresti fare a meno dell'assemblaggio), ma non ha idea di cosa dovrebbe fare il tuo algoritmo, quindi non può fare ottimizzazioni intelligenti. Inoltre, rimarrai sorpreso da quanti cicli puoi vincere implementando il codice critico in assembly o intrinseci .... se sai cosa stai facendo. I compilatori non sono così intelligenti come sono fatti per essere, non sanno cose che fai a meno che tu non le dica esplicitamente dappertutto (come usare 'restringere' religiosamente).
Kaj,

1
E ancora una volta devo commentare che se cerchi solo punti caldi ti perderai molti cicli perché non troverai alcun ciclo traboccante su tutta la linea (ad esempio smartpointer .... dereferenze ovunque, non comparire mai come hotspot perché effettivamente l'intero programma è un hotspot).
Kaj,

1
Sono d'accordo con entrambi i tuoi punti, ma mi occuperei della maggior parte di ciò sotto "usare le giuste strutture dati e algoritmi" Se passi dappertutto puntatori intelligenti contati di nuovo e stai scorrendo i cicli attraverso il conteggio, hai sicuramente scelto la struttura dei dati sbagliata.
munifico

5

Ricorda, tuttavia, anche "pessimizzazione precoce". Anche se non è necessario andare hardcore su ogni riga di codice, c'è la giustificazione per rendersi conto che stai effettivamente lavorando a un gioco, che ha implicazioni in tempo reale sulle prestazioni.
Mentre tutti ti dicono di misurare e ottimizzare i punti caldi, quella tecnica non ti mostrerà prestazioni perse in luoghi nascosti. Ad esempio, se ogni operazione '+' nel tuo codice impiegherà il doppio del tempo necessario, non verrà visualizzata come hot-spot e quindi non la ottimizzerai o addirittura realizzerai, tuttavia poiché viene utilizzata allo stesso tempo posto potrebbe costare un sacco di prestazioni. Sareste sorpresi da quanti di questi cicli fuoriescono senza mai essere rilevati. Quindi sii consapevole di ciò che fai.
A parte questo, tendo a profilare regolarmente per avere un'idea di cosa c'è e di quanto tempo rimane per frame. Per me il tempo per fotogramma è il più logico in quanto mi dice direttamente dove sono con obiettivi framerate. Prova anche a scoprire dove sono i picchi e cosa li provoca: preferisco un framerate stabile rispetto a un framerate alto con punte.


Questo mi sembra così sbagliato. Certo, il mio '+' può richiedere il doppio del tempo ogni volta che viene chiamato, ma questo conta davvero solo in un circuito ristretto. All'interno di un loop stretto, la modifica di un singolo "+" può fare ordini di grandezza più che la modifica di un "+" all'esterno del loop. Perché pensare a un decimo di microsecondo, quando un millisecondo può essere salvato?
Wilduck,

1
Quindi non capisci l'idea alla base della perdita di mantenimento. '+' (solo come esempio) viene chiamato centinaia di migliaia di volte per fotogramma, non solo in loop stretti. Se questo perde alcuni cicli ogni volta che hai perso molti cicli su tutta la linea, ma non verrà mai visualizzato come hotspot poiché le chiamate sono distribuite uniformemente sul tuo codebase / percorso di esecuzione. Quindi non stai parlando di un decimo di microsecondo, ma in effetti migliaia di decimi di microsecondi, sommando a più millisecondi. Dopo aver scelto la frutta bassa (anelli stretti) ho guadagnato millisecondi in questo modo più di una volta.
Kaj,

È come un rubinetto che gocciola. Perché preoccuparsi di salvare quella piccola goccia? - "Se il tuo rubinetto gocciola al ritmo di una goccia al secondo, puoi aspettarti di sprecare 2700 galloni all'anno".
Kaj,

Oh, suppongo che non fosse chiaro che intendevo quando l'operatore + era sovraccarico, quindi influirebbe su ogni "+" nel codice - in effetti non vorrai ottimizzare ogni "+" nel codice. Cattivo esempio, immagino ... lo intendevo come un esempio di "funzionalità di base che viene chiamata dappertutto dove l'implementazione potrebbe essere più lenta di quanto ipotizzato, specialmente se nascosta da un sovraccarico dell'operatore o da altri costrutti offuscati del C ++".
Kaj,

3

Una volta che un gioco è pronto per essere rilasciato (finale o beta), o è notevolmente lento, è probabilmente il momento migliore per creare un profilo della tua app. Ovviamente, puoi sempre eseguire il profiler in qualsiasi momento; ma sì, l'ottimizzazione prematura è la radice di tutti i mali. Ottimizzazione anche infondata; hai bisogno di dati reali per mostrare che un po 'di codice è lento, prima di provare a "ottimizzarlo". Un profiler lo fa per te.

Se non conosci un profiler, imparalo! Ecco un buon post sul blog dimostra l'utilità di un profiler.

La maggior parte dell'ottimizzazione del codice di gioco si riduce alla riduzione dei cicli della CPU necessari per ciascun frame. Un modo per farlo è semplicemente ottimizzare ogni routine mentre la scrivi e assicurarti che sia il più veloce possibile. Tuttavia, c'è un detto comune che il 90% dei cicli della CPU viene speso nel 10% del codice. Ciò significa che indirizzare tutto il lavoro di ottimizzazione su queste routine di collo di bottiglia avrà l'effetto 10 volte di ottimizzare tutto in modo uniforme. Quindi, come si identificano queste routine? La profilazione lo rende semplice.

Altrimenti, se il tuo piccolo gioco funziona a 200 FPS anche se ha un algoritmo inefficiente, hai davvero un motivo per ottimizzare? Dovresti avere una buona idea delle specifiche della tua macchina target e assicurarti che il gioco funzioni bene su quella macchina, ma qualsiasi cosa al di là di questo è (probabilmente) tempo perso che potrebbe essere meglio spendere codificando o lucidando il gioco.


Mentre la frutta bassa appesa tende a essere nel 10% del codice e viene facilmente catturata dalla profilazione alla fine, puramente lavorando sulla profilazione per questo ti farà perdere le routine che sono chiamate molto ma hanno solo un po ' un po 'di codice errato ciascuno - non verranno visualizzati nel tuo profilo ma sanguineranno molti cicli per chiamata. Si somma davvero.
Kaj,

@ Kaj, i bravi profilatori sommano tutte le centinaia di singole esecuzioni dell'algoritmo cattivo e ti mostrano il totale. Quindi dirai "Ma se avessi 10 metodi sbagliati e tutti chiamassero a 1/10 della frequenza?" Se passi tutto il tuo tempo su quei 10 metodi, ti perderai tutta la frutta bassa in cui otterrai un botto molto più grande per il tuo dollaro.
John McDonald,

2

Trovo utile creare profili. Anche se non stai ottimizzando attivamente, è bene avere un'idea di ciò che sta limitando le tue prestazioni in un dato momento. Molti giochi hanno una sorta di HUD sovrapponibile che mostra un semplice grafico (di solito solo una barra colorata) che mostra per quanto tempo le varie parti del ciclo di gioco stanno prendendo ogni frame.

Sarebbe una cattiva idea lasciare l'analisi delle prestazioni e l'ottimizzazione a una fase troppo tardiva. Se hai già creato il gioco e hai il 200% del budget della CPU e non riesci a trovarlo tramite l'ottimizzazione, sei fregato.

Devi sapere quali sono i budget per la grafica, la fisica, ecc., Mentre scrivi. Non puoi farlo se non hai idea di quale sarà la tua esibizione, e non puoi indovinarlo senza sapere sia quale sia la tua esibizione, sia quanto debole potrebbe esserci.

Quindi aggiungi alcune statistiche sulle prestazioni sin dal primo giorno.

Per quanto riguarda quando affrontare le cose - di nuovo, probabilmente è meglio non lasciarle troppo tardi, per non dover refactificare metà del motore. D'altra parte, non lasciarti coinvolgere troppo dall'ottimizzazione delle cose per eliminare ogni ciclo se pensi di poter cambiare completamente l'algoritmo domani o se non hai inserito dati di gioco reali.

Raccogli i frutti bassi mentre vai avanti, affronta periodicamente le cose grandi e dovresti andare bene.


Per aggiungere al profiler del gioco (con cui sono totalmente d'accordo), estendere il tuo profilo del gioco per visualizzare più barre (per più fotogrammi) ti aiuta a correlare il comportamento del gioco ai picchi e potrebbe aiutarti a trovare colli di bottiglia che non verranno visualizzati nella tua acquisizione media con un profiler.
Kaj,

2

Se guardiamo la citazione di Knuth nel suo contesto, continua spiegando che dovremmo ottimizzare ma con strumenti, come un profiler.

È necessario creare costantemente un profilo e un profilo di memoria dell'applicazione dopo aver creato l'architettura di base.

La profilazione non ti aiuterà solo ad aumentare la velocità, ma ti aiuterà a trovare i bug. Se il tuo programma cambia improvvisamente drasticamente la velocità, questo di solito è a causa di un bug. Se non stai profilando, potrebbe passare inosservato.

Il trucco per l'ottimizzazione è farlo in base alla progettazione. Non aspettare l'ultimo minuto. Assicurati che il design del tuo programma ti offra le prestazioni di cui hai bisogno senza davvero trucchi con loop interni.


1

Per il mio progetto, di solito applico alcune ottimizzazioni MOLTO necessarie nel mio motore di base. Ad esempio, mi piace sempre implementare una buona solida implementazione SIMD usando SSE2 e 3DNow! Questo assicura che la mia matematica in virgola mobile sia in sintonia con dove voglio che sia. Un'altra buona pratica è quella di prendere l'abitudine di ottimizzazioni mentre si codifica invece di tornare indietro. Il più delle volte queste piccole pratiche richiedono tanto tempo quanto ciò che stavi codificando comunque. Prima di codificare una funzione, assicurati di cercare il modo più efficiente per farlo.

In conclusione, secondo me, è più difficile rendere il tuo codice più efficiente dopo che è già stato fatto schifo.


0

Direi che il modo più semplice sarebbe usare il tuo buon senso - se qualcosa sembra rallentare, allora dai un'occhiata. Vedi se è un collo di bottiglia.
Usa un profiler per vedere le funzioni di velocità che stanno prendendo e quanto spesso vengono chiamate.
Non ha assolutamente senso ottimizzare o passare il tempo a cercare di ottimizzare qualcosa che non ne ha bisogno.


0

Se il tuo codice funziona lentamente, esegui un profiler e vedi cosa sta causando l'esecuzione più lenta. Oppure potresti essere proattivo e avere già un profiler in esecuzione prima di iniziare a notare problemi di prestazioni.

Dovrai ottimizzare quando il tuo framerate scende a un punto che il gioco inizia a soffrire. Il tuo colpevole più probabile sarà che la tua CPU sia stata utilizzata troppo (100%).


Direi che la GPU è altrettanto probabile della CPU. In effetti, a seconda di quanto siano strettamente accoppiati, è del tutto possibile essere fortemente legati alla CPU in metà del frame e pesantemente GPU nell'altra metà. La profilatura muta può persino mostrare un utilizzo inferiore al 100% su entrambi. Assicurati che la tua profilazione sia sufficientemente dettagliata per dimostrare che (ma non così fine da essere invadente!)
JasonD

0

Dovresti ottimizzare il tuo codice ... tutte le volte che è necessario.

Quello che ho fatto in passato è semplicemente eseguire il gioco continuamente con la profilazione attivata (almeno un contatore di framerate sullo schermo in ogni momento). Se il gioco sta rallentando (ad esempio sotto il framerate di destinazione sulla tua macchina con specifiche min.), Attiva il profiler e vedi se si presentano punti caldi.

A volte non è il codice. Molti dei problemi che ho incontrato in passato sono stati orientati alla gpu (scontato, questo era su iPhone). Problemi di riempimento, troppe chiamate di disegno, non raggruppamento di geometrie sufficienti, shader inefficienti ...

A parte algoritmi inefficienti per problemi difficili (ad esempio pathfinding, fisica), raramente ho riscontrato problemi in cui il codice stesso era il colpevole. E quei problemi difficili dovrebbero essere cose su cui spendi molto per ottenere l'algoritmo giusto e non preoccuparti di cose più piccole.


0

Per me è il migliore seguire un modello di dati ben preparato. E ottimizzazione prima del principale passo in avanti. Voglio dire prima di iniziare a implementare qualcosa di nuovo. Un altro motivo per l'ottimizzazione è quando sto perdendo il controllo delle risorse, l'app ha bisogno di molto carico della CPU / carico della GPU o memoria e non so perché :) o è troppo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.