Ottimizzare C++/Ottimizzazione del codice C++/Allocazione e deallocazione

Indice del libro

Per quanto sia efficiente l'allocatore, le operazioni di allocazione e deallocazione, richiedono parecchio tempo, e spesso l'allocatore non è molto efficiente.

In questa sezione si descrivono alcune tecniche per ridurre il numero complessivo di allocazioni di memoria, e quindi delle corrispondenti deallocazioni. Sono da adottare solamente nei colli di bottiglia, cioè dopo aver constatato che il grande numero di allocazioni ha un impatto significativo sulle prestazioni.

La funzione alloca

modifica

In funzioni non-ricorsive, per allocare spazio di dimensione variabile ma non grande, usa la funzione alloca.

È molto efficiente, in quanto alloca spazio sullo stack.

È una funzione non-standard, ma presente in molti compilatori per vari sistemi operativi.

Può essere usata anche per allocare un array di oggetti che hanno un costruttore, purché si chiami l'operatore new di piazzamento sullo spazio ottenuto, ma non dovrebbe essere usata per array di oggetti che hanno un distruttore o che, direttamente o indirettamente, contengono membri che hanno un distruttore, in quanto tali distruttori non verrebbero mai chiamati.

Tuttavia, è piuttosto pericolosa, in quanto, se chiamata troppe volte o con un valore troppo grande, esaurisce lo stack, e, se usata per oggetti aventi un distruttore, provoca resource leak. Pertanto si consiglia di usare con grande moderazione questa funzione.

Spostare le allocazioni e le deallocazioni

modifica

Sposta prima dei colli di bottiglia le allocazioni di memoria, e dopo i colli di bottiglia le corrispondenti deallocazioni.

La gestione di memoria dinamica di dimensione variabile è molto più lenta della gestione della memoria sullo stack.

Analoga ottimizzazione va fatta per le operazioni che provocano allocazioni indirettamente, come la copia di oggetti che, direttamente o indirettamente, possiedono memoria dinamica.

La funzione reserve

modifica

Prima di aggiungere elementi a un oggetto vector o string, chiama la sua funzione membro reserve con una dimensione sufficiente per la maggior parte dei casi.

Se si aggiungono ripetutamente elementi a oggetti vector o string, ogni tanto viene eseguita una costosa operazione di riallocazione del contenuto. Per evitare tali riallocazioni, basta allocare inizialmente lo spazio che probabilmente sarà sufficiente.

Mantenere la capacità dei vector

modifica

Per svuotare un oggetto x di tipo vector<T> senza deallocarne la memoria, usa l'istruzione x.resize(0);; per svuotarlo deallocandone la memoria, usa l'istruzione vector<T>().swap(x);.

Per svuotare un oggetto vector, esiste anche la funzione membro clear(); tuttavia lo standard C++ non specifica se tale istruzione conserva la capacità allocata del vector oppure no.

Se riempi e svuoti ripetutamente un oggetto vector, e quindi vuoi essere sicuro di evitare frequenti riallocazioni, esegui lo svuotamento chiamando la funzione resize, che, secondo lo standard, conserva sicuramente la capacità per il successivo riempimento.

Se invece hai finito di usare un oggetto vector di grandi dimensioni, e per un po' di tempo non lo userai più, oppure lo userai con un numero molto più piccolo di elementi, e quindi vuoi essere sicuro di liberare la memoria usata da tale collezione, chiama la funzione swap su un nuovo oggetto temporaneo vector vuoto.

Ridefinire la funzione swap

modifica

Per ogni classe concreta copiabile T che, direttamente o indirettamente, possiede della memoria dinamica, ridefinisci le appropriate funzioni swap.

In particolare, aggiungi alla classe una funzione membro public con la seguente firma:

void swap(T&) throw();

e aggiungi la seguente funzione non-membro nello stesso namespace che contiene la classe T:

void swap(T& lhs, T& rhs) { lhs.swap(rhs); }

e, se la classe non è un template di classe, aggiungi la seguente funzione non-membro nello stesso file che contiene la definizione della classe T:

namespace std { template<> swap(T& lhs, T& rhs) { lhs.swap(rhs); } }

Nella libreria standard la funzione std::swap viene richiamata frequentemente da molti algoritmi. Tale funzione ha una implementazione generica e ha implementazioni specializzate per vari tipi della libreria standard.

Se gli oggetti di una classe non-standard vengono usati in algoritmi della libreria standard, e non viene fornito un overload della funzione swap, viene usata l'implementazione generica.

L'implementazione generica di swap comporta la creazione e la distruzione di un oggetto temporaneo e l'esecuzione di due assegnamenti di oggetti. Tali operazioni richiedono molto tempo se applicate ad oggetti che possiedono della memoria dinamica, in quanto tale memoria viene riallocata per tre volte.

Il possesso di memoria dinamica citato nella linea-guida può essere anche solo indiretto. Per esempio, se una variabile membro è un oggetto string o vector, o è un oggetto che contiene un oggetto string o vector, la memoria posseduta da tali oggetti viene riallocata ogni volta che si copia l'oggetto che li contiene. Quindi anche in tali casi si devono ridefinire le funzioni swap.

Se l'oggetto non possiede memoria dinamica, la copia dell'oggetto è molto più veloce, e comunque non sensibilmente più lenta che usando altre tecniche, e quindi non si devono ridefinire funzioni swap.

Se la classe non è copiabile o è astratta, la funzione swap non dovrebbe mai essere chiamata sugli oggetti di tale tipo, e quindi anche in questo caso non si devono ridefinire funzioni swap.

Per velocizzare la funzione swap, la si deve specializzare per la propria classe. Ci sono due modi possibili per farlo: nel namespace della stessa classe (che può essere quello globale) come overload, oppure nel namespace std come specializzazione del template standard. È meglio definirla in entrambi i modi, in quanto, in primo luogo, se si tratta di un template di classe solo il primo modo è possibile, e poi alcuni compilatori non accettano o segnalano un avvertimento se è definita solo nel primo modo.

L'implementazione di tali funzioni devono accedere a tutti i membri dell'oggetto, e quindi hanno bisogno di richiamare una funzione membro, che per convenzione si chiama ancora swap, che effettua il lavoro.

Tale lavoro deve consistere nello scambiare tutti i membri non-static dei due oggetti, tipicamente chiamando la funzione swap su di essi, senza qualificarne il namespace.

Per consentire di trovare la funzione std::swap, la funzione membro deve iniziare con la seguente istruzione:

using std::swap;