Tabulka virtuálních metod

Aktuální verze stránky ještě nebyla zkontrolována zkušenými přispěvateli a může se výrazně lišit od verze recenzované 4. června 2015; kontroly vyžadují 24 úprav .

Tabulka virtuálních metod ( VMT ) - koordinační  tabulka nebo vtable - mechanismus používaný v programovacích jazycích pro podporu dynamického párování (nebo metody pozdní vazby).

Řekněme, že program obsahuje několik tříd v hierarchii dědičnosti: základní třídu Cat a dvě podtřídy DomesticCat a Lion. Třída Catdefinuje virtuální funkci speak , takže její podtřídy mohou poskytovat vhodnou implementaci (tj. „mňau“ nebo „řev“).

Když program volá metodu speakna ukazateli Cat(který může ukazovat na třídu Catnebo jakoukoli podtřídu Cat), kontextové prostředí (běhové prostředí) musí být schopno určit, která implementace je volána, v závislosti na aktuálním typu ukazovaného objektu.

Existuje mnoho různých způsobů, jak implementovat dynamické propojení, jako je toto, ale řešení virtuální tabulky je v C++ a příbuzných jazycích (jako D a C# ) zcela běžné. Jazyky, které mají oddělení mezi API objektu a jeho implementací, jako Visual Basic a Delphi , mají také tendenci používat analogy virtuálních tabulek, protože to umožňuje objektům používat jinou implementaci jednoduše pomocí jiné sady ukazatelů metod.

Implementace

Koordinační tabulka objektu obsahuje adresy dynamicky propojených metod objektu. Metoda je volána, když je adresa metody načtena z tabulky. Koordinační tabulka bude stejná pro všechny objekty patřící do stejné třídy, takže sdílení je povoleno. Objekty, které patří do typově kompatibilních tříd (například ty, které jsou na stejné úrovni v hierarchii dědičnosti), budou mít podobné koordinační tabulky: adresa dané metody bude pevně nastavena na stejném offsetu pro všechny typově kompatibilní třídy. Volbou adresy metody z dané koordinační tabulky pomocí offsetu tedy získáme metodu spojenou s aktuální třídou objektu. [jeden]

Standardy C++ jasně nedefinují, jak by měla být dynamická koordinace implementována, ale kompilátory často používají některé variace stejného základního modelu.

Obvykle kompilátor vytvoří samostatnou virtuální tabulku pro každou třídu. Po vytvoření objektu je přidán ukazatel na tuto virtuální tabulku, nazývaný ukazatel virtuální tabulky nebo vpointer (někdy také nazývaný vptr nebo vfptr), jako skrytý člen daného objektu (a často jako první člen). Kompilátor také generuje "skrytý" kód v konstruktoru každé třídy, aby inicializoval vpointery svého objektu s adresami odpovídající vtable.

Příklad

Zvažte následující deklarace tříd v C++:

třída B1 { veřejnost : void f0 () {} virtuální void f1 () {} int int_v_b1 ; }; třída B2 { veřejnost : virtuální void f2 () {} int int_v_b2 ; };

použijte k vytvoření následující třídy:

třída D : veřejná B1 , veřejná B2 { veřejnost : void d () {} void f2 () {} // přepíše B2::f2() int int_in_d ; };

a následující fragment kódu C++:

B2 * b2 = nové B2 (); D * d = nové D ();

G++ 3.4.6 ze sady GCC vytváří následující 32bitovou paměťovou mapu pro objekt b2 (здесь и далее ТВМ - таблица виртуальных методов): [nb 1]

b2: +0: ​​​​ukazatel na TVM B2 +4: hodnota int_in_b2 TVM B2: +0: ​​​​B2::f2()

a pro objekt dbude schéma paměti vypadat takto:

d: +0: ​​​​ukazatel na TVM D (pro B1) +4: hodnota int_in_b1 +8: ukazatel na TVM D (pro B2) +12: hodnota int_in_b2 +16: hodnota int_in_d Celková velikost: 20 bajtů. TVM D (pro B1): +0: ​​​​B1::f1() // B1::f1() není předefinováno TVM D (pro B2): +0: ​​​​D::f2() // B2::f2() nahrazeno D::f2()

Je třeba poznamenat, že nevirtuální funkce (jako je f0) se obecně nemohou objevit ve virtuální tabulce, ale v některých případech existují výjimky (jako je výchozí konstruktor).

Předefinování metody f2()ve třídě Dje implementováno duplikováním TCM B2a nahrazením ukazatele na B2::f2()ukazatelem na D::f2().

Vícenásobná dědičnost

Vícenásobné dědění tříd do B1a B2ze třídy Dpomocí dvou tabulek virtuálních metod, jedné pro každou základní třídu. (Existují další způsoby, jak implementovat vícenásobnou dědičnost, ale tento je nejběžnější.) To má za následek potřebu " ukazatelů na záznam adresy " (vazby) při vytváření.

Zvažte následující kód C++:

D * d = nové D (); B1 * b1 = dynamic_cast < B1 *> ( d ); B2 * b2 = dynamic_cast < B2 *> ( d );

Zatímco da b1ukazovat na jedno místo v paměti po provedení tohoto kódu, b2bude ukazovat na umístění v paměti d+8(posun osmi bajtů od umístění d). Ukazuje tedy b2na oblast paměti v d, která "vypadá" jako entita B2, tj. má stejné rozložení paměti jako entita B2.

Výzva

K volání d->f1()dojde, když je vpointer dereferencován D::B1z d: vyhledání položky o f1ve virtuální tabulce a poté dereferencování tohoto ukazatele volá kód.

V případě jediné dědičnosti (nebo v případě jazyka, který podporuje pouze jedinou dědičnost), pokud je vpointer vždy prvním prvkem v d(jako je tomu u mnoha kompilátorů), je to vyřešeno následujícím pseudo-C++ kódem :

* (( * d ) [ 0 ]) ( d )

V obecnějším případě, jak je uvedeno výše, bude volání f1(), D::f2()a B2::f2()dále dobtížnější

* (( d -> /*ukazatel D TBM (pro B1)*/ )[ 0 ])( d ) // d->f1(); * (( d -> /*ukazatel D TBM (pro B2)*/ )[ 0 ])( d + 8 ) // d->f2(); * (( /* adresa TVM B2 */ )[ 0 ])( d + 8 ) // d->B2::f2();

Ve srovnání s tím je volání d->f0()mnohem jednodušší:

* B1 :: f0 ( d )

Účinnost

Virtuální volání vyžaduje alespoň další indexovanou dereferenci a někdy další „opravu“ podobnou nevirtuálnímu volání, což je jednoduchý skok na zkompilovaný ukazatel. Volání virtuálních funkcí je proto ze své podstaty pomalejší než volání nevirtuálních. Experiment provedený v roce 1996 ukázal, že přibližně 6–13 % doby provádění je vynaloženo na pouhé hledání vhodné funkce, zatímco celkový nárůst doby provádění může dosáhnout 50 % [2] . Náklady na používání virtuálních funkcí na moderních architekturách procesorů nemusí být tak vysoké kvůli přítomnosti mnohem větších mezipamětí a lepší predikci větví .

V prostředí, kde se nepoužívá kompilace JIT , nemohou být volání virtuálních funkcí obvykle interní . I když je možné, aby kompilátor nahradil vyhledávání a nepřímé vyvolání, například podmíněným provedením každého vnitřního těla, taková optimalizace není běžná.

Aby se předešlo takovému plýtvání, kompilátoři se obvykle vyhýbají používání virtuálních tabulek, kdykoli lze provést volání v době kompilace.

Výše uvedené volání f1tedy nemusí vyžadovat vyhledání virtuální tabulky, protože kompilátor může hlásit pouze to, co dv daném okamžiku může mít D, Dnež předefinovat f1. Nebo kompilátor (nebo alternativně optimalizátor) může zjistit nepřítomnost podtříd B1v programu, který přepíše f1. Volání B1::f1nebo B2::f2pravděpodobně nebude vyžadovat vyhledání virtuální tabulky kvůli explicitní implementaci (ačkoli vazba na ukazatel 'toto' je stále vyžadována).

Srovnání s alternativami

Virtuální tabulka obecně obětuje výkon, aby dosáhla dynamického výběru, ale existuje k němu mnoho alternativ, jako je výběr binárního stromu, který má lepší výkon, ale jinou rychlost provádění [3] .

Virtuální tabulka je však poskytována pouze pro jedno odeslání se speciálním parametrem „this“, na rozdíl od vícenásobného odeslání (jako v CLOS nebo Dylan ), kde lze typy všech parametrů přiřadit během odesílání.

Virtuální tabulka také funguje pouze v případě, že je odeslání omezeno na známou sadu metod, takže mnoho virtuálních tabulek lze v době kompilace vložit do jednoduchého pole, na rozdíl od jazyků, které podporují psaní typu duck (jako je Smalltalk , Python nebo JavaScript ).

Jazyky, které podporují jednu nebo obě tyto možnosti, často odesílají vyhledáním řetězce v hašovací tabulce nebo jinou ekvivalentní metodou. Existuje několik triků, jak zlepšit rychlost (např. tokenizace názvů metod, použití ukládání do mezipaměti, kompilace JIT ) a čas odeslání často nemá významný dopad na celkovou dobu zpracování, ale i přes to je vyhledávání virtuálních tabulek znatelně rychlejší . . Virtuální tabulka se také snáze implementuje a ladí a je také blíže „filozofii C“ než odkaz na řetězcové hash tabulky? .

Viz také

Poznámky

  1. Argument G++ -fdump-class-hierarchylze použít k resetování TVM (pro „ruční“ kontrolu. Pro kompilátor AIX VisualAge XlC se používá -qdump_class_hierarchyk resetování hierarchie tříd a schématu TVF.

Poznámky

  1. Ellis & Stroustrup 1990, pp. 227–232
  2. Driesen, Karel a Hölzle, Urs, „Přímé náklady na volání virtuálních funkcí v C++“ Archivováno 10. srpna 2017 na Wayback Machine , OOPSLA 1996
  3. Zendra, Olivier a Driesen, Karel, "Stress-testing Control Structures for Dynamic Dispatch in Java" Archivováno 27. září 2007 na Wayback Machine , Pp. 105–118, Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02)

Odkazy