Nelle implementazioni C # e Java, gli oggetti in genere hanno un singolo puntatore alla sua classe. Ciò è possibile perché sono linguaggi a eredità singola. La struttura della classe contiene quindi la vtable per la gerarchia di ereditarietà singola. Ma chiamare i metodi di interfaccia ha anche tutti i problemi dell'ereditarietà multipla. Questo è in genere risolto inserendo vtables aggiuntivi per tutte le interfacce implementate nella struttura della classe. Ciò consente di risparmiare spazio rispetto alle tipiche implementazioni di ereditarietà virtuali in C ++, ma rende più complicata la distribuzione dei metodi di interfaccia, che può essere parzialmente compensata dalla memorizzazione nella cache.
Ad esempio nella JVM OpenJDK, ogni classe contiene un array di vtables per tutte le interfacce implementate (un'interfaccia vtable è chiamata itable ). Quando viene chiamato un metodo di interfaccia, questo array viene cercato linearmente per l'Itable di quell'interfaccia, quindi il metodo può essere inviato attraverso quell'Itable. La memorizzazione nella cache viene utilizzata in modo che ciascun sito di chiamata ricordi il risultato dell'invio del metodo, pertanto questa ricerca deve essere ripetuta solo quando il tipo di oggetto concreto cambia. Pseudocodice per l'invio del metodo:
// Dispatch SomeInterface.method
Method const* resolve_method(
Object const* instance, Klass const* interface, uint itable_slot) {
Klass const* klass = instance->klass;
for (Itable const* itable : klass->itables()) {
if (itable->klass() == interface)
return itable[itable_slot];
}
throw ...; // class does not implement required interface
}
(Confronta il codice reale nell'interprete HotSpot OpenJDK o nel compilatore x86 ).
C # (o più precisamente, il CLR) utilizza un approccio correlato. Tuttavia, qui gli itable non contengono puntatori ai metodi, ma sono mappe di slot: puntano a voci nella vtable principale della classe. Come per Java, la ricerca dell'Itable corretto è solo lo scenario peggiore e si prevede che la memorizzazione nella cache nel sito di chiamata possa evitare quasi sempre questa ricerca. Il CLR utilizza una tecnica chiamata Virtual Stub Dispatch per correggere il codice macchina compilato da JIT con diverse strategie di memorizzazione nella cache. pseudocodice:
Method const* resolve_method(
Object const* instance, Klass const* interface, uint interface_slot) {
Klass const* klass = instance->klass;
// Walk all base classes to find slot map
for (Klass const* base = klass; base != nullptr; base = base->base()) {
// I think the CLR actually uses hash tables instead of a linear search
for (SlotMap const* slot_map : base->slot_maps()) {
if (slot_map->klass() == interface) {
uint vtable_slot = slot_map[interface_slot];
return klass->vtable[vtable_slot];
}
}
}
throw ...; // class does not implement required interface
}
La differenza principale rispetto allo pseudocodice OpenJDK è che in OpenJDK ogni classe ha un array di tutte le interfacce implementate direttamente o indirettamente, mentre il CLR mantiene solo un array di mappe di slot per interfacce implementate direttamente in quella classe. Pertanto, dobbiamo spostare la gerarchia ereditaria verso l'alto fino a quando non viene trovata una mappa di slot. Per gerarchie ereditarie profonde ciò si traduce in un risparmio di spazio. Questi sono particolarmente rilevanti nel CLR a causa del modo in cui vengono implementati i generici: per una specializzazione generica, la struttura della classe viene copiata e i metodi nella tabella principale possono essere sostituiti da specializzazioni. Le mappe di slot continuano a puntare alle voci vtable corrette e possono quindi essere condivise tra tutte le specializzazioni generiche di una classe.
Come nota finale, ci sono più possibilità per implementare l'invio dell'interfaccia. Invece di posizionare il puntatore vtable / itable nell'oggetto o nella struttura della classe, possiamo usare puntatori grassi sull'oggetto, che sono fondamentalmente una (Object*, VTable*)
coppia. Lo svantaggio è che questo raddoppia la dimensione dei puntatori e che gli upcasts (da un tipo concreto a un tipo di interfaccia) non sono gratuiti. Ma è più flessibile, ha meno riferimenti indiretti e significa anche che le interfacce possono essere implementate esternamente da una classe. Gli approcci correlati sono utilizzati dalle interfacce Go, i tratti Rust e le macchine da scrivere Haskell.
Riferimenti e ulteriori letture:
- Wikipedia: cache in linea . Discute gli approcci di memorizzazione nella cache che possono essere utilizzati per evitare costose ricerche di metodi. In genere non è necessario per l'invio basato su vtable, ma è molto desiderabile per meccanismi di invio più costosi come le strategie di invio dell'interfaccia sopra riportate.
- OpenJDK Wiki (2013): Interface Calls . Discute su itables.
- Pobar, Neward (2009): Internals SSCLI 2.0. Il capitolo 5 del libro tratta in dettaglio le mappe delle slot. Non è mai stato pubblicato ma reso disponibile dagli autori sui loro blog . Da allora il link PDF è stato spostato. Questo libro probabilmente non riflette più lo stato attuale del CLR.
- CoreCLR (2006): Virtual Stub Dispatch . In: Book Of The Runtime. Discute le mappe delle slot e la memorizzazione nella cache per evitare ricerche costose.
- Kennedy, Syme (2001): Progettazione e implementazione di Generics per .NET Common Language Runtime . ( Collegamento PDF ). Discute vari approcci per implementare generici. I generici interagiscono con l'invio dei metodi perché i metodi potrebbero essere specializzati, quindi potrebbe essere necessario riscrivere i vtables.