Qual è il costo dell'utilizzo di un puntatore alla funzione membro rispetto a uno switch?

StackOverflow https://stackoverflow.com/questions/113150

  •  02-07-2019
  •  | 
  •  

Domanda

Ho la seguente situazione:


class A
{
public:
    A(int whichFoo);
    int foo1();
    int foo2();
    int foo3();
    int callFoo(); // cals one of the foo's depending on the value of whichFoo
};

Nella mia attuale implementazione, salvo il valore di whichFoo in un membro di dati nel costruttore e utilizzo un switch in callFoo () in decidere quale dei foo deve chiamare. In alternativa, posso usare un switch nel costruttore per salvare un puntatore a fooN () giusto per essere chiamato in callFoo () .

La mia domanda è quale sia il modo più efficace se un oggetto di classe A viene costruito una sola volta, mentre callFoo () viene chiamato un numero molto elevato di volte. Quindi nel primo caso abbiamo più esecuzioni di un'istruzione switch, mentre nel secondo c'è un solo switch e più chiamate di una funzione membro usando il puntatore ad essa. So che chiamare una funzione membro usando un puntatore è più lento di chiamarlo direttamente. Qualcuno sa se questo sovraccarico è superiore o inferiore al costo di un switch ?

Chiarimento: mi rendo conto che non sai mai quale approccio offre prestazioni migliori fino a quando non lo provi e non cronometri. Tuttavia, in questo caso ho già implementato l'approccio 1 e volevo scoprire se l'approccio 2 può essere più efficiente almeno in linea di principio. Sembra che possa essere, e ora ha senso preoccuparmi di implementarlo e provarlo.

Oh, e mi piace anche l'approccio 2 meglio per motivi estetici. Immagino che sto cercando una giustificazione per attuarlo. :)

È stato utile?

Soluzione

Quanto sei sicuro che chiamare una funzione membro tramite un puntatore sia più lento della semplice chiamata diretta? Puoi misurare la differenza?

In generale, non fare affidamento sul proprio intuito quando si effettuano valutazioni delle prestazioni. Siediti con il tuo compilatore e una funzione di temporizzazione, e in realtà misura le diverse scelte. Potresti essere sorpreso!

Ulteriori informazioni: c'è un eccellente articolo Puntatori alle funzioni membro e delegati C ++ più veloci possibili che approfondisce in modo molto approfondito l'implementazione dei puntatori alle funzioni membro.

Altri suggerimenti

Puoi scrivere questo:

class Foo {
public:
  Foo() {
    calls[0] = &Foo::call0;
    calls[1] = &Foo::call1;
    calls[2] = &Foo::call2;
    calls[3] = &Foo::call3;
  }
  void call(int number, int arg) {
    assert(number < 4);
    (this->*(calls[number]))(arg);
  }
  void call0(int arg) {
    cout<<"call0("<<arg<<")\n";
  }
  void call1(int arg) {
    cout<<"call1("<<arg<<")\n";
  }
  void call2(int arg) {
    cout<<"call2("<<arg<<")\n";
  }
  void call3(int arg) {
    cout<<"call3("<<arg<<")\n";
  }
private:
  FooCall calls[4];
};

Il calcolo dell'attuale puntatore a funzione è lineare e veloce:

  (this->*(calls[number]))(arg);
004142E7  mov         esi,esp 
004142E9  mov         eax,dword ptr [arg] 
004142EC  push        eax  
004142ED  mov         edx,dword ptr [number] 
004142F0  mov         eax,dword ptr [this] 
004142F3  mov         ecx,dword ptr [this] 
004142F6  mov         edx,dword ptr [eax+edx*4] 
004142F9  call        edx 

Nota che non è nemmeno necessario correggere il numero della funzione effettiva nel costruttore.

Ho confrontato questo codice con l'asm generato da un switch . La versione switch non fornisce alcun aumento delle prestazioni.

Per rispondere alla domanda posta: al livello più fine, il puntatore alla funzione membro funzionerà meglio.

Per rispondere alla domanda non posta: cosa fa " meglio " intendi qui? Nella maggior parte dei casi, mi aspetto che la differenza sia trascurabile. A seconda di ciò che la classe sta facendo, tuttavia, la differenza può essere significativa. Il test delle prestazioni prima di preoccuparsi della differenza è ovviamente il primo passo giusto.

Se continuerai a usare un interruttore, il che è perfettamente a posto, probabilmente dovresti mettere la logica in un metodo di supporto e chiamare se dal costruttore. In alternativa, questo è un classico caso del Strategy Pattern . È possibile creare un'interfaccia (o classe astratta) denominata IFoo che ha un metodo con la firma di Foo. Avresti il ??costruttore prendere in un'istanza di IFoo (costruttore Dependancy Injection che implementava il metodo foo quello che vorresti. Avresti un IFoo privato che sarebbe impostato con questo costruttore e ogni volta che avresti voluto chiamare Foo avresti chiamato la versione del tuo IFoo.

Nota: non lavoro con C ++ dal college, quindi il mio gergo potrebbe essere fuori di qui, se le idee generali valgono per la maggior parte delle lingue OO.

Se il tuo esempio è il codice reale, penso che dovresti rivisitare il tuo design di classe. Passare un valore al costruttore e usarlo per cambiare comportamento equivale davvero alla creazione di una sottoclasse. Prendi in considerazione il refactoring per renderlo più esplicito. L'effetto di ciò è che il tuo codice finirà per usare un puntatore a funzione (tutti i metodi virtuali sono, in realtà, puntatori a funzioni nelle tabelle di salto).

Se, tuttavia, il tuo codice fosse solo un esempio semplificato per chiederti se, in generale, le tabelle di salto sono più veloci delle istruzioni switch, la mia intuizione direbbe che le tabelle di salto sono più veloci, ma sei dipendente dal passaggio di ottimizzazione del compilatore. Ma se le prestazioni sono davvero una tale preoccupazione, non fare mai affidamento sull'intuizione: abbattere un programma di test e testarlo o guardare l'assemblatore generato.

Una cosa è certa, un'istruzione switch non sarà mai più lenta di una tabella di salto. Il motivo è che il meglio che un ottimizzatore di un compilatore può fare sarà anche trasformare una serie di test condizionali (cioè un interruttore) in una tabella di salto. Quindi, se vuoi davvero esserne certo, togli il compilatore dal processo decisionale e usa una tabella di salto.

Sembra che dovresti rendere callFoo una pura funzione virtuale e creare alcune sottoclassi di A .

A meno che tu non abbia davvero bisogno della velocità, abbia fatto profilature e strumentazioni estese e abbia stabilito che le chiamate a callFoo sono davvero il collo di bottiglia. Hai?

I puntatori a funzione sono quasi sempre migliori di quelli concatenati. Fanno un codice più pulito e sono quasi sempre più veloci (tranne forse in un caso in cui è solo una scelta tra due funzioni ed è sempre previsto correttamente).

Dovrei pensare che il puntatore sarebbe più veloce.

Istruzioni per il prefetch delle moderne CPU; i rami predetti erroneamente svuotano la cache, il che significa che si blocca mentre riempie la cache. Un puntatore non lo fa.

Ovviamente, dovresti misurare entrambi.

Ottimizza solo quando necessario

Primo: la maggior parte delle volte probabilmente non ti interessa, la differenza sarà molto piccola. Assicurati di ottimizzare prima questa chiamata. Solo se le tue misurazioni mostrano che c'è un tempo davvero significativo speso nell'overhead della chiamata, procedi all'ottimizzazione (plug spudorato - Cf. Come ottimizzare un'applicazione per renderla più veloce? ) Se l'ottimizzazione non è significativa, preferisci il codice più leggibile.

Il costo delle chiamate indirette dipende dalla piattaforma di destinazione

Dopo aver stabilito che vale la pena applicare l'ottimizzazione di basso livello, allora è il momento di capire la tua piattaforma di destinazione. Il costo che puoi evitare qui è la penalità per errore di filiale. Nelle moderne CPU x86 / x64 è probabile che questo errore sia molto piccolo (possono prevedere le chiamate indirette abbastanza bene per la maggior parte del tempo), ma quando si prendono di mira PowerPC o altre piattaforme RISC, le chiamate / salti indiretti spesso non sono previsti affatto ed evitano possono causare un significativo aumento delle prestazioni. Vedi anche Il costo della chiamata virtuale dipende dalla piattaforma .

Il compilatore può implementare anche lo switch usando la tabella di salto

One gotcha: lo switch a volte può essere implementato anche come una chiamata indiretta (usando una tabella), specialmente quando si passa tra molti valori possibili. Tale interruttore presenta lo stesso errore di una funzione virtuale. Per rendere affidabile questa ottimizzazione, si preferirebbe probabilmente usare if invece di passare al caso più comune.

Usa i timer per vedere quale è più veloce. Anche se a meno che questo codice non sia ripetutamente, è improbabile che noterai alcuna differenza.

Assicurati che se stai eseguendo il codice dal costruttore che se la costruzione fallisce non perderai memoria.

Questa tecnica è ampiamente utilizzata con il sistema operativo Symbian: http://www.titu.jyu.fi/modpa/Patterns/ pattern-TwoPhaseConstruction.html

Se stai chiamando callFoo () solo una volta, rispetto a molto probabilmente il puntatore alla funzione sarà più lento di un importo insignificante. Se lo chiami più volte di molto probabilmente , il puntatore alla funzione sarà più veloce di una quantità insignificante (perché non è necessario che passi attraverso lo switch).

In ogni caso, guarda il codice assemblato per scoprire con certezza che sta facendo quello che pensi che stia facendo.

Uno dei vantaggi spesso trascurati del passaggio (anche rispetto all'ordinamento e all'indicizzazione) è se si sa che nella maggior parte dei casi viene utilizzato un valore particolare. È facile ordinare l'interruttore in modo che i più comuni siano controllati per primi.

ps. Per rafforzare la risposta di Greg, se ti interessa la velocità, misura. Osservare l'assemblatore non aiuta quando le CPU hanno ramificazioni predittive / predittive e pipeline ecc.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top