Ottimizzare C++/Scrivere codice C++ efficiente: differenze tra le versioni

Contenuto cancellato Contenuto aggiunto
Nuova pagina: In questa sezione vengono proposte linee-guida per la programmazione in C++ finalizzate a evitare operazioni banalmente inefficienti, senza con questo rendere il codice meno manutenibi...
 
Nessun oggetto della modifica
Riga 2:
 
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.
 
== 3.1. Come approfittare dei costrutti C++ che migliorano le prestazioni ==
3.1.1. Non verificare che un puntatore sia non-nullo prima di chiamare delete su di esso.
 
=== 3.1.1. Non verificare che un puntatore sia non-nullo prima di chiamare delete su di esso. ===
 
Tale controllo viene già fatto da ogni implementazione di delete conforme allo standard, e quindi sarebbe ridondante.
 
=== 3.1.2. Invece delle funzioni di libreria qsort e bsearch, usa le funzioni std::sort e std:lower_bound. ===
 
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.
 
=== 3.1.3. Dichiara const ogni funzione membro che non modifica lo stato dell’oggetto. ===
 
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.
 
=== 3.1.4. Per rappresentare dei simboli interni, usa degli enumerati, invece di stringhe. ===
 
Un enumerato è implementato come un intero. Tipicamente, rispetto ad un intero, una stringa occupa più spazio, ed è più lenta da copiare e da confrontare.
 
=== 3.1.5. Se devi confrontare un valore intero con una serie di valori costanti, invece di una serie di istruzioni “if”, usa un’istruzione “switch”. ===
 
I compilatori possono sfruttare la regolarità di tale istruzione per applicare alcune ottimizzazioni, in particolare se viene applicata la linea-guida 3.1.11.
 
=== 3.1.6. Incapsula le collezioni in astrazioni di livello più alto, in modo da garantire l’intercambiabilità di implementazione. ===
 
In fase di ottimizzazione sarà così possibile scegliere l’implementazione più efficiente per la struttura dati, senza dover modificare molto codice.
 
I contenitori STL attuano già parzialmente questo principio, ma alcune operazioni sono disponibili solo per alcuni contenitori.
 
=== 3.1.7. Nell'uso dei contenitori STL, a parità di prestazioni, fa' in modo di rendere intercambiabile il contenitore. ===
 
Per esempio, chiama a.empty() invece di a.size() == 0, chiama iter != a.end() invece di iter < a.end().
 
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.
 
=== 3.1.8. Invece di scrivere un ciclo for 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 36 ⟶ 44:
 
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.
 
=== 3.1.9. Se devi scegliere un contenitore a lunghezza variabile, e sei incerto su quale contenitore scegliere, usa un vector. ===
 
Per insiemi fino a 8 elementi, il vector è 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 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.
 
=== 3.1.10. Se usi compilatori che consentono l’ottimizzazione dell’intero programma e l’espansione inline di qualunque funzione che il compilatore ritenga opportuno, usa tali opzioni e non dichiarare inline nessuna funzione. Se questi requisiti non fossero soddisfatti, dichiara inline nei file di intestazione solo le funzioni che contengono non più di tre righe di codice, nelle quali non ci sono cicli. ===
 
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.
Line 50 ⟶ 60:
 
Tra le routine non piccolissime, solo quelle critiche per la velocità verranno rese inline in fase di ottimizzazione.
 
=== 3.1.11. Come costanti per le istruzioni switch, usa sequenze compatte di valori, cioè senza lacune o con poche lacune. ===
 
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.
Line 56 ⟶ 67:
Per esempio, il seguente codice:
 
switch (i) {
case 10:
case 13:
funz_a();
break;
case 11:
funz_b();
break;
}
 
probabilmente genera il seguente pseudo-codice:
 
static indirizzo a[] = { caso_a, caso_b, 0, caso_a };
unsigned int indice = i - 10;
if (indice <= 3) goto a[indice];
goto fine:
caso_a: funz_a(); goto fine;
caso_b: funz_b(); goto fine;
fine:
 
ed è più efficiente del seguente:
 
switch (i) {
case 100:
case 130:
funz_a();
break;
case 110:
funz_b();
break;
}
 
che probabilmente genera il seguente pseudo-codice:
 
if (i == 100) goto caso_a;
if (i == 130) goto caso_a;
if (i == 110) goto caso_b;
goto fine;
caso_a: funz_a(); goto fine;
caso_b: funz_b(); goto fine;
fine:
 
=== 3.1.12. Nelle istruzioni switch, poni prima i casi più tipici. ===
 
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.
=== 3.1.13. Invece di elaborare in parallelo due o più array della stessa lunghezza, elabora un solo array di oggetti compositi. ===
 
Esempio: Invece del seguente codice:
const int n = 10000;
 
double a[n], b[n], c[n];
const int n = 10000;
for (int i = 0; i < n; ++i) {
double a[n], b[n], c[n];
for (int i = 0; a[i] <= n;b[i] ++ c[i) {];
}
a[i] = b[i] + c[i];
 
scrivi il seguente codice:
const int n = 10000;
 
struct { double a, b, c; } s[n];
const int n = 10000;
struct for {(int doublei a,= b, c0; }i < s[n]; ++i) {
for (int i = 0; s[i].a <= n;s[i].b ++ s[i) {].c;
}
s[i].a = s[i].b + s[i].c;
}
 
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.
 
=== 3.1.14. Se una funzione riceve almeno sei argomenti, e viene chiamata spesso con gli stessi valori tranne eventualmente uno o due, crea un oggetto che contiene tutti gli argomenti, e passa alla funzione tale oggetto per puntatore a costante o riferimento a costante. ===
 
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.
Line 128 ⟶ 138:
Per esempio, rispetto al seguente codice:
 
for (int i = 0; i < 1000; ++i) {
f(i, a1, a2, a3, a4, a5, a6, a7, a8);
}
 
è probabilmente più efficiente il seguente codice:
 
struct {
int i;
tipo a1, a2, a3, a4, a5, a6, a7, a8;
} s;
s.a1 = a1; s.a2 = a2; s.a3 = a3; s.a4 = a4;
s.a5 = a5; s.a6 = a6; s.a7 = a7; s.a8 = a8;
for (int i = 0; i < 1000; ++i) {
s.i = i;
f(s);
}
 
=== 3.1.15. Per memorizzare in un oggetto dei numeri interi, usa il tipo “int” o il tipo “unsigned int”, a meno che sia necessario un tipo più lungo; e per memorizzare dei numeri a virgola mobile, usa il tipo “double”, a meno che sia necessario il tipo “long double”. Se l’oggetto risultante è medio o grande, sostituisci i tipi interi con il tipo intero più piccolo in grado di contenerlo, ma senza usare i bit-field, e sostituisci i tipi a virgola mobile con il tipo “float”, a meno che sia necessaria maggiore precisione. ===
 
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”.
 
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.
 
=== 3.1.16. Per cercare un elemento in un contenitore, usa una funzione membro del contenitore, invece di un algoritmo STL. ===
 
Se è stata creata una tale funzione membro, è solo perché è più efficiente dell'algoritmo STL generico.
 
=== 3.1.17. Per cercare un elemento in una sequenza ordinata, usa gli algoritmi lower_bound, upper_bound, equal_range, o binary_search. ===
 
Dato che tutti i citati algoritmi usano la ricerca binaria, sono più veloci dell'algoritmo find, che usa la ricerca sequenziale
 
== 3.2. Come evitare i costi di costrutti C++ che peggiorano le prestazioni ==
 
Rispetto al linguaggio C, il linguaggio C++ aggiunge alcuni costrutti, il cui utilizzo peggiora l'efficienza.