Ottimizzare C++/Scrivere codice C++ efficiente: differenze tra le versioni
Contenuto cancellato Contenuto aggiunto
Nessun oggetto della modifica |
Nessun oggetto della modifica |
||
Riga 3:
Tali linee-guida potrebbero non dare alcun vantaggio prestazionale, ma molto probabilmente non danno neanche svantaggi, e quindi le si può applicare senza preoccuparsi del loro impatto. Si consiglia di abituarsi ad adottare sempre tali linee-guida, fin dalla prima stesura, anche nel codice che non ha particolari requisiti di efficienza.
==
===
Tale controllo viene già fatto da ogni implementazione di delete conforme allo standard, e quindi sarebbe ridondante.
===
Le prime due funzioni richiedono un puntatore a funzione, mentre le seconde richiedono un oggetto-funzione (o un'espressione lambda). I puntatori a funzione spesso non sono espansi inline e sono quindi meno efficienti.
===
Questa indicazione equivale a specificare l'argomento implicito this come puntatore a const. Per l'uso di const negli argomenti di funzione, vedi la linea-guida 3.3.5.
===
Un enumerato è implementato come un intero. Tipicamente, rispetto ad un intero, una stringa occupa più spazio, ed è più lenta da copiare e da confrontare.
===
I compilatori possono sfruttare la regolarità di tale istruzione per applicare alcune ottimizzazioni, in particolare se viene applicata la linea-guida 3.1.11.
===
In fase di ottimizzazione sarà così possibile scegliere l’implementazione più efficiente per la struttura dati, senza dover modificare molto codice.
Riga 31:
I contenitori STL attuano già parzialmente questo principio, ma alcune operazioni sono disponibili solo per alcuni contenitori.
===
Per esempio, chiama a.empty() invece di a.size() == 0, chiama iter != a.end() invece di iter < a.end().
Riga 37:
Purtroppo, non è sempre possibile scrivere del codice egualmente efficiente e valido per ogni tipo di contenitore. Tuttavia, riducendo il numero di istruzioni dipendenti dal tipo di contenitore, se in fase di ottimizzazione si dovesse sostituire il contenitore con un'altro, si dovrà modificare meno codice.
===
Gli algoritmi di STL sono già dei cicli ottimizzati per gli specifici contenitori, ed evitano il rischio di introdurre operazioni inefficienti.
Riga 45:
Senza avere a disposizione la funzionalità lambda, nella maggior parte dei casi è troppo scomodo usare gli algoritmi STL perché ne valga la pena; invece, usando le espressioni lambda, si possono scrivere corpi arbitrari per tali cicli.
===
Per insiemi fino a 8 elementi, il vector è il contenitore a lunghezza variabile più efficiente per qualunque operazione.
Riga 51:
Per insiemi più grandi, altri contenitori possono diventare gradualmente più efficienti a seconda delle operazioni, ma il vector rimane quello che ha minore occupazione di memoria (purché non ci sia capacità in eccesso), minor tempo di scansione completa, e maggiore località di riferimento.
===
Le funzioni inline non hanno il costo della chiamata di funzione, che è tanto più grande quanti più sono gli argomenti della funzione. Inoltre, dato che il codice è consecutivo con quello del chiamante, hanno migliore località di riferimento. Infine, dato che il codice intermedio delle funzioni espanse inline si fonde con quello del chiamante, tale codice può essere facilmente ottimizzato dal compilatore.
Riga 61:
Tra le routine non piccolissime, solo quelle critiche per la velocità verranno rese inline in fase di ottimizzazione.
===
I compilatori ottimizzanti, quando compilano un’istruzione switch i cui valori dei casi comprendono la maggior parte dei valori interi compresi in un intervallo, invece di generare una sequenza di istruzioni if, generano una jump-table, ossia un array degli indirizzi in cui inizia il codice di ogni caso, e quando eseguono lo switch usano tale tabella per saltare al codice associato al caso.
Riga 109:
fine:
===
Se il compilatore non genera la jump-table, i casi vengono confrontati in ordine di comparizione, per cui nei casi più tipici vengono fatti meno confronti.
===
Esempio: Invece del seguente codice:
Riga 130:
In tal modo, i dati da elaborare insieme sono più vicini tra di loro in memoria, e questo permette di ottimizzare l'uso della cache dei dati e di indirizzare tali dati con istruzioni più compatte che quindi ottimizzano l'uso della cache del codice.
===
Se una funzione riceve pochi argomenti, questi vengono posti direttamente nei registri e quindi il passaggio è molto veloce; ma se non sono pochi, gli argomenti devono essere posti nello stack, anche se sono gli stessi della precedente chiamata alla stessa funzione.
Riga 155:
}
===
I tipi “int” e “unsigned int” sono per definizione quelli più efficienti su qualunque piattaforma. I tipi double sono efficienti quanto i float, ma sono più precisi. Tuttavia, nel caso di interi contenuti in array medi o grandi, è meglio minimizzare la dimensione in byte dell’array. Per esempio, per memorizzare un numero intero che può essere compreso tra -1000 e 1000, si può usare uno “short”, mentre per memorizzare un numero intero che può essere compreso tra 0 e 200, si può usare un “unsigned char”.
Riga 161:
I bit-field contribuirebbero a minimizzare la dimensione in byte dell’array, ma la loro elaborazione introduce un rallentamento che potrebbe essere eccessivo, per cui andrebbero introdotti solo in fase di ottimizzazione.
===
Se è stata creata una tale funzione membro, è solo perché è più efficiente dell'algoritmo STL generico.
===
Dato che tutti i citati algoritmi usano la ricerca binaria, sono più veloci dell'algoritmo find, che usa la ricerca sequenziale
==
Rispetto al linguaggio C, il linguaggio C++ aggiunge alcuni costrutti, il cui utilizzo peggiora l'efficienza.
Riga 177:
Altri costrutti sono alquanto inefficienti, e devono quindi essere usati con grande parsimonia.
===
Il sollevamento di una eccezione ha un costo molto elevato, dell’ordine dei 10000 cicli di processore. Se tale operazione viene effettuata solamente ogni volta che un messaggio viene mostrato all’utente o scritto in un file di log, si ha la garanzia che non verrà eseguita troppo spesso. Se invece la si effettua come operazione algoritmica, anche se pensata inizialmente per essere eseguita raramente, potrebbe finire per essere eseguita frequentemente.
===
Le funzioni membro delle classi base derivate in modo virtual sono un po’ più lente delle funzioni membro derivate in modo non-virtual.
Riga 205:
Questa situazione è l'unica in cui è necessaria la derivazione virtual.
===
A parità di condizioni, le classi che contengono almeno una funzione membro virtual occupano un po’ più spazio delle classi che non ne contengono, e gli oggetti delle classi che contengono almeno una funzione membro virtual occupano un po’ più spazio e la loro costruzione richiede un po’ più di tempo rispetto agli oggetti di classi che non contengono funzioni membro virtual. Le funzioni membro virtual occupano po’ più spazio e sono un po’ più lente da chiamare delle funzioni membro non-virtual.
===
In altre parole, dichiara static tutte le funzioni membro che puoi.
Riga 215:
In questo modo, non viene passato l’argomento implicito this.
===
I template di classe, ogni volta che vengono istanziati, producono una copia del codice oggetto, e se contengono funzioni virtuali producono una copia della vtable e della RTTI. Questi dati ingrandiscono eccessivamente il programma.
===
L’uso più tipico dell’operatore delete sia ha quando in un distruttore si dealloca un oggetto posseduto dall’oggetto in corso di distruzione; in tale contesto, si chiama delete su una variabile membro di tipo puntatore. Dato che solitamente un distruttore non è così complicato da faticare a capire che cosa è già stato distrutto dell’oggetto corrente e che cosa non ancora, e dato che il puntatore usato per la chiamata a delete cesserà di esistere alla fine del distruttore, anche se si lascia il valore corrente del puntatore, non si corre il rischio di deallocarlo una seconda volta. D’altra parte, annullare il puntatore richiede una seppur piccolissima quantità di tempo.
===
La garbage collection, cioè il recupero automatico della memoria non più referenziata, fornisce la comodità di non doversi occupare della deallocazione della memoria, e previene i memory leak. Tale funzionalità non viene fornita dalla libreria standard, ma viene fornita da librerie non-standard. Tuttavia, tale tecnica di gestione della memoria offre prestazioni peggiori della deallocazione esplicita.
Riga 231:
Normalmente bisognerebbe, in fase di progettazione, cercare di assegnare ogni oggetto ad un proprietario, che avrà la responsabilità di distruggerlo. Quando tale assegnazione è difficile, in quanto più oggetti tendono a rimpallarsi la responsabilità di distruggere un oggetto, allora è opportuno usare uno smart-pointer con reference-count oopure una libreria di garbage-collection.
==
===
Dichiarare una variabile il più tardi possibile, significa sia dichiararla nell’ambito più stretto possibile, sia dichiararla il più avanti possibile entro quell’ambito. Essere nell’ambito più stretto possibile comporta che se tale ambito non viene mai eseguito, l'oggetto associato alla variabile non viene mai costruito né distrutto. Essere il più avanti possibile all’interno di un ambito comporta che se prima di tale dichiarazione c’è un’uscita prematura, tramite return o break o continue, l’oggetto associato alla variabile non viene mai costruito né distrutto.
Riga 239:
Inoltre, spesso all'inizio di una routine non si ha un valore appropriato per inizializzare l'oggetto associato alla variabile, e quindi si è costretti a inizializzarla con un valore di default, e poi assegnarle il valore appropriato. Se invece la si dichiara quando si ha a disposizione il valore appropriato, la si può inizializzare con tale valore senza bisogno di fare un successivo assegnamento.
===
Se un oggetto di classe non viene inizializzato esplicitamente, viene comunque inizializzato automaticamente dal costruttore di default. In generale, chiamare il costruttore di default seguito da un assegnamento di un valore è meno efficiente o ugualmene efficiente che chiamare solo un costruttore con tale valore.
===
Se l’oggetto incrementato è di un tipo fondamentale, non ci sono differenze tra le due forme, ma se si tratta di un tipo composito, l’operatore postfisso comporta la creazione di un inutile oggetto temporaneo e quello prefisso no.
Riga 251:
Se invece il valore dell’espressione formata dall’operatore di incremento o decremento viene usata in un’espressione più grande, potrebbe essere opportuno usare l’operatore postfisso.
===
Tipicamente un operatore infisso, come nell’espressione “a + b”, crea un oggetto temporaneo. Per esempio, nel seguente codice, gli operatori “+” creano stringhe temporanee, la cui creazione e distruzione richiede tempo:
Riga 267:
in quanto l’operatore += non crea oggetti temporanei.
===
In particolare, quando devi passare un argomento x di tipo T a una funzione usa il seguente criterio:
Se x è un argomento di solo input,
Riga 288:
Un oggetto composito veloce da copiare potrebbe essere efficientemente passato per valore, ma, a meno che si tratti di un iteratore o di un oggetto-funzione, per i quali si assume l’efficienza della copia, tale tecnica è rischiosa, in quanto l’oggetto potrebbe diventare in futuro più lento da copiare. Per esempio, se un oggetto di classe Point contiene solo due float, potrebbe essere efficientemente passato per valore; ma se in futuro si aggiunge un terzo float, o se i float diventano double, potrebbe diventare più efficiente il passaggio per riferimento.
===
I costruttori impliciti possono essere chiamati automaticamente dal compilatore che esegue una conversione automatica. A seconda della complessità del costruttore, tale chiamata può richiedere molto più tempo del necessario. Rendendo obbligatoriamente esplicita tale conversione, il compilatore potrebbe scegliere un’altra funzione in overload, evitando così di chiamare il costruttore, oppure segnalare errore e costringere il programmatore a scegliere un’altra strada per evitare la chiamata al costruttore.
Riga 294:
Per i costruttori di copia delle classi concrete si deve fare eccezione, per consentirne il passaggio per valore. Per le classi astratte, anche i costruttori di copia possono essere dichiarati explicit, in quanto, per definizione, le classi astratte non si possono istanziare, e quindi gli oggetti di tale tipo non dovrebbero mai essere passati per valore.
===
Gli operatori di conversioni consentono conversioni implicite, e quandi incorrono nello stesso problema dei costruttori impliciti.
Riga 300:
Se tali conversioni sono necessarie, fornisci invece una funzione membro equivalente, che può essere chiamata solo esplicitamente.
===
L'idioma Pimpl consiste nel memorizzare nell'oggetto solamente un puntatore alla struttura che contiene tutte le informazioni utili di tale oggetto.
Riga 308:
Tale idioma consente anche di velocizzare alcune operazioni, come lo swap tra due oggetti, ma in generale rallenta gli accessi ai dati dell'oggetto a causa del livello di indirettezza, e provoca un'allocazione aggiuntiva per ogni creazione e copia di tale oggetto. Quindi non dovrebbe essere usato per classi le cui funzioni membro pubblche sono chiamate frequentemente.
===
Gli algoritmi di STL passano tali oggetti per valore. Pertanto, se la loro copia non è estremamente efficiente, gli algoritmi STL diventano lenti.
==
L’allocazione e la deallocazione di memoria dinamica sono operazioni molto lente, se confrontate con l’allocazione e la deallocazione di memoria automatica, cioè su stack.
Riga 324:
Tuttavia, qui si presenteranno regole per ridurre il numero di allocazioni di memoria per un dato numero di chiamate all'operatore new.
===
I vector memorizzano i dati in un buffer allocato dinamicamente, mentre le altre soluzioni proposte allocano i dati nell’oggetto stesso. Questo consente di evitare allocazioni/deallocazioni di memoria dinamica e di favorire la località dei dati. Se l’array è medio o grande, tali vantaggi diminuiscono, e invece risulta più importante evitare di usare troppo spazio sullo stack.
===
Un allocatore a pool alloca blocchi di memoria e fornisce servizi di allocazione/deallocazione di blocchi più piccoli di dimensione costante, offrendo alta velocità di allocazione/deallocazione, bassa frammentazione della memoria, uso efficiente delle cache dei dati e della memoria virtuale, grazie alla località dei dati. In particolare, un allocatore di questo tipo migliora notevolmente le prestazioni dei contenitori standard “list”, “set”, “multi_set”, “map”, e “multi_map”. Se la tua implementazione della libreria standard non usa già un allocatore a pool per questi contenitori, dovresti procurartene uno (per esempio, questo: http://www.codeproject.com/KB/stl/blockallocator.aspx), e specificarlo come parametro di template per le istanze di tali template di contenitori.
===
La funzione push_back garantisce un tempo lineare ammortizzato, in quanto, nel caso del vector, ingrandisce esponenzialmente la capacità.
Riga 340:
La funzione insert permette di inserire in modo ottimizzato un'intera sequenza, e quindi una chiamata di questo tipo è più veloce di numerose chiamate a push_back.
==
===
La cache dei dati ottimizza gli accessi alla memoria in ordine sequenziale crescente. Quando si itera su un array multidimensionale, il loop più interno deve iterare sull’ultimo indice. In tal modo, è garantito che le celle vengono elaborate nell’ordine in cui si trovano in memoria. Per esempio:
Riga 354:
}
===
I compilatori attivano di default un criterio di allineamento dei tipi fondamentali, per cui le variabili di ogni tipo possono iniziare solo a determinati indirizzi di memoria. Tale criterio solitamente garantisce le massime prestazioni, ma può introdurre degli spazi inutilizzati tra le variabili. Se per alcune strutture è necessario eliminare tali spazi, usa le direttiva “pragma” per confinare tale impaccamento alle sole strutture per cui è necessario.
===
In tal modo, il codice macchina prodotto compilando tali routine e i dati statici a cui accederà avranno indirizzi vicini tra loro.
==
===
In tal modo, il thread principale si occupa di gestire l’interfaccia utente ed è pronta per rispondere ad altri comandi. Assegnando al thread di calcolo priorità più bassa del normale, l’interfaccia utente rimane veloce quasi come se non ci fosse un’elaborazione in corso.
===
In tal modo ogni core può elaborare un thread. Se i thread di calcolo fossero più dei processori, ci sarebbe contesa tra i thread, e questo rallenterebbe l’elaborazione. Il thread di interfaccia utente non rallenta, in quanto è pressoché inattivo.
===
Le tecniche per rendere thread-safe una libreria possono dover usare memoria e tempo. Se non usi i thread, evita di pagarne il costo.
===
Le tecniche per rendere thread-safe una libreria possono dover usare memoria e tempo. Se gli utenti della tua libreria non usano i thread, evita di fargliene pagare il costo.
===
Le primitive di mutua esclusione richiedono tempo.
|