Ottimizzare C++/Ottimizzazione del codice C++/Numero di istruzioni

Wikibooks, manuali e libri di testo liberi.
Indice del libro

Anche i costrutti che generano del codice espanso inline possono avere un costo significativo, in quanto tali istruzioni devono pur essere eseguite.

In questa sezione si descrivono le tecniche per ridurre il numero complessivo di istruzioni che il processore dovrà eseguire per compiere una data operazione.

Ordine dei casi in istruzioni switch[modifica]

Nelle istruzioni switch, poni i casi in ordine di probabilità decrescente.

Nella linea-guida "Ordine dei casi dell'istruzione switch" del capitolo 3.1, si consigliava già di porre prima di casi più tipici, cioè quelli che si presume siano più probabili. Come ulteriore ottimizzazione, si potrebbe 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.

Parametri interi di template[modifica]

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 la seguente funzione di libreria, in cui sia x che y non hanno un valore definito in fase di sviluppo della libreria:

int f1(int x, int 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);

Se, mentre scrivi la libreria, sai che il chiamante ti passerà sicuramente una costante intera come argomento y, puoi trasformare la tua funzione nel seguente template di funzione:

template <int Y> int f2(int x) { return x * Y; }

Tale funzione può essere chiamata dal seguente codice applicativo:

int a = f2<4>(b);

Tale chiamata istanzia automaticamente la seguente funzione:

int f2(int x) { return x * 4; }

Quest'ultima funzione è più veloce della precedente funzione f1, per i seguenti motivi:

  • Viene passato un solo parametro alla funzione (x) invece di due (x e y).
  • La moltiplicazione per una costante intera (4) è più veloce della moltiplicazione per una variabile intera (y).
  • Dato che il valore costante (4) è una potenza di due, il compilatore, invece di eseguire una moltiplicazione intera, esegue uno scorrimento di bit.

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 pre-calcolate in fase di compilazione.

Se invece di avere una funzione avevi già un template di funzione, basta aggiungere un ulteriore parametro a tale template.

Il Curiously Recurring Template Pattern[modifica]

Se devi scrivere una classe base astratta di libreria tale che in ogni algoritmo nel codice 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:
    void g() { f(); }
private:
    virtual void f() = 0;
};

In questa classe, la funzione g esegue un algoritmo che chiama la funzione f come operazione astratta per l'algoritmo. Nella terminologia dei design pattern, g è un template method, ossia un algoritmo astratto con uno o più punti di personalizzazione. Lo scopo di questa classe è consentire di scrivere il seguente codice applicativo:

class Derivata1: public Base {
private:
    virtual void f() { ... }
};
...
Base* p1 = new Derivata1;
p1->g();

In tal caso, è possibile convertire il precedente codice di libreria nel seguente:

template <class Derivata> class Base {
public:
    void g() { f(); }
private:
    void f() { static_cast<Derivata*>(this)->f(); }
};

Il codice applicativo, di conseguenza, diventerà il seguente:

class Derivata1: public Base<Derivata1> {
public:
    void f() { ... }
};
...
Derivata1* p1 = new Derivata1;
p1->g();

In tal modo, si ha binding statico alla funzione membro Derivata1::f della chiamata a f fatta all'interno della funzione membro Base<Derivata1>::g, cioè la chiamata a tale funzione non è più di tipo virtual, e può essere espansa inline.

Tuttavia, supponiamo che si volesse aggiungere la seguente definizione:

class Derivata2: public Base<Derivata2> {
public:
    void f() { ... }
};

Con questa soluzione non sarebbe più possibile definire un puntatore o riferimento a una classe base comune sia a Derivata1 che a Derivata2, in quanto tali classi risultano due tipi senza alcuna relazione; di conseguenza, questa soluzione non è applicabile se si vuole permettere al codice applicativo di definire un contenitore di oggetti arbitrari derivati dalla classe Base.

Altre limitazioni sono le seguenti:

  • Base è necessariamente un tipo astratto;
  • un oggetto di tipo Derivata1 non può essere convertito in un oggetto di tipo Derivata2 o viceversa;
  • per ogni derivazione di Base, tutto il codice macchina di Base viene duplicato.

Il pattern Strategy[modifica]

Se un oggetto che implementa il design pattern Strategy (noto anche come design pattern Policy) è una costante in ogni algoritmo nel codice applicativo, elimina tale oggetto, rendi static tutti i suoi membri, e aggiungi tale classe come parametro di template.

Supponi che stai scrivendo il seguente codice di libreria, che implementa il design 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); }
private:
    Strategy s_;
};

Questo codice di libreria ha 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 mia strategia.
C c; // Oggetto contenente un algoritmo con strategia personalizzabile.
c.set_strategy(s); // Assegnazione della strategia personalizzata.
c.f(); // Esecuzione dell'algoritmo con la strategia assegnata.

In tal caso, è possibile convertire il precedente codice di libreria nel seguente:

template <class Strategy>
class C {
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 assegnata staticamente.
c.f(); // Esecuzione con strategia assegnata staticamente.

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 virtual.

Tuttavia, tale soluzione non consente di decidere la strategia in fase di esecuzione, e tanto meno di cambiarla durante la vita dell'oggetto. Inoltre, il codice dell'algoritmo astratto viene duplicato ogni volta che viene personalizzato.

Operatori bit-a-bit[modifica]

Dovendo eseguire operazioni booleane su un insieme di singoli bit, affianca tali bit in un oggetto 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.