Ottimizzazione di oggetti polimorfici in presenza di una sola implementazione

1

Diciamo che ho un'interfaccia chiamata ParentClass . ParantClass ha due implementazioni, ParentClassA e ParentClassB . C'è anche l'interfaccia ChildClass , con un'implementazione ChildClassA e ChildClassB . L'interfaccia ParentClass ha una funzione chiamata createChild , che restituisce un puntatore a un'interfaccia ChildClass del tipo appropriato ( ParentClassA::createChild restituisce un ChildClassA mentre ParentClassB::createChild restituisce un ChildClassB ).

Il codice tende ad assomigliare a questo:

ChildClass *child = parentClass->createChild();
child->destroy();

L'inefficienza qui è che l'interfaccia ParentClass restituirà sempre un'interfaccia ChildClass della stessa implementazione, quindi è in uso una tonnellata di vtables ma c'è solo un'implementazione a cui portano.

Una soluzione teoricamente più efficiente sarebbe che ParentClass::createChild restituisca un puntatore opaco e che tutte le funzioni membro contenute in ChildClass vengano spostate in ParentClass e che il puntatore opaco venga passato come primo argomento. Ciò si tradurrà in un singolo vtable, ma il codice finisce per essere un po 'più brutto.

E quel codice tende ad assomigliare a questo:

ChildClass *child = parentClass->createChild();
parentClass->destroyChild(child);

Questa parte della mia applicazione viene utilizzata abbastanza frequentemente, quindi le prestazioni sono una considerazione importante. Ma anche leggibilità e codice gestibile. Non sono sicuro di quale approccio dovrei usare.

    
posta User9123 02.06.2017 - 10:01
fonte

3 risposte

2

Non mettere la macchina davanti al cavallo. Finché non si ha la prova che le prestazioni sono compromesse da qualche inefficienza, non importa dove siano le inefficienze. Scrivi il codice più semplice e più gestibile che puoi, refactoring in caso di problemi.

    
risposta data 02.06.2017 - 12:23
fonte
1

Per espandere il commento di @ amon, puoi utilizzare i modelli per eseguire il polimorfismo in fase di compilazione, anziché eseguirlo in modalità virtuale in fase di runtime.

Se l'implementazione di ParentA e ParentB sono identiche a prescindere dagli usi di ChildA e ChildB, scrivi solo template<typename Child> class Parent{ ... }; direttamente.

Altrimenti, potresti comunque voler avere un'interfaccia, ma anche quello può essere un modello. Avresti qualcosa di simile

template<typename Child>
struct Parent {
    using child_type = Child;
    virtual Child * createChild() = 0;
    // rest of interface, using Child type parameter directly
};

struct ParentA : public Parent<ChildA> {
    // use of final is a strong hint for the compiler to devirtualize
    ChildA * createChild() final;
    // more implementation for ChildA
};

Se hai sottoclassi concrete per genitore, potresti avere un aiuto per ottenere il genitore per ogni bambino

template<typename Child>
struct child_traits;

template<>
struct child_traits<ChildA> { 
    using parent_type = ParentA; 
}

Quindi, con entrambe le opzioni, le altre parti del tuo programma usano Child (e possibilmente Parent

template<typename Child> 
void ParentCollaborator(ParentArgs args) {
    Parent<Child> parent(args); 
    Child * child parent.makeChild();
    // template type deduction allows omitting <Child> here
    doParentChildStuff(parent, *child);
}
    
risposta data 02.06.2017 - 14:03
fonte
0

Non vedo come le due opzioni che proponi siano diverse da una prospettiva di efficienza di dispacciamento.

Il primo approccio utilizza un dispatch per ParentClass , quindi ChildClass , mentre il secondo fa un invio per ParentClass , quindi ParentClass di nuovo.

La tua seconda soluzione non è molto diversa perché ParentClass ha anche due implementazioni.

Ora se in qualche modo ParentClass ha un'implementazione non virtuale di destroyChild , sarebbe diverso, ma non c'è nulla nel tuo post che lo suggerisca, e in effetti, come ho letto io, hai due diverse implementazioni della stessa interfaccia, che mi dice che destroyChild è virtuale.

In un runtime diretto, entrambi sarebbero relativamente la stessa performance. L'unica differenza è che quest'ultimo ha una migliore possibilità di aver riscaldato la cache usando lo stesso vtable due volte.

Tuttavia, in un test di looping, non sono sicuro che lo si possa notare, poiché dopo la prima iterazione la cache sarebbe calda a tutti e si dovrebbe esaurire la cache durante il ciclo per fare in modo che diventi un problema .

Oltre alla cache di dati per il vtable, la CPU eseguirà la previsione del ramo e lo farà bene in un test di looping. Quindi, avrai due meccanismi di caching che funzionano su questi dispacci. (Per non parlare della cache del codice.)

Vorrei andare con qualsiasi cosa rendesse il tuo codice più gestibile.

    
risposta data 02.06.2017 - 16:34
fonte