Virtuell metodtabell

Den aktuella versionen av sidan har ännu inte granskats av erfarna bidragsgivare och kan skilja sig väsentligt från versionen som granskades den 4 juni 2015; kontroller kräver 24 redigeringar .

Virtuell metodtabell ( VMT) - en koordinerande  tabell eller vtable - en mekanism som används i programmeringsspråk för att stödja dynamisk matchning (eller sen bindningsmetod).

Låt oss säga att ett program innehåller flera klasser i en arvshierarki: en basklass Cat och två underklasser DomesticCat och Lion. Klassen Catdefinierar en virtuell funktion speak så att dess underklasser kan tillhandahålla den lämpliga implementeringen (d.v.s. "mjau" eller "vrål").

När ett program anropar en metod speakpå en pekare Cat(som kan peka på en klass Cateller vilken underklass Cat), måste kontextmiljön (runtime-miljön) kunna avgöra vilken implementering som anropas, beroende på den aktuella typen av det spetsiga objektet.

Det finns många olika sätt att implementera dynamisk länkning som detta, men den virtuella tabelllösningen är ganska vanlig i C++ och relaterade språk (som D och C# ). Språk som har en separation mellan ett objekts API och dess implementering, som Visual Basic och Delphi , tenderar också att använda virtuella tabellanaloger, eftersom detta tillåter objekt att använda en annan implementering helt enkelt genom att använda en annan uppsättning metodpekare.

Implementering

Ett objekts koordineringstabell innehåller adresserna till objektets dynamiskt länkade metoder. Metoden anropas när adressen till metoden hämtas från tabellen. Koordineringstabellen kommer att vara densamma för alla objekt som tillhör samma klass, så delning är tillåten. Objekt som tillhör typkompatibla klasser (till exempel de som är på samma nivå i arvshierarkin) kommer att ha liknande koordineringstabeller: adressen för en given metod kommer att fixeras med samma offset för alla typkompatibla klasser. Således, genom att välja adressen till en metod från den givna koordineringstabellen med en offset, får vi metoden associerad med den aktuella klassen för objektet. [ett]

C++-standarderna definierar inte tydligt hur dynamisk koordinering ska implementeras, men kompilatorer använder ofta någon variant av samma grundmodell.

Vanligtvis skapar kompilatorn en separat virtuell tabell för varje klass. Efter att objektet har skapats läggs en pekare till den virtuella tabellen, kallad en virtuell tabellpekare eller vpointer (även ibland kallad vptr eller vfptr), som en dold medlem av det objektet (och ofta som den första medlemmen). Kompilatorn genererar också "dold" kod i konstruktorn för varje klass för att initiera dess objekts vpointers med adresserna till motsvarande vtabell.

Exempel

Tänk på följande klassdeklarationer i C++:

klass B1 { offentliga : void f0 () {} virtuellt tomrum f1 () {} int int_in_b1 ; }; klassB2 { _ offentliga : virtuellt tomrum f2 () {} int int_in_b2 ; };

använd för att skapa följande klass:

klass D : offentlig B1 , offentlig B2 { offentliga : void d () {} void f2 () {} // åsidosätt B2::f2() int int_in_d ; };

och följande C++-kodavsnitt:

B2 * b2 = ny B2 (); D * d = nytt D ();

G++ 3.4.6 från GCC -sviten skapar följande 32-bitars minneskarta för objekt b2 (здесь и далее ТВМ - таблица виртуальных методов): [anm 1]

b2: +0: ​​pekare till TVM B2 +4: int_in_b2 värde TVM B2: +0: ​​​​B2::f2()

och för objektet dkommer minnesschemat att vara så här:

d: +0: ​​pekare till TVM D (för B1) +4: int_in_b1 värde +8: pekare till TVM D (för B2) +12: int_in_b2 värde +16: int_in_d värde Total storlek: 20 byte. TVM D (för B1): +0: ​​​​B1::f1() // B1::f1() omdefinieras inte TVM D (för B2): +0:​D::f2() // B2::f2() ersatt av D::f2()

Det bör noteras att icke-virtuella funktioner (som f0) i allmänhet inte kan visas i en virtuell tabell, men det finns undantag i vissa fall (som standardkonstruktorn).

Omdefiniering av en metod f2()i en klass Dimplementeras genom att duplicera TCM B2och ersätta pekaren till med en B2::f2()pekare till D::f2().

Multipelt arv

Multipelt arv av klasser till B1och B2från klassen Dmed två virtuella metodtabeller, en för varje basklass. (Det finns andra sätt att implementera multipelt arv, men detta är det vanligaste.) Detta resulterar i behovet av " pekare till adresspost " (bindningar) vid skapande.

Tänk på följande C++-kod:

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

While doch b1peka på en plats i minnet efter exekvering av denna kod b2kommer att peka på en minnesplats d+8(en förskjutning på åtta byte från plats d). Pekar alltså b2på en minnesregion inom d, som "ser ut" som en entitet B2, dvs. har samma minneslayout som enheten B2.

Utmana

Anropet d->f1()inträffar när vpointern är avreferens D::B1från d: letar upp o-posten f1i den virtuella tabellen och sedan avreferenser den pekaren anropar koden.

När det gäller enkelarv (eller i fallet med ett språk som endast stöder enkelarv), om vpointer alltid är det första elementet i d(som är fallet med många kompilatorer), så löses detta med följande pseudo-C++-kod :

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

I ett mer allmänt fall, som nämnts ovan, f1()blir det svårare att ringa D::f2()och B2::f2()påd

* (( d -> /*TBM-pekare D (för B1)*/ )[ 0 ])( d ) // d->f1(); * (( d -> /*TBM-pekare D (för B2)*/ )[ 0 ])( d + 8 ) // d->f2(); * (( /* adress till TVM B2 */ )[ 0 ])( d + 8 ) // d->B2::f2();

I jämförelse är samtalet d->f0()mycket enklare:

* B1 :: f0 ( d )

Effektivitet

Ett virtuellt samtal kräver åtminstone en extra indexerad dereferens, och ibland en extra "fixup" som liknar ett icke-virtuellt samtal, vilket är ett enkelt hopp till en kompilerad pekare. Därför är det långsammare att anropa virtuella funktioner än att anropa icke-virtuella. Ett experiment som genomfördes 1996 visade att ungefär 6-13 % av exekveringstiden går åt till att helt enkelt söka efter lämplig funktion, medan den totala ökningen av exekveringstiden kan nå 50 % [2] . Kostnaden för att använda virtuella funktioner på moderna processorarkitekturer kanske inte är lika hög på grund av förekomsten av mycket större cacher och bättre förutsägelse av grenar .

I en miljö där JIT -kompilering inte används kan virtuella funktionsanrop vanligtvis inte vara interna . Även om det är möjligt för kompilatorn att ersätta lookup och indirekt anrop, till exempel genom att villkorligt exekvera varje intern kropp, är en sådan optimering inte vanlig.

För att undvika sådant slöseri undviker kompilatorer vanligtvis att använda virtuella tabeller närhelst ett anrop kan göras vid kompileringstillfället.

Sålunda kan det hända att ovanstående anrop f1inte kräver en uppslagning av den virtuella tabellen, eftersom kompilatorn bara kan rapportera vad den dkan ha vid den tidpunkten D, snarare Dän att omdefiniera f1. Eller kompilatorn (eller alternativt optimeraren) kan upptäcka frånvaron av underklasser B1i programmet som åsidosätter f1. Att anropa B1::f1eller B2::f2kommer förmodligen inte att kräva en uppslagning av den virtuella tabellen på grund av den explicita implementeringen (även om bindning av "den här"-pekaren fortfarande krävs).

Jämförelse med alternativ

Den virtuella tabellen offrar generellt prestanda för att uppnå dynamiskt urval, men det finns många alternativ till det, såsom binärt trädval, som har bättre prestanda men olika exekveringshastigheter [3] .

Den virtuella tabellen tillhandahålls dock endast för enstaka sändningar på den speciella "detta" parametern, till skillnad från multipel sändning (som i CLOS eller Dylan ), där typerna av alla parametrar kan tilldelas under sändningen.

En virtuell tabell fungerar också bara om sändningen är begränsad till en känd uppsättning metoder, så många virtuella tabeller kan placeras i en enkel array vid kompilering, till skillnad från språk som stöder duck typing (som Smalltalk , Python eller JavaScript ).

Språk som stöder ett eller båda av dessa alternativ skickas ofta genom att slå upp en sträng i en hashtabell, eller någon annan likvärdig metod. Det finns en hel del knep för att förbättra hastigheten (t.ex. tokenisering av metodnamn, applicering av cachning, JIT -kompilering) och sändningstiden har ofta inte någon betydande inverkan på den totala bearbetningstiden, men trots detta är uppslagningar av virtuella tabeller märkbart snabbare . . En virtuell tabell är också lättare att implementera och felsöka, och är också närmare "C-filosofin" än stränghashtabeller länk? .

Se även

Anteckningar

  1. Argumentet G++ -fdump-class-hierarchykan användas för att återställa TVM (för "manuell" kontroll. För AIX VisualAge XlC-kompilatorn används det för -qdump_class_hierarchyatt återställa klasshierarkin och TVF-schemat.

Anteckningar

  1. Ellis & Stroustrup 1990, s. 227–232
  2. Driesen, Karel och Hölzle, Urs, "The Direct Cost of Virtual Function Calls in C++" Arkiverad 10 augusti 2017 på Wayback Machine , OOPSLA 1996
  3. Zendra, Olivier och Driesen, Karel, "Stress-testing Control Structures for Dynamic Dispatch in Java" Arkiverad 27 september 2007 på Wayback Machine , s. 105–118, Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02)

Länkar