Ottimizzare C++/Scrivere codice C++ efficiente: differenze tra le versioni
Contenuto cancellato Contenuto aggiunto
Nessun oggetto della modifica |
|||
Riga 5:
== Come approfittare dei costrutti C++ che migliorano le prestazioni ==
=== Operatore
'''Non verificare che un puntatore sia non-nullo prima di chiamare <code>delete</code> su di esso.'''
Tale controllo viene già fatto da ogni implementazione di <code>delete</code> conforme allo standard, e quindi sarebbe ridondante.
=== Funzioni
'''Invece delle funzioni
Le prime due funzioni richiedono necessariamente un puntatore a funzione, mentre le seconde
I puntatori a funzione spesso non sono espansi inline e sono quindi meno efficienti degli oggetti-funzione.
=== Funzioni membro
'''Dichiara <code>const</code> ogni funzione membro che non modifica lo stato dell'oggetto a cui è applicata.'''
Ogni funzione membro riceve l'argomento implicito
La linea-guida 3.3.5 consiglia di usare lo specificatore <code>const</code> negli argomenti di funzione.
=== Rappresentazione di simboli ===
Line 33 ⟶ 34:
Tipicamente, rispetto ad un intero, una stringa occupa più spazio, ed è più lenta da copiare e da confrontare.
=== Istruzioni
Se devi confrontare un valore intero con una serie di valori costanti, invece di una serie di istruzioni
I compilatori possono sfruttare la regolarità di tale istruzione per applicare alcune ottimizzazioni, in particolare se viene applicata la linea-guida 3.1.11.
Line 44 ⟶ 45:
In fase di progettazione è difficile decidere quale struttura dati avrà prestazioni ottimali nell'uso effettivo dell'applicazione.
In fase di ottimizzazione ci si può accorgere che cambiando il tipo di un contenitore, per esempio passando da
Tuttavia, tale modifica comporta la modifica di gran parte del codice sorgente che utilizza direttamente il contenitore che ha cambiato di tipo.
Line 56 ⟶ 57:
'''Nell'uso dei contenitori STL, a parità di prestazioni, fa' in modo di rendere intercambiabile il contenitore.'''
Per esempio, chiama <code>a.empty()</code> invece di <code>a.size() == 0</code>, e chiama <code>iter != a.end()</code> invece di <code>iter < a.end()</code>.
Purtroppo, non è sempre possibile scrivere del codice egualmente efficiente e valido per ogni tipo di contenitore.
Line 63 ⟶ 64:
=== Empressioni lambda ===
'''Invece di scrivere un ciclo <code>for</code> su un contenitore STL, usa un algoritmo di STL con un'espressione lambda (usando Boost o C++0x).'''
Gli algoritmi di STL sono già dei cicli ottimizzati per gli specifici contenitori, ed evitano il rischio di introdurre operazioni inefficienti.
Line 73 ⟶ 74:
=== Scelta del contenitore di default ===
'''Se devi scegliere un contenitore a lunghezza variabile, e sei incerto su quale contenitore scegliere, usa un
Per insiemi fino a 8 elementi, il <code>vector</code> è il contenitore a lunghezza variabile più efficiente per qualunque operazione.
Per insiemi più grandi, altri contenitori possono diventare gradualmente più efficienti a seconda delle operazioni, ma il <code>vector</code> 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.
=== Funzioni espanse
'''Se usi compilatori che consentono l'ottimizzazione dell'intero programma e l'espansione
Le funzioni espanse
Le funzioni molto piccole, cioè un semplice assegnamento o una semplice istruzione
Tuttavia, ogni volta che viene espansa
Tra le routine non piccolissime, solo quelle critiche per la velocità verranno rese ''inline'' in fase di ottimizzazione.
=== Valori dei casi di istruzioni
'''Come costanti per i casi delle istruzioni
I compilatori ottimizzanti, quando compilano un'istruzione
Per esempio, il seguente codice C++:
Line 150 ⟶ 151:
</source>
Per così pochi casi, probabilmente non ci sono molte differenze, ma con l'aumentare del numero di casi la primo codice è più efficiente in quanto esegue un solo ''goto calcolato'' invece di una cascata di
=== Ordine dei casi
'''Nelle istruzioni
Se il compilatore non generasse la ''jump-table'', i casi verrebbero confrontati in ordine di comparizione, per cui nei casi più tipici verrebbero fatti meno confronti.
=== Raggruppamento di più array in un array di strutture ===
Line 217 ⟶ 218:
=== Uso dei tipi più efficienti ===
'''Per memorizzare in un oggetto dei numeri interi, usa il tipo <code>int</code> o il tipo <code>unsigned int</code>, a meno che sia necessario un tipo più lungo; per memorizzare dei caratteri usa il tipo <code>char</code>,
I tipi <code>int</code> e <code>unsigned int</code> sono per definizione quelli più efficienti su qualunque piattaforma.
Line 225 ⟶ 226:
Alcuni tipi di processore elaborano più velocemente gli oggetti di tipo <code>signed char</code>, mentre altre elaborano più velocemente gli oggetti di tipo <code>unsigned char</code>.
Pertanto, in C e in C++ esiste il tipo <code>char</code> che corrisponde al tipo di carattere elaborato più velocemente sul processore target.
Il tipo <code>char</code> può contenere solo piccoli insiemi di caratteri; tipicamente fino a un massimo di 255 caratteri distinti.
Per memorizzare set di caratteri più grandi, si deve ricorrere al tipo <code>wchar_t</code>, che ovviamente è meno efficiente.
Nel caso di interi contenuti in array medi o grandi, o in collezioni che si presume saranno tipicamente medie o grandi, è meglio minimizzare la dimensione in byte della collezione.
Line 238 ⟶ 242:
Se è stata creata una tale funzione membro specifica quando esisteva già un algoritmo STL generico, è solo perché tale funzione membro è più efficiente.
Per esempio, per cercare in un oggetto
=== Ricerca in sequenze ordinate ===
'''Per cercare un elemento in una sequenza ordinata, usa gli algoritmi
Dato che tutti i citati algoritmi usano la ricerca binaria di complessità logaritmica (O(log(n))), sono più veloci dell'algoritmo
== Come evitare i costi di costrutti C++ che peggiorano le prestazioni ==
Line 254 ⟶ 258:
Altri costrutti sono alquanto inefficienti, e devono quindi essere usati con grande parsimonia.
===
'''Chiama l'
Il sollevamento di una eccezione ha un costo molto elevato, dell'ordine dei 10000 cicli di processore.
Line 262 ⟶ 266:
Se invece la si effettua come operazione algoritmica, anche se pensata inizialmente per essere eseguita raramente, potrebbe finire per essere eseguita frequentemente.
=== Derivazione
'''Non usare sistematicamente la derivazione
Le funzioni membro delle classi base derivate in modo
Per esempio, considera le seguenti definizioni di classe:
Line 278 ⟶ 282:
Con tali definizioni, ogni oggetto di classe C contiene due oggetti distinti di classe A, uno facente parte della classe base B1, e l'altro facente parte della classe base B2.
Questo non costituisce un problema se la classe A non ha nessuna variabile membro non-<code>static</code>.
Se invece tale oggetto di classe A contiene qualche variabile membro, e si intende che debba essere unico per ogni oggetto di classe C, si deve usare la derivazione <code>virtual</code>, nel seguente modo:
<source lang=cpp>
Line 289 ⟶ 293:
</source>
Questa situazione è l'unica in cui è necessaria la derivazione
=== Funzioni membro
'''In ogni classe, non definire sistematicamente
A parità di condizioni, le classi che contengono almeno una funzione membro
Le funzioni membro ''virtual'' occupano po' più spazio e sono un po' più lente da chiamare delle funzioni membro non-''virtual''.▼
▲Le funzioni membro
=== Funzioni membro ''static'' ===▼
'''In
In altre parole, dichiara <code>static</code> tutte le funzioni membro che puoi.
In questo modo, non viene passato l'argomento implicito ''this''.▼
=== Template di classi polimorfiche ===
Line 312 ⟶ 317:
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.
=== Annullamento dell'argomento di
'''Non annullare un puntatore dopo aver chiamato
L'uso più tipico dell'operatore
Dato che solitamente un distruttore non è così complicato da faticare a capire quali parti dell'oggetto corrente sono già state distrutte e quali non ancora, e dato che il puntatore usato per la chiamata a
D'altra parte, annullare il puntatore richiede una seppur piccolissima quantità di tempo.
Come tecnica di collaudo o di debugging, si
=== Uso di deallocatori automatici ===
Line 328 ⟶ 333:
'''Non usare una libreria di garbage-collection né gli smart-pointer con reference-count (boost::shared_ptr), a meno che se ne dimostri l’opportunità per il caso specifico.'''
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. La libreria standard del C++98 contiene un solo smart-pointer, l'
Normalmente bisognerebbe, in fase di progettazione, cercare di assegnare ogni oggetto ad un proprietario, che avrà la responsabilità di distruggerlo.
Solo quando tale assegnazione è difficile, in quanto più oggetti tendono a rimpallarsi la responsabilità di distruggere un oggetto, risulta opportuno usare uno smart-pointer con reference-count oppure una libreria di garbage-collection.
=== Il modificatore
'''Non definire sistematicamente
L'uso del modificatore
Questo garantisce che tutti i dispositivi e tutti i thread ''vedano'' la stessa variabile, ma rende molto più lente le operazioni che manipolano tale variabile.
== Come evitare
=== Ambito delle variabili ===
Line 350 ⟶ 356:
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
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.
Line 360 ⟶ 366:
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
=== Operatori di incremento/decremento ===
'''Usa gli operatori prefissi di incremento (<code>++</code>) e decremento (<code>--</code>) invece dei corrispondenti operatori postfissi, se il valore dell'espressione non viene usato.'''
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, mentre l'operatore prefisso no.
Line 374 ⟶ 380:
=== Operatori compositi di assegnamento ===
'''Usa gli operatori compositi di assegnamento (come in
Tipicamente un operatore semplice, come nell'espressione ''a + b'', crea un oggetto temporaneo.
Line 395 ⟶ 401:
=== Passaggio di argomenti alle funzioni ===
'''Quando devi passare un argomento
Se
se
passalo per puntatore a costante (
altrimenti, se
passalo per valore (
altrimenti,
passalo per riferimento a costante (
altrimenti, cioè se
se
passalo per puntatore a non-costante (
altrimenti,
passalo per riferimento a non-costante (
Il passaggio per riferimento è più efficiente del passaggio per puntatore in quanto facilita al compilatore l’eliminazione della variabile, e in quanto il chiamato non deve verificare se il riferimento è valido o nullo; tuttavia, il puntatore ha il pregio di poter rappresentare un valore nullo, ed è più efficiente passare solo un puntatore, che un riferimento a un oggetto insieme a un booleano che indica se tale riferimento è valido.
Per oggetti che possono essere contenuti in uno o due registri, il passaggio per valore è più efficiente o ugualmente efficiente del passaggio per riferimento, in quanto tali oggetti possono essere contenuti in registri e non hanno livelli di indirettezza, pertanto questo è il modo più efficiente di passare oggetti sicuramente piccoli, come i tipi fondamentali, gli iteratori e gli oggetti-funzione.
Per oggetti più grandi di due registri, il passaggio per riferimento è più efficiente del passaggio per valore, in quanto tali oggetti non devono essere copiati. 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 <code>Point</code> contiene solo due <code>float</code>, potrebbe essere efficientemente passato per valore; ma se in futuro si ===
'''Dichiara
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
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
=== Operatori di conversione ===
'''Non dichiarare operatori di conversione, se non per mantenere la compatibilità con una libreria obsoleta (in C++0x, dichiarali explicit).'''
Gli operatori di conversioni consentono conversioni implicite, e
Se tali conversioni sono necessarie, fornisci invece una funzione membro equivalente, che può essere chiamata solo esplicitamente.
L'unico utilizzo che rimane accettabile per gli operatori di conversione si ha quando si sta convertendo una grande mole di software dall'uso di una libreria all'uso di un'altra libreria simile. Durante la fase di transizione, in cui le due librerie coesistono, può essere comodo avere operatori che convertono automaticamente gli oggetti dai tipi della vecchia libreria ai tipi della nuova libreria.
=== Idioma ''Pimpl'' ===
Line 435 ⟶ 445:
'''Non usare sistematicamente l'idioma ''Pimpl'', ma solo quando vuoi rendere il resto del programma indipendente dall'implementazione di una classe.'''
L'idioma ''Pimpl'' (che significa Puntatore a IMPLementazione) consiste nel memorizzare nell'oggetto solamente un puntatore alla struttura che contiene tutte le informazioni utili di tale oggetto.
Il vantaggio principale di tale idioma è che velocizza la compilazione incrementale del codice, cioè rende meno probabile che una piccola modifica ai sorgenti comporti la necessità di ricompilare grandi quantità di codice.
Tale idioma consente anche di velocizzare alcune operazioni, come lo <code>swap</code> 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 pubbliche sono chiamate frequentemente.
=== Iteratori e
'''Nelle classi di iteratori o di oggetti-funzione, fa' in modo che tali oggetti siano piccolissimi e che non allochino memoria dinamica.'''
Line 455 ⟶ 465:
Inoltre, tale tipo di allocazione comporta uno spreco di spazio per ogni allocazione, genera frammentazione della memoria virtuale, e produce una scarsa località dei dati, con conseguente scadente utilizzo sia delle cache dei dati della CPU che della memoria virtuale.
Tale allocazione/deallocazione in linguaggio C veniva fatta con le funzioni malloc e free. In C++, pur essendo ancora disponibili tali funzioni, le funzioni normalmente usate a tale scopo sono gli operatori <code>new</code>, <code>new[]</code>, <code>delete</code>, e <code>delete[]</code>.
Ovviamente, un modo di ridurre le allocazioni è ridurre il numero di oggetti costruiti, e quindi la sezione “Come evitare inutili costruzioni e le distruzioni di oggetti” serve indirettamente anche allo scopo di questa sezione.
Line 463 ⟶ 473:
=== Array di lunghezza fissa ===
'''Se un array statico o non grande ha lunghezza costante, non usare un oggetto <code>vector</code>, ma usa un array del C, o un oggetto <code>boost:array</code>.'''
I vector memorizzano i dati in un buffer allocato dinamicamente, mentre le altre soluzioni proposte allocano i dati nell'oggetto stesso.
Line 476 ⟶ 486:
Un ''allocatore a blocchi'' (detto anche ''allocatore a pool'') alloca blocchi di memoria medi o grandi, 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.
In particolare, un allocatore di questo tipo migliora notevolmente le prestazioni dei contenitori
Se la tua implementazione della libreria standard non usa già un allocatore a blocchi 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.
Line 482 ⟶ 492:
=== Aggiunta di elementi a collezione ===
'''Per aggiungere elementi in fondo a una collezione, usa
La funzione
La classe
La funzione
== Come velocizzare l'accesso alla memoria principale ==
Line 513 ⟶ 523:
'''Lascia l'allineamento di memoria suggerito dal compilatore.'''
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. === Raggruppamento di funzioni in unità di compilazione ===
'''Definisci nella stessa unità di compilazione tutte le funzioni membro di una classe, le funzioni
In tal modo, sia il codice macchina prodotto compilando tali routine sia i dati statici definiti in tali classi e routine avranno indirizzi vicini tra loro; inoltre, così si consente ai compilatori che non effettuano ottimizzazioni sull'intero programma di ottimizzare le chiamate tra tali funzioni.
Line 533 ⟶ 545:
In tal modo, si dichiara che non verranno usate da altre unità di compilazione. Questo permette ai compilatori che non effettuano ottimizzazioni sull'intero programma di ottimizzare l'utilizzo di tali variabili e funzioni.
==
=== Thread di lavoro ===
|