18

Speriamo che questo non è troppo specializzato di una domanda per lo StackOverflow: se è e potrebbe essere migrato altrove fatemi sapere ...C++: specializzazione di classe una trasformazione valida per un compilatore conforme?

molte lune fa, ho scritto una tesi di laurea che propone varie tecniche devirtualization per C++ e linguaggi correlati, generalmente basati sull'idea della specializzazione precompilata di percorsi di codice (un po 'come i modelli) ma con i controlli per scegliere le specializzazioni corrette vengono scelti in fase di esecuzione nei casi in cui non possono essere selezionati in fase di compilazione (come devono essere i modelli).

Il (molto) idea di base è qualcosa di simile a quanto segue ... si supponga di avere una classe C come la seguente:

class C : public SomeInterface 
{ 
public: 
    C(Foo * f) : _f(f) { } 

    virtual void quack() 
    { 
     _f->bark(); 
    } 

    virtual void moo() 
    { 
     quack(); // a virtual call on this because quack() might be overloaded 
    } 

    // lots more virtual functions that call virtual functions on *_f or this 

private: 
    Foo * const _f; // technically doesn't have to be const explicitly 
        // as long as it can be proven not be modified 
}; 

E si sapeva che esistono sottoclassi concrete di Foo come FooA, FooB, ecc., con tipi completi conosciuti (senza necessariamente avere una lista esaustiva), quindi è possibile precompilare le versioni specializzate di Foo per alcune sottoclassi selezionate di Foo, ad esempio (si noti che il costruttore non è incluso qui, di proposito, poiché non essere chiamato):

class C_FooA final : public SomeInterface 
{ 
public: 
    virtual void quack() final 
    { 
     _f->FooA::bark(); // non-polymorphic, statically bound 
    } 

    virtual void moo() final 
    { 
     C_FooA::quack(); // also static, because C_FooA is final 
     // _f->FooA::bark(); // or you could even do this instead 
    } 

    // more virtual functions all specialized for FooA (*_f) and C_FooA (this) 

private: 
    FooA * const _f; 
}; 

e sostituire il costruttore di C con qualcosa come il seguente:

C::C(Foo * f) : _f(f) 
{ 
    if(f->vptr == vtable_of_FooA) // obviously not Standard C++ 
     this->vptr = vtable_of_C_FooA; 
    else if(f->vptr == vtable_of_FooB) 
     this->vptr = vtable_of_C_FooB; 
    // otherwise leave vptr unchanged for all other values of f->vptr 
} 

Quindi, in pratica, il tipo dinamico dell'oggetto in costruzione viene modificata in base al tipo dinamica degli argomenti al suo costruttore. (Nota, non puoi farlo con i modelli perché puoi creare solo un C<Foo> se conosci il tipo di f in fase di compilazione). D'ora in poi, qualsiasi chiamata a FooA::bark() attraverso C::quack() comporta una sola chiamata virtuale: o la chiamata a C::quack() è legato staticamente alla versione non-specializzato che richiama dinamicamente FooA::bark(), o la chiamata a C::quack() viene inoltrata in modo dinamico per C_FooA::quack() che chiama staticamente FooA::bark(). Inoltre, la spedizione dinamica potrebbe essere completamente eliminata in alcuni casi se l'analizzatore di flusso dispone di informazioni sufficienti per effettuare una chiamata statica a C_FooA::quack(), che potrebbe essere molto utile in un ciclo stretto se consente di eseguire l'inlining. (Anche se tecnicamente a quel punto probabilmente staresti bene anche senza questa ottimizzazione ...)

(nota che questa trasformazione è sicura, anche se meno utile, anche se _f è non-const e protetta invece che privata e C è ereditato da una diversa unità di traduzione ... l'unità di traduzione che crea il vtable per la classe ereditata non saprà nulla delle specializzazioni e il costruttore della classe ereditata imposterà semplicemente lo this->vptr sul proprio vtable, che non lo farà fai riferimento a funzioni specializzate perché non ne saprà nulla.)

Questo potrebbe sembrare un grande sforzo per eliminare un livello di riferimento indiretto, ma il punto è che puoi farlo ad un y livello di nidificazione arbitrario (qualsiasi profondità delle chiamate virtuali che seguono questo modello potrebbe essere ridotta a una) basata solo su informazioni locali all'interno di un'unità di traduzione, e farlo in modo resiliente anche se i nuovi tipi sono definiti in altre unità di traduzione che non si Lo so ... potresti aggiungere un sacco di codice in abbondanza che non avresti altrimenti se lo avessi fatto in modo ingenuo.

In ogni caso, indipendentemente dal fatto che questo tipo di ottimizzazione sarebbe davvero avere abbastanza bang-for-the-buck valere lo sforzo di attuazione e anche la pena lo spazio in testa nella eseguibile risultante, la mia domanda è, c'è qualcosa in Standard C++ che impedirebbe a un compilatore di eseguire tale trasformazione?

La mia sensazione è no, dal momento che lo standard non specifica affatto come viene eseguita la distribuzione virtuale o come vengono rappresentate le funzioni dei puntatori a membro. Sono abbastanza sicuro che non ci sia nulla nel meccanismo RTTI che impedisca a C e C_FooA di mascherarsi come lo stesso tipo per tutti gli scopi, anche se hanno tabelle virtuali diverse. L'unica altra cosa che potrei pensare potrebbe essere una lettura attenta dell'ODR, ma probabilmente no.

Sto trascurando qualcosa? Escludendo i problemi di collegamento/ABI, sarebbero possibili trasformazioni come questa senza rompere i programmi conformi C++? (Inoltre, se sì, questo potrebbe essere fatto al momento con l'Itanium e/o MSVC ABI Sono abbastanza sicuro che la risposta non è sì, come pure, ma si spera che qualcuno può confermare?).

EDIT: Does qualcuno sa se qualcosa di simile è implementato in qualsiasi compilatore/JIT mainstream per C++, Java o C#? (Vedi discussione e chat collegata nei commenti sottostanti ...) Sono consapevole che le JIT fanno speculazioni statiche-vincolanti/inlining di virtuals direttamente sui siti di chiamata, ma non so se facciano qualcosa del genere (con vtables completamente nuovi essere generato e scelto in base a un singolo controllo di tipo eseguito dal costruttore, piuttosto che in ogni sito di chiamata).

+0

Non è più o meno la stessa cosa che creare un modello ? – cooky451

+0

Sì, ma non è possibile scegliere una specializzazione in fase di esecuzione a meno che non lo si faccia manualmente ... è possibile scegliere solo un modello specifico in fase di compilazione, ovvero non è possibile eseguire il wrapping del polimorfismo in fase di compilazione con il polimorfismo di esecuzione senza eseguire il tipo controlla te stesso manualmente, che è soggetto a errori e fragile. Questo metodo consiste essenzialmente nel fare il compilatore per te, in base alle informazioni di runtime. –

+0

Beh, il giusto foo deve essere scelto manualmente comunque, quindi non vedo davvero la differenza. Bene, ora è possibile inserire più C con diversi foos in un contenitore, ma questo probabilmente non accadrà mai poiché C ha funzioni virtuali di per sé e probabilmente un contenitore >. – cooky451

risposta

1

C'è qualcosa in Standard C++ che impedisca a un compilatore di eseguire tale trasformazione?

Se non si è sicuri che il comportamento osservabile sia invariato, è la "regola as-if" che è la sezione Standard 1.9.

Ma questo potrebbe fare dimostrando che la trasformazione è corretto piuttosto difficile: 12,7/4:

Quando una funzione virtuale viene chiamato direttamente o indirettamente da un costruttore (compreso il mem-inizializzatore o brace -o-equal-initializer per un membro dati non statico) o da un distruttore e l'oggetto a cui si applica la chiamata è l'oggetto in costruzione o distruzione, la funzione chiamata è quella definita nella classe del costruttore o del distruttore o in una delle sue basi, ma non una funzione che lo sovrascrive in una classe derivata dalla classe del costruttore o del distruttore, o o verriderlo in una delle altre classi base dell'oggetto più derivato.

Quindi, se il distruttore Foo::~Foo() capita di chiamare direttamente o indirettamente C::quack() su un oggetto c, dove c._f punti a l'oggetto che viene distrutto, è necessario chiamare Foo::bark(), anche se _f era un FooA quando si costruito oggetto c.

+0

Sì, sembra che sia necessario l'origine di tutti i distruttori virtuali non puri di tutte le classi base di 'FooA' disponibili nel TU per fare questo, quindi, e lo fai solo se puoi provare che non ci sono chiamate da lì a nessuna funzione virtuale di 'C'. Scomodo, ma possibile lavorare con. Ad ogni modo, grazie, è stato utile ... qualche altro buco a cui puoi pensare? –

+0

Darn, devi anche proteggerti dal fatto che il costruttore di 'C' viene chiamato durante la costruzione di una sottoclasse di' FooA' ... Penso che tu possa aggirare questo problema, ma è necessario che tu controlli 'FooA' e fornire un meccanismo che permetta a 'C' di verificare che' FooA' sia completamente costruito. –

+0

"non-puro" è irrilevante qui. Un distruttore virtuale puro ha una definizione e si chiama proprio come qualsiasi altro e può causare esattamente lo stesso scenario. – aschepler

0

In prima lettura, sembra una variazione focalizzata in C++ di polymorphic inline caching. Penso che sia V8 che Oracle JVM lo usano entrambi e so che è .NET does.

Per rispondere alla tua domanda iniziale: non credo ci sia nulla nello standard che proibisca questo tipo di implementazioni. C++ prende abbastanza seriamente la regola "così com'è"; Fintanto che implementate fedelmente la semantica giusta, potete eseguire l'implementazione in qualsiasi modo pazzo che vi piace. C++ le chiamate virtuali non sono molto complicate, quindi dubito che tu possa inciampare in qualsiasi caso limite (diversamente se, per esempio, stavi cercando di fare qualcosa di intelligente con vincolante statico).

+0

Il caching in linea polimorfico AFAIK viene eseguito solo nel sito di chiamata, tuttavia, giusto, quindi i controlli di sicurezza devono ancora essere eseguiti per chiamata? (Questa tecnica esegue un solo controllo di guardia al costruttore, se la classe soddisfa determinati schemi, e successivamente non subisce alcun sovraccarico). –

+0

Sì, ma in pratica se hai un PIC hai anche un JIT, quindi se puoi dimostrare in modo statico che il bersaglio ha un particolare tipo, allora puoi compilare anche il ramo. Ma non sono sicuro che ti daresti fastidio; le filiali sono economiche a patto che siano facili da prevedere, quindi finché il tipo è sempre lo stesso, vinci. –

+0

Tuttavia non è sempre lo stesso tipo, questo è il problema. È lo stesso tipo per un singolo oggetto dato, ma lo stesso codice potrebbe essere chiamato in alternativa tra molti oggetti diversi che producono bersagli diversi, rovinando la previsione del ramo. Questo specializza l'intera classe per ogni tipo, quindi ogni classe può prevedere il ramo in modo indipendente. –