Ottimizzare C++/Ottimizzazione del codice C++/Costruzioni e distruzioni

Indice del libro

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 la distruzione di un oggetto di un tipo fondamentale sono abbastanza veloci. Invece, se l'oggetto è invece di un tipo composito, la sua creazione e la sua distruzione hanno un costo illimitato, in quanto comportano la chiamata di un costruttore e di un distruttore, che possono avere qualunque durata.

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.

Valore di ritorno di funzioni

modifica

Per le funzioni che non siano espanse inline, cerca di dichiarare un tipo di ritorno tale che la copia di oggetti di tale tipo non sposta più di 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 oggetto la cui copia non sposta più di 8 byte costa poco o niente, ma generare oggetti più complessi richiede tempo. Se l'oggetto temporaneo possiede delle risorse, il tempo richiesto è enormemente maggiore, ma anche senza allocazioni, il tempo richiesto cresce al crescere del 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 questi sono legati a variabili locali (con la cosiddetta Named Return Value Optimization), ma in generale questo non è garantito e ha comunque alcune limitazioni.

Per verificare se viene attuata una di tali ottimizzazioni, incrementa un contatore statico nei costruttori, nel distruttore, e nell'operatore di assegnamento della classe dell'oggetto ritornato. Nel caso non risultassero applicate ottimizzazioni, ricorri a una delle seguenti tecniche alternative:

  • Rendi la funzione void, e aggiungile un argomento passato per riferimento, che funge da valore di ritorno.
  • Trasforma la funzione in un costruttore del tipo ritornato, che riceve gli stessi parametri della funzione.
  • Fai in modo che la funzione restituisca un oggetto di un tipo ausiliario che ruba le risorse e le cede all'oggetto destinazione, senza copiarle.
  • Usa un expression template, che è una tecnica avanzata, facente parte del paradigma di programmazione detto Template metaprogramming.
  • Se usi lo standard C++0x, usa un rvalue reference.

Spostamento di variabili all'esterno di cicli

modifica

Se una variabile è dichiarata all'interno di un ciclo, e l'assegnamento ad essa costa di meno di una costruzione più una distruzione, sposta tale dichiarazione prima del ciclo.

Se la variabile è dichiarata all'interno del ciclo, l'oggetto associato ad essa viene costruito e distrutto a ogni iterazione, mentre se è esterna al ciclo, tale oggetto viene costruito e distrutto una volta sola, ma presumibilmente viene assegnato una volta in più nel corpo del ciclo.

Tuttavia, in molti casi un assegnamento costa esattamente quanto una coppia costruzione+distruzione, per cui in tali casi non ci sono vantaggi a spostare la dichiarazione all'esterno e aggiungere un assegnamento all'interno.

Operatore di assegnamento

modifica

In un overload dell'operatore di assegnamento (operator=), se sei sicuro che non solleverà eccezioni, copia ogni variabile membro, invece di usare l'idioma copy&swap.

La tecnica più efficiente per copiare un oggetto è imitare una corretta lista di inizializzazione di un costruttore di copia, cioè, prima, si chiama l'analoga funzione membro delle classi base, e poi si copiano tutte le variabili membro, in ordine di dichiarazione.

Purtroppo, tale tecnica non è exception-safe, cioè se durante questa operazione viene sollevata un'eccezione, i distruttori di alcuni sotto-oggetti già costruiti potrebbero non venire mai chiamati. Pertanto, se c'è la possibilità che durante la copia venga sollevata un'eccezione, si deve usare una tecnica exception-safe, che tuttavia non avrà prestazioni ottimali.

La tecnica di assegnamento exception-safe più elegante è l'idioma copy&swap. Viene mostrata dal seguente codice, nel quale C rappresenta il nome della classe, e swap una funzione membro che dovrà essere definita:

C& C::operator=(C new_value) {
    swap(new_value);
    return *this;
}

Overload per evitare conversioni

modifica

Per evitare costose conversioni di tipo, definisci delle funzioni in overload per i tipi di argomento più comune.

Supponiamo di aver scritto la seguente funzione:

int f(const std::string& s) { return s[0]; }

il cui scopo è consentire di scrivere il segueente codice:

std::string s("abc");
int n = f(s);

Tale funzione può però essere usata anche dal seguente codice:

int n = f(string("abc"));

E, grazie alla conversione implicita da char* a std::string, può essere usata anche dal seguente codice:

int n = f("abc");

Entrambe le due ultime chiamate alla funzione f sono inefficienti, perché creano un oggetto temporaneo std::string non vuoto.

Per mantenere l'efficienza della prima chiamata dell'esempio, si dovrebbe definire anche la seguente funzione in overload:

int f(const char* s) { return s[0]; }

In generale, se una funzione è chiamata passandole un argomento di un tipo non consentito ma che può venire implicitamente convertito a un tipo consentito, viene creato un oggetto temporaneo del tipo consentito.

Per evitare tale oggetto temporaneo, si deve definire una funzione in overload rispetto alla funzione originale, che prenda un argomento del tipo dell'effettivo oggetto passato, evitando così la necessità di una conversione.