У реалізаціях C # та Java об'єкти, як правило, мають один вказівник на його клас. Це можливо, оскільки вони є одномовними спадковими мовами. Потім структура класу містить vtable для ієрархії одного успадкування. Але виклик методів інтерфейсу також має всі проблеми багаторазового успадкування. Зазвичай це вирішується шляхом додавання додаткових vtables для всіх реалізованих інтерфейсів у структуру класу. Це економить простір порівняно з типовими реалізаціями віртуального успадкування в C ++, але ускладнює розсилку методів інтерфейсу - які можна частково компенсувати кешуванням.
Наприклад, у JVM OpenJDK кожен клас містить масив vtables для всіх реалізованих інтерфейсів (інтерфейс vtable називається itable ). Коли викликається метод інтерфейсу, цей масив шукається лінійно для використання цього інтерфейсу, тоді метод може бути відправлений через цей itable. Кешування використовується таким чином, щоб кожен сайт виклику запам'ятовував результат відправки методу, тому цей пошук повинен повторюватися лише тоді, коли тип конкретного об'єкта змінюється. Псевдокод для відправки методу:
// 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
}
(Порівняйте реальний код в інтерпретаторі HotJS OpenJDK або компіляторі x86 .)
C # (а точніше, CLR) використовує пов'язаний підхід. Однак тут ітабелі не містять покажчиків на методи, а є картами слотів: вони вказують на записи в головному vtable класу. Як і у Java, пошук потрібних версій є найгіршим сценарієм, і очікується, що кешування на сайті виклику може уникнути цього пошуку майже завжди. CLR використовує техніку під назвою Virtual Stub Dispatch для того, щоб виправити JIT-компільований машинний код за допомогою різних стратегій кешування. Псевдокод:
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
}
Основна відмінність псевдокоду OpenJDK полягає в тому, що в OpenJDK кожен клас має масив усіх безпосередньо чи опосередковано реалізованих інтерфейсів, тоді як CLR зберігає лише масу слот-карт для інтерфейсів, які були безпосередньо реалізовані в цьому класі. Тому нам потрібно рухати ієрархію спадкування вгору, поки не буде знайдена карта слотів. Для ієрархій глибокого успадкування це призводить до економії місця. Вони особливо актуальні в CLR через те, як реалізуються дженерики: для загальної спеціалізації скопіюється структура класів, а методи в основному vtable можуть бути замінені спеціалізаціями. Карти слотів продовжують вказувати на правильні записи vtable, і тому їх можна ділити між усіма загальними спеціалізаціями класу.
Як закінчення, є більше можливостей для здійснення розсилки інтерфейсу. Замість того, щоб розміщувати покажчик vtable / itable в об'єкті або в структурі класу, ми можемо використовувати жирні покажчики на об’єкт, які в основному є (Object*, VTable*)
парою. Недолік полягає в тому, що це подвоює розмір покажчиків і те, що оновлення (від конкретного типу до типу інтерфейсу) не є безкоштовними. Але він більш гнучкий, має менше непрямості, а також означає, що інтерфейси можна реалізувати зовнішньо з класу. Пов'язані підходи використовуються інтерфейсами Go, рисовими ознаками та класами типу Haskell.
Посилання та додаткове читання:
- Вікіпедія: вбудоване кешування . Обговорюють підходи до кешування, які можна використовувати, щоб уникнути дорогого пошуку способу. Зазвичай не потрібна для відправки на основі Vtable, але дуже бажана для більш дорогих механізмів диспетчеризації, як описано вище.
- OpenJDK Wiki (2013): інтерфейсні дзвінки . Обговорюйте ідентифікатори.
- Pobar, Neward (2009): SSCLI 2.0 Internals. Глава 5 книги дуже детально розглядає карти слотів. Ніколи не публікувався, але авторами не був доступний у своїх блогах . Посилання PDF з цього моменту перейшло. Ця книга, ймовірно, більше не відображає поточний стан CLR.
- CoreCLR (2006): віртуальна диспетчерська робота . В: Книга виконання. Обговорюйте карти слотів та кешування, щоб уникнути дорогих пошукових запитів.
- Кеннеді, Сайм (2001): Розробка та впровадження загальної інформації для .NET Common Language Runtime . ( Посилання в PDF ). Обговорюють різні підходи до впровадження генерики. Генеріки взаємодіють з відправленням методу, оскільки методи можуть бути спеціалізованими, тому vtables, можливо, доведеться переписувати.