W implementacjach C # i Java obiekty zwykle mają pojedynczy wskaźnik do swojej klasy. Jest to możliwe, ponieważ są to języki jednego dziedzictwa. Struktura klas zawiera następnie vtable dla hierarchii pojedynczego dziedziczenia. Ale wywoływanie metod interfejsu ma również wszystkie problemy związane z wielokrotnym dziedziczeniem. Zwykle rozwiązuje się to poprzez umieszczenie dodatkowych tabel vtable dla wszystkich zaimplementowanych interfejsów w strukturze klasy. Oszczędza to miejsce w porównaniu z typowymi implementacjami wirtualnego dziedziczenia w C ++, ale komplikuje wysyłanie metod interfejsu - co można częściowo skompensować przez buforowanie.
Np. W OpenJDK JVM każda klasa zawiera tablicę vtables dla wszystkich zaimplementowanych interfejsów (interfejs vtable nazywa się itable ). Gdy wywoływana jest metoda interfejsu, tablica jest przeszukiwana liniowo pod kątem itable tego interfejsu, a następnie metoda może zostać wysłana przez tę itable. Buforowanie jest używane, aby każda strona wywołująca zapamiętała wynik wysłania metody, więc to wyszukiwanie musi być powtórzone tylko wtedy, gdy zmienia się konkretny typ obiektu. Pseudokod dla wysyłki metody:
// 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
}
(Porównaj prawdziwy kod w interpreterie OpenJDK HotSpot lub kompilatorze x86 ).
C # (a ściślej CLR) stosuje podobne podejście. Jednak tutaj itables nie zawierają wskaźników do metod, ale są mapami gniazd: wskazują na wpisy w głównej tabeli vt klasy. Podobnie jak w przypadku Javy, konieczność znalezienia właściwej opcji jest tylko najgorszym scenariuszem i oczekuje się, że buforowanie w witrynie wywołującej może prawie zawsze uniknąć tego wyszukiwania. CLR używa techniki o nazwie Virtual Stub Dispatch w celu łatania skompilowanego kodu maszynowego JIT za pomocą różnych strategii buforowania. Pseudo kod:
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
}
Główną różnicą w stosunku do pseudokodu OpenJDK jest to, że w OpenJDK każda klasa ma tablicę wszystkich bezpośrednio lub pośrednio zaimplementowanych interfejsów, podczas gdy CLR zachowuje tylko tablicę map gniazd dla interfejsów, które zostały bezpośrednio zaimplementowane w tej klasie. Dlatego musimy podążać hierarchią dziedziczenia w górę, dopóki nie zostanie znaleziona mapa miejsca. W przypadku hierarchii głębokiego dziedziczenia powoduje to oszczędność miejsca. Są one szczególnie istotne w CLR ze względu na sposób implementacji generics: w przypadku specjalizacji ogólnej struktura klasy jest kopiowana, a metody w głównej tabeli można zastąpić specjalizacjami. Mapy gniazd nadal wskazują prawidłowe wpisy vtable i dlatego mogą być współużytkowane przez wszystkie ogólne specjalizacje klasy.
Na koniec, istnieje więcej możliwości zaimplementowania wysyłki interfejsu. Zamiast umieszczać wskaźnik vtable / itable w obiekcie lub w strukturze klasy, możemy użyć wskaźników tłuszczu do obiektu, które są w zasadzie (Object*, VTable*)
parą. Wadą jest to, że podwaja rozmiar wskaźników i że upcasty (od konkretnego typu do typu interfejsu) nie są darmowe. Ale jest bardziej elastyczny, ma mniej pośredni, a także oznacza, że interfejsy mogą być implementowane zewnętrznie z klasy. Podobne podejścia są stosowane w interfejsach Go, cechach Rust i typach klas Haskell.
Referencje i dalsza lektura:
- Wikipedia: buforowanie wbudowane . Omówiono metody buforowania, których można użyć, aby uniknąć kosztownego wyszukiwania metod. Zwykle nie jest potrzebny do wysyłki opartej na tabeli, ale jest bardzo pożądany w przypadku droższych mechanizmów wysyłki, takich jak powyższe strategie wysyłki interfejsu.
- OpenJDK Wiki (2013): Połączenia interfejsu . Omawia itables.
- Pobar, Neward (2009): SSCLI 2.0 Internals. Rozdział 5 książki szczegółowo omawia mapy automatów. Nigdy nie został opublikowany, ale udostępniony przez autorów na ich blogach . Link PDF od tego czasu przeniósł. Ta książka prawdopodobnie nie odzwierciedla już obecnego stanu CLR.
- CoreCLR (2006): Virtual Stub Dispatch . W: Book Of Runtime. Omawia mapy miejsc i pamięć podręczną, aby uniknąć kosztownych wyszukiwań.
- Kennedy, Syme (2001): Projektowanie i implementacja generics dla środowiska uruchomieniowego .NET Common Language . ( Link PDF ). Omawia różne podejścia do implementacji generycznych. Generics wchodzi w interakcję z wysyłaniem metod, ponieważ metody mogą być wyspecjalizowane, więc vtables może wymagać przepisania.