Ottimizzare C++/Ottimizzazione del codice C++: differenze tra le versioni

Contenuto cancellato Contenuto aggiunto
Nessun oggetto della modifica
Nessun oggetto della modifica
Riga 44:
using std::swap;
 
== 5.2. Come ridurre i costrutti che richiamano routine del supporto run-time ==
=== 5.2.1. Evita di usare l’operatore “typeid”. Al suo posto, cerca di usare una funzione virtual o il design pattern Visitor. ===
 
Tale operatore può richiedere un tempo superiore a quello richiesto da una semplice chiamata a funzione.
=== 5.2.2. Evita di usare l’operatore “dynamic_cast”. Piuttosto, usa l’operatore “typeid”. ===
 
Tale operatore può richiedere un tempo notevolmente superiore a quello richiesto da una semplice chiamata a funzione, e maggiore anche a quello richiesto dall’operatore “typeid”.
=== 5.2.3. Usa la specifica di eccezioni vuota (cioè aggiungi “throw()” dopo la dichiarazione) alle funzioni di cui sei certo che non solleveranno eccezioni. ===
 
Alcuni compilatori utilizzano tale informazione per ottimizzare la contabilità necessaria per gestire le eventuali eccezioni.
=== 5.2.4. Sposta le istruzioni “try” prima dei colli di bottiglia, e le istruzioni “catch” dopo, in modo che includano l’intero collo di bottiglia. ===
 
L’esecuzione di un blocco try/catch talvolta ha costo zero, ma altre volte comporta un rallentamento. Evita di eseguire tale blocco all’interno dei colli di bottiglia.
=== 5.2.5. Se il processore non contiene un’unità a virgola mobile, evita le operazioni a virgola mobile. ===
 
Gli odierni processori per sistemi desktop e server contengono hardware dedicato all’aritmetica a virgola mobile, per cui tali operazioni, eccettuate somme e sottrazioni, sono pressoché altrettanto veloci quanto quelle su numeri interi. Alcuni processori dedicati per sistemi embedded, invece, non contengono hardware dedicato all’aritmetica a virgola mobile. Pertanto tali operazioni vengono solamente emulate con lentissime funzioni di libreria. In tal caso, risulta molto più veloce utilizzare l’aritmetica intera, o meglio a virgola fissa, cioè usando degli interi intendendoli moltiplicati per un fattore di scala. Ogni numero viene moltiplicato per tale fattore in fase di input e viene per lo stesso fattore in fase di output, o viceversa.
=== 5.2.6. Le funzioni standard di conversione da numero intero a stringa e da numero a virgola mobile a stringa sono inefficienti. Per ottimizzarle, usa librerie non-standard o riscrivi tali funzioni. ===
== 5.3. Come diminuire il numero di istruzioni eseguite ==
=== 5.3.1. Per controllare se un numero intero “i” è compreso tra “min_i” e “max_i”, estremi inclusi, usa l’espressione “unsigned(i – min_i) <= unsigned(max_i – min_i)”. ===
 
La suddetta formula è equivalente a quella più intuitiva:
 
min_i <= i && i <= max_i
 
la quale tuttavia richiede due confronti invece di uno solo. In particolare, per controllare che un numero “i” sia valido come indice per accedere a un array di “size” elementi, si può scrivere l’espressione:
Riga 73:
 
Se le espressioni sono già unsigned, le conversioni non sono necessarie.
=== 5.3.2. Nelle istruzioni switch, poni i casi in ordine di probabilità decrescente. ===
 
Nella linea-guida 3.1.12 si consigliava di porre prima di casi più tipici, cioè quelli che si presume siano più probabili. Come ulteriore ottimizzazione, si dovrebbe contare, in esecuzioni tipiche, il numero di volte in cui viene eseguito ognuno dei singoli casi, e porre i casi in ordine da quello eseguito più volte a quello eseguito meno volte.
=== 5.3.3. Se un certo valore intero è una costante nel codice applicativo, ma è una variabile nel codice di libreria, rendilo un parametro di template. ===
 
Supponi che stai scrivendo il seguente template di funzione di libreria, in cui sia x che y non hanno un valore definito in fase di sviluppo della libreria:
 
template <typename T> T f1(T x, T y) { return x * y; }
 
Tale funzione può essere chiamata dal seguente codice applicativo, nel quale x non ha un valore costante, ma y è la costante 4:
 
int a = f1(b, 4);
 
Tale chiamata istanzia automaticamente la seguente funzione:
 
int f1(int x, int y) { return x * y; }
 
Se mentre scrivi la libreria sai che il chiamante ti passerà sicuramente una costante intera come argomento y, puoi trasformare il template nel seguente:
Riga 109:
 
In generale, i parametri di template interi sono delle costanti per chi istanzia il template e quindi per il compilatore, e le costanti sono gestite in modo più efficiente delle variabili. Inoltre, alcune operazioni su costanti vengono calcolate in fase di compilazione.
=== 5.3.4. Se devi scrivere una classe base astratta di libreria tale che in ogni algoritmo applicativo si userà una sola classe derivata da tale classe base, usa il “Curiously Recurring Template Pattern”. ===
 
Supponi che stai scrivendo la seguente classe base di libreria:
 
class Base {
public:
virtual void f() = 0;
void g() { f(); }
};
 
In questa classe, la funzione g esegue un algoritmo che usa una chiamata a f come punto di personalizzazione dell'algoritmo. Lo scopo è consentire di scrivere il seguente codice applicativo:
 
class Derivata1: public Base {
public: virtual void f() { ... }
};
 
class Derivata2: public Base {
 
public: virtual void f() { ... }
 
};
class Derivata2: public Base {
...
public: virtual void f() { ... }
Derivata1 Base* p1 = new Derivata1;
};
p1->g();
...
Base* p1p2 = new Derivata1Derivata2;
p1 p2->g();
Base* p2 = new Derivata2;
p2->g();
 
In tal caso, è possibile convertire il precedente codice di libreria nel seguente:
 
template <class Derivata> class Base {
public:
void f() { static_cast<Derivata*>(this)->f(); }
void g() { f(); }
};
 
Il codice applicativo, di conseguenza, diventerà il seguente:
 
class Derivata1: public Base<Derivata1> {
public: void f() { ... }
};
 
class Derivata2: public Base<Derivata2> {
};
public: void f() { ... }
 
};
 
...
 
Derivata1* p1 = new Derivata1;
class Derivata2: public Base<Derivata2> {
p1->g();
public: void f() { ... }
Base Derivata2* p2 = new Derivata2;
 
p2->g();
};
...
Derivata1* p1 = new Derivata1;
p1->g();
Derivata2* p2 = new Derivata2;
p2->g();
 
In tal modo, si ha binding statico delle funzioni membro Derivata1::f e Derivata2::f rispettivamente alla funzioni membro Base<Derivata1>::g e Base<Derivata1>::g, cioè si evitano chiamate a funzioni virtuali.
Line 172 ⟶ 166:
Supponi che stai scrivendo il seguente codice di libreria, che implementa il pattern strategy,
 
class C;
class Strategy {
public:
virtual bool is_valid(const C&) const = 0;
virtual void run(C&) const = 0;
};
 
class C {
 
public:
};
void set_strategy(const Strategy& s): s_(s) { }
 
void f() { if (s.is_valid(*this)) s.run(*this); }
class C {
private:
public:
void set_strategy(const Strategy& s): s_(s) { };
};
 
void f() { if (s.is_valid(*this)) s.run(*this); }
private:
Strategy s_;
};
 
avente lo scopo di consentire il seguente codice applicativo:
 
class MyStrategy: public Strategy {
public:
virtual bool is_valid(const C& c) const { ... }
virtual void run(C& c) const { ... }
};
...
 
MyStrategy s; // Oggetto che rappresenta la strategia.
C c; // Oggetto con strategia personalizzabile.
c.set_strategy(s); // Assegnazione della strategia.
c.f(); // Esecuzione con strategia assegnata.
 
In tal caso, è possibile convertire il precedente codice di libreria nel seguente:
 
template <class Strategy>
 
class C {
public:
 
class C {
void f() { if (Strategy::is_valid(*this)) Strategy::run(*this); }
public:
};
void f() { if (Strategy::is_valid(*this)) Strategy::run(*this); }
};
 
Il codice applicativo, di conseguenza, diventerà il seguente:
 
class MyStrategy {
public:
static bool is_valid(const C<MyStrategy>& c) { ... }
static void run(C<MyStrategy>& c) { ... }
};
...
 
C<MyStrategy> c; // Oggetto con strategia preassegnata.
c.f(); // Esecuzione con strategia preassegnata.
 
In tal modo, si evita l'oggetto-strategia, e si ha il binding statico delle funzioni membro MyStrategy::is_valid e MyStrategy::run, cioè si evitano chiamate a funzioni virtuali.
 
Tuttavia, tale soluzione non consente né di decidere la strategia in fase di esecuzione, né di cambiarla durante la vita dell'oggetto, e duplica il codice della strategia per ogni istanziazione di tale classe.
=== 5.3.6. Dovendo eseguire operazioni booleane su un insieme di singoli bit, affianca tali bit in una variabile di tipo “unsigned int”, e usa gli operatori bit-a-bit su tale oggetto. ===
 
Gli operatori bit-a-bit (“&”, “|”, “^”, “<<”, e “>>”) sono tradotti in singole istruzioni veloci, e operano su tutti i bit di un registro in una sola istruzione.
== 5.4. Come ridurre le costruzioni e distruzioni di oggetti ==
 
Spesso capita che per elaborare un’espressione venga creato un oggetto temporaneo, che viene distrutto alla fine della stessa espressione in cui viene creato. Se tale oggetto è di un tipo fondamentale, il compilatore quasi sempre riesce a evitarne la creazione, e comunque la creazione e distruzione di un oggetto di un tipo fondamentale sono veloci. Se l’oggetto è invece di un tipo composito. La creazione e distruzione possono avere un costo arbitrariamente alto, in quanto comportano la chiamata di un costruttore e di un distruttore. In questa sezione si descrivono alcune tecniche per evitare che siano creati oggetti temporanei di tipo composito, e quindi che siano chiamati i relativi costruttori e distruttori.
=== 5.4.1. Per le funzioni che non siano espanse inline, cerca di dichiarare il tipo di ritorno “void” o “bool” o intero o puntatore o riferimento. Comunque, evita di dichiarare un tipo di ritorno la cui copia sposta oltre 8 byte. Se non fosse fattibile, almeno costruisci l’oggetto da ritornare nelle stesse istruzioni “return”. ===
 
Nella compilazione di una funzione non espansa inline, il compilatore non può sapere se il valore di ritorno verrà usato, e quindi lo deve comunque generare. Generare un intero o un puntatore o un riferimento costa poco o niente, ma generare numeri a virgola mobile od oggetti più complessi richiede tempo. Se la copia comporta l’allocazione di risorse, il tempo richiesto è enormemente maggiore, ma anche senza allocazioni, il tempo richiesto è cresce al crescere dei numero delle word che vengono copiate quando si copia un oggetto di tale tipo. Comunque, se si costruisce l’oggetto da ritornare nelle stesse istruzioni “return”, senza quindi assegnare tale valore a una variabile, si sfrutta l’ottimizzazione garantita dallo standard detta Return Value Optimization, che previene la creazione di oggetti temporanei. Alcuni compilatori riescono a evitare la creazione di oggetti temporanei anche se sono legati a variabili locali (Named Return Value Optimization), ma in generale questo non è garantito. Per verificare se viene attuata una di tali ottimizzazioni, inserisci un contatore nei costruttori, nei distruttori, e negli operatori di assegnamento della classe dell’oggetto ritornato. Nel caso non risultassero applicate ottimizzazioni, ricorri a una delle seguenti tecniche alternative:
Line 259 ⟶ 249:
}
 
=== 5.4.4. Definisci funzioni in overload per i tipi di argomento più comune, per evitare conversioni. ===
 
Spesso si vuole chiamare una funzione passando come argomento un'espressione che non è del tipo dell'argomento, ma può essere convertita in tale tipo.