Suggerimenti per il miglioramento del codice critico
Per scrivere codice che deve essere eseguito rapidamente è necessario comprendere tutti gli aspetti dell'applicazione e come essa interagisce con il sistema. Questo articolo suggerisce alternative ad alcune delle tecniche di codifica più ovvie per garantire che le parti critiche del codice vengano eseguite in modo soddisfacente.
In breve, per migliorare la scrittura di codice da eseguire rapidamente è necessario:
Sapere quali sono le parti del programma che devono essere eseguite velocemente.
Conoscere le dimensioni e la velocità di esecuzione del codice.
Conoscere l'impatto delle nuove funzionalità.
Sapere quali sono le operazioni minime che consentono di eseguire il processo.
Per ottenere informazioni sulle prestazioni del codice, è possibile usare Performance Monitor (perfmon.exe).
Sezioni incluse in questo articolo
Mancati riscontri nella cache e errori di pagina
Le richieste non soddisfatte dalla cache, sia interna che esterna, nonché gli errori di pagina (le chiamate a supporti di archiviazione secondari per istruzioni e dati dei programmi) rallentano le prestazioni di un programma.
Un riscontro nella cache della CPU può costare i cicli di clock di 10-20 del programma. Un hit della cache esterna può costare cicli di clock da 20 a 40. Un errore di pagina può costare un milione di cicli di clock (presupponendo un processore che gestisce 500 milioni di istruzioni al secondo e un'ora di 2 millisecondi per un errore di pagina). È quindi importante, per ottimizzare l'esecuzione del programma, scrivere codice che consenta di ridurre il più possibile il numero delle richieste non soddisfatte dalla cache e degli errori di pagina.
Uno dei motivi della lentezza di alcuni programmi è il fatto che si verifica un numero di errori di pagina o di richieste non soddisfatte dalla cache più alto del necessario. Per evitare questo problema, è importante usare strutture di dati con una buona località di riferimento, il che significa mantenere insieme gli elementi correlati. Talvolta una struttura di dati che sembra rappresentare la soluzione migliore non fornisce i risultati sperati proprio a causa del cattivo posizionamento dei riferimenti, e viceversa. Di seguito sono riportati due esempi:
Gli elenchi collegati allocati dinamicamente possono ridurre le prestazioni del programma. Quando si cerca un elemento o quando si attraversa un elenco alla fine, ogni collegamento ignorato potrebbe perdere la cache o causare un errore di pagina. Un'implementazione di elenco basata su matrici semplici potrebbe essere più veloce a causa di una migliore memorizzazione nella cache e un minor numero di errori di pagina. Anche se si consente il fatto che la matrice sarebbe più difficile da crescere, potrebbe essere ancora più veloce.
Le prestazioni di tabelle hash che usano elenchi collegati allocati in modo dinamico possono risultare ridotte. Per estensione, le tabelle hash che usano elenchi collegati allocati in modo dinamico per l'archiviazione dei contenuti possono subire un'ulteriore riduzione di prestazioni. Una semplice ricerca lineare all'interno di una matrice potrebbe, in ultima analisi e a seconda delle circostanze, risultare più rapida. L'uso di una tabella hash basata su matrice (cosiddetta "hashing chiuso") è un'implementazione spesso trascurata che presenta spesso prestazioni superiori.
Ordinamento e ricerca
In confronto ad altre operazioni comuni, l'ordinamento è un'operazione che richiede molto tempo. Il modo migliore per evitare rallentamenti non necessari è quello di non eseguire le operazioni di ordinamento nei momenti critici. Potrebbe essere possibile:
Rinviare l'ordinamento fino a un tempo non critico per le prestazioni.
Ordinare i dati in un momento precedente e non critico per le prestazioni.
Ordinare solo la parte di dati necessaria.
Talvolta è possibile creare un elenco già ordinato. Prestare attenzione perché, per l'inserimento di dati già ordinati potrebbe essere necessaria una struttura di dati più complessa con un posizionamento dei riferimenti non ottimale, che può causare richieste della cache non soddisfatte o errori di pagina. Non esiste un approccio che funziona in tutti i casi. È sempre necessario provare diversi approcci e valutarne le differenze.
Di seguito sono riportati alcuni suggerimenti generici per l'ordinamento:
Usare un ordinamento predefinito per ridurre al minimo i bug.
Per ridurre la complessità dell'ordinamento è sempre consigliabile eseguire in anticipo tutte le operazioni possibili. Se un passaggio monouso sui dati semplifica i confronti e riduce l'ordinamento da O(n log n) a O(n), quasi sicuramente verrà visualizzato in anticipo.
Tenere sempre presente la posizione dei riferimenti dell'algoritmo di ordinamento e i dati su cui questo verrà eseguito.
Per le ricerche sono disponibili meno alternative che per l'ordinamento. Se la velocità della ricerca è un fattore critico, una ricerca binaria o una ricerca in una tabella hash rappresenta quasi sempre il metodo preferibile, ma, come per l'ordinamento, è necessario tenere presente anche il problema della posizione dei dati. Una ricerca lineare tramite una matrice di piccole dimensioni può essere più veloce di una ricerca binaria tramite una struttura di dati con molti puntatori che causano errori di pagina o mancati riscontri nella cache.
Librerie MFC e classi
Le classi MFC (Microsoft Foundation Classes) possono semplificare notevolmente la scrittura del codice. Quando si scrive codice che deve essere eseguito rapidamente, è opportuno essere consapevoli dell'overhead specifico di alcune classi. Esaminare il codice MFC usato dal codice che deve essere eseguito rapidamente per verificare che soddisfi i requisiti richiesti. Nell'elenco seguente sono riportate le funzioni e le classi MFC che è opportuno conoscere:
CString
MFC chiama la libreria di runtime C per allocare memoria per un oggettoCString
in modo dinamico. In generale,CString
è efficiente come qualsiasi altra stringa allocata dinamicamente. Come le altre stringhe allocate in modo dinamico, infatti, presenta l'overhead dovuto all'allocazione dinamica e al conseguente rilascio. Una semplice matricechar
sullo stack spesso può servire al medesimo scopo e garantire maggiore velocità. Non usareCString
per archiviare una stringa costante. Utilizzare invececonst char *
. Qualsiasi operazione effettuata con un oggettoCString
presenta un determinato overhead. L'uso delle funzioni stringa della libreria di runtime potrebbe essere più veloce.CArray
UnCArray
oggetto offre flessibilità che una matrice regolare non ha, ma potrebbe non essere necessaria dal programma. Se si conoscono i limiti specifici della matrice, è possibile usare una matrice fissa globale. Se si usaCArray
, usareCArray::SetSize
per impostarne le dimensioni e specificare il numero di elementi che è possibile aggiungere quando sarà necessaria una riallocazione. In caso contrario, l'aggiunta di elementi potrebbe causare una frequente riallocazione e copia della matrice, con conseguente riduzione delle prestazioni e frammentazione della memoria. Inoltre, se si inserisce un elemento in una matrice,CArray
sposta gli elementi successivi in memoria e potrebbe essere necessario aumentare la matrice. Queste operazioni possono causare richieste non soddisfatte dalla cache ed errori di pagina. Attraverso un esame preventivo del codice MFC ci si potrebbe quindi rendere conto della necessità di scrivere codice più specifico in base alle proprie esigenze al fine di ottenere prestazioni migliori. Dal momento che, ad esempio,CArray
è un modello, sarà possibile fornire specificheCArray
più particolari per i singoli casi.CList
CList
è un elenco collegato doubly, quindi l'inserimento di elementi è veloce in corrispondenza della testa, della coda e in una posizione nota (POSITION
) nell'elenco. La ricerca di un elemento in base al valore o all'indice richiede tuttavia una ricerca sequenziale che può essere lenta se l'elenco è lungo. Se il codice non richiede un elenco collegato doubly, è consigliabile riconsiderare l'uso diCList
. L'uso di un elenco collegato singly consente di risparmiare il sovraccarico dell'aggiornamento di un altro puntatore per tutte le operazioni e la memoria per tale puntatore. La memoria aggiuntiva non è grande, ma è un'altra opportunità per errori di cache o di pagina.IsKindOf
Questa funzione può generare molte chiamate e può accedere alla memoria in aree dati diverse, causando una cattiva località di riferimento. È utile per una compilazione di debug ,ad esempio in una chiamata ASSERT, ma provare a evitare di usarla in una build di versione.PreTranslateMessage
UtilizzarePreTranslateMessage
quando un albero specifico di finestre richiede tasti di scelta rapida diversi o quando è necessario inserire la gestione dei messaggi nel message pump.PreTranslateMessage
modifica i messaggi di invio MFC. Eseguire l'override diPreTranslateMessage
se necessario, solo al livello richiesto. Ad esempio, non è necessario eseguire l'overrideCMainFrame::PreTranslateMessage
se si è interessati solo ai messaggi destinati agli elementi figlio di una visualizzazione specifica. Eseguire invece l'override diPreTranslateMessage
per la classe di visualizzazione.Non aggirare il normale percorso di invio usando
PreTranslateMessage
per gestire qualsiasi messaggio inviato a qualsiasi finestra. A tale scopo, usare le procedure di finestra e le mappe messaggi MFC.OnIdle
Gli eventi inattivi possono verificarsi a volte non previsti, ad esempio traWM_KEYDOWN
eWM_KEYUP
eventi. I timer possono rappresentare un modo più efficace per attivare il codice. Non forzareOnIdle
la chiamata ripetutamente generando messaggi falsi o restituendoTRUE
sempre da un override diOnIdle
, che non consentirebbe mai al thread di dormire. Anche in questo caso sarebbe più appropriato usare un timer o un thread separato.
Librerie condivise
Riutilizzare il codice può essere utile. Tuttavia, se si intende usare il codice di un altro utente, è necessario assicurarsi di sapere esattamente cosa fa in questi casi in cui le prestazioni sono fondamentali per l'utente. Il modo migliore per comprenderlo consiste nell'scorrere il codice sorgente o misurare con strumenti come PView o Monitor prestazioni.
Cumuli
È consigliabile ricorrere all'utilizzo di più heap solo in casi particolari. Gli heap aggiuntivi creati con HeapCreate
e HeapAlloc
consentono di gestire e quindi eliminare un set di allocazioni correlato. Non impegnare una quantità eccessiva di memoria. Se si usano più heap, prestare particolare attenzione alla quantità di memoria inizialmente impegnata.
Anziché più heap, è possibile usare funzioni di supporto come collegamento tra il codice e l'heap predefinito. Le funzioni di supporto facilitano eventuali strategie di allocazione personalizzate che possono migliorare le prestazioni dell'applicazione. Se ad esempio si eseguono frequentemente piccole allocazioni, sarà possibile confinare queste allocazioni a una parte dell'heap predefinito. È possibile allocare un blocco di memoria di grandi dimensioni, quindi usare una funzione di supporto per effettuare sottoallocazioni da tale blocco. Quindi non si avranno più heap con memoria inutilizzata, perché l'allocazione esce dall'heap predefinito.
In alcuni casi, tuttavia, il ricorso all'heap predefinito può ridurre la vicinanza dei riferimenti. Usare Process Viewer, Spy++ o Performance Monitor per misurare gli effetti dello spostamento degli oggetti da un heap all'altro.
Misurare gli heap in modo da controllare ogni allocazione su di essi. Usare le routine dell'heap di debug di runtime C per checkpoint e dump dell'heap. È possibile esportare l'output in un programma di foglio di calcolo, ad esempio Microsoft Excel, e usare tabelle pivot per visualizzare i risultati. Verificare il numero totale, la dimensione e la distribuzione delle allocazioni. Confrontare questi risultati con le dimensioni dei set di lavoro. Esaminare inoltre i cluster degli oggetti di dimensioni correlate.
È anche possibile usare i contatori delle prestazioni per monitorare l'utilizzo della memoria.
Threads
Per le attività eseguite in background, un'efficace gestione dell'inattività degli eventi può risultare più veloce rispetto all'utilizzo dei thread. È più facile comprendere i concetti relativi alla posizione dei riferimenti in un programma a thread singolo.
In genere è buona norma usare un thread solo se la notifica del sistema operativo in base a cui si esegue il blocco costituisce la base delle attività in background. I thread sono la soluzione migliore in questo caso perché è poco pratico bloccare un thread principale in un evento.
I thread presentano inoltre problemi di comunicazione. È necessario gestire il collegamento di comunicazione tra i thread mediante un elenco di messaggi oppure allocando e usando la memoria condivisa. La gestione del collegamento di comunicazione richiede in genere una sincronizzazione al fine di evitare race condition e problemi di deadlock. Questa complessità può facilmente determinare bug e problemi di prestazioni.
Per altre informazioni, vedere Elaborazione ciclo inattiva e multithreading.
Piccolo working set
I working set di dimensioni inferiori sono caratterizzati da posizionamento ottimale dei riferimenti, un numero inferiore di errori di pagina e un maggior numero di richieste soddisfatte dalla cache. Il working set del processo è la metrica più precisa fornita direttamente dal sistema operativo per valutare la posizione dei riferimenti.
Per impostare i limiti superiori e inferiori del working set, usare
SetProcessWorkingSetSize
.Per ottenere i limiti superiori e inferiori del working set, usare
GetProcessWorkingSetSize
.Per visualizzare le dimensioni del working set, usare Spy++.