Ottimizzare C++/Scrivere codice C++ efficiente/Costrutti che peggiorano le prestazioni
Rispetto al linguaggio C, il linguaggio C++ aggiunge alcuni costrutti, il cui utilizzo peggiora l'efficienza.
Alcuni di essi sono piuttosto efficienti, e quindi li si può usare tranquillamente quando servono, ma si dovrebbe evitare di pagarne il costo quando non li si usa.
Altri costrutti sono invece alquanto inefficienti, e devono quindi essere usati con parsimonia.
In questa sezione si presentano le linee-guida per evitare i costi dei costrutti C++ che peggiorano le prestazioni.
L'operatore throw
[modifica | modifica sorgente]Chiama l'operatore throw
solamente quando vuoi avvisare un utente del fallimento del comando corrente.
Il sollevamento di una eccezione ha un costo molto elevato, rispetto a quello di una chiamata di funzione. Sono migliaia di 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 senza che ce ne si accorga. Invece, se si sollevano eccezioni per scopi algoritmici, anche se tali operazioni sono state pensate inizialmente per essere eseguite raramente, potrebbero finire per essere eseguite troppo frequentemente.
Funzioni membro static
[modifica | modifica sorgente]In ogni classe, dichiara static
ogni funzione membro che non accede ai membri non-static
di tale classe.
In altre parole, dichiara static
tutte le funzioni membro che puoi.
In questo modo, non viene passato l'argomento implicito this
.
Funzioni membro virtual
[modifica | modifica sorgente]In ogni classe, definisci virtual
il distruttore se e solo se la classe contiene almeno un'altra funzione membro virtual
, e, a parte i costruttori e il distruttore, definisci virtual
solamente le funzioni membro che ritieni possa essere utile ridefinire.
Classi che contengono almeno una funzione membro virtual
occupano un po' più di spazio delle classi che non ne contengono. Le istanze delle classi che contengono almeno una funzione membro virtual
occupano un po' più di spazio (tipicamente, un puntatore ed eventualmente dello spazio di allineamento) e la loro costruzione richiede un po' più di tempo (tipicamente, per impostare tale puntatore) rispetto alle istanze di classi prive di funzioni membro virtual
.
Inoltre, ogni funzione membro virtual
è più lenta da chiamare di un'identica funzione membro non-virtual
.
Derivazione virtual
[modifica | modifica sorgente]Usa la derivazione virtual
solo quando due o più classi devono condividere la rappresentazione di una classe base comune.
Per esempio, considera le seguenti definizioni di classe:
class A { ... };
class B1: public A { ... };
class B2: public A { ... };
class C: public B1, public B2 { ... };
Con tali definizioni, ogni oggetto di classe C contiene due oggetti distinti di classe A, uno ereditato dalla classe base B1, e l'altro ereditato dalla classe base B2.
Questo non costituisce un problema se la classe A non ha nessuna variabile membro non-static
.
Se invece tale classe A contiene qualche variabile membro, e si intende che tale variabile membro debba essere unica per ogni istanza di classe C, si deve usare la derivazione virtual
, nel seguente modo:
class A { ... };
class B1: virtual public A { ... };
class B2: virtual public A { ... };
class C: public B1, public B2 { ... };
Questa situazione è l'unica in cui è necessaria la derivazione virtual
.
Se si usa la derivazione virtual
, le funzioni membro della classe A sono un po' più lente da chiamare su un oggetto di classe C.
Template di classi polimorfiche
[modifica | modifica sorgente]Non definire template di classi polimorfiche.
In altre parole, non usare le parole-chiave "template
" e "virtual
" nella stessa definizione di classe.
I template di classe, ogni volta che vengono istanziati, producono una copia di tutte le funzioni membro utilizzate, e se tali classi contengono funzioni virtuali, vengono replicate anche le vtable e le informazioni RTTI. Questi dati ingrandiscono eccessivamente il programma.
Uso di deallocatori automatici
[modifica | modifica sorgente]Usa un gestore della memoria basato su garbage-collection o un tipo di smart-pointer dotato di reference-count (come lo shared_ptr
della libreria Boost) solamente 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 potrebbe produrre prestazioni peggiori rispetto alla deallocazione esplicita (cioè chiamando l'operatore delete
).
La libreria standard del C++98 contiene un solo smart-pointer, l'auto_ptr
, che è efficiente. Altri smart-pointer sono forniti da librerie non-standard, come Boost, o verranno forniti dal C++0x.
Tra di essi, gli smart-pointer basati su reference-count, come lo shared_ptr
di Boost, sono meno efficienti dei puntatori semplici, e quindi devono essere usati solo nei casi in cui se ne dimostra la necessità.
In particolare, compilando con l'opzione di gestione del multithreading, tali puntatori hanno pessime prestazioni nella creazione, distruzione e copia dei puntatori, in quanto devono garantire la mutua esclusione delle operazioni.
Normalmente bisognerebbe, in fase di progettazione, cercare di assegnare ogni oggetto allocato dinamicamente ad un proprietario, cioè a un altro oggetto che lo possiede. Solo quando tale assegnazione risulta difficile, in quanto più oggetti tendono a rimpallarsi la responsabilità di distruggere l'oggetto, risulta opportuno usare uno smart-pointer con reference-count per gestire tale oggetto.
Il modificatore volatile
[modifica | modifica sorgente]Definisci volatile
solamente quelle variabili che vengono modificate in modo asincrono da dispositivi hardware o da più thread.
L'uso del modificatore volatile
impedisce al compilatore di allocare una variabile in un registro, anche per un breve periodo.
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.