Czy posiadanie jednej funkcji wirtualnej spowalnia całą klasę?
Czy tylko wywołanie funkcji wirtualnej? I czy ma to wpływ na prędkość, jeśli funkcja wirtualna zostanie faktycznie nadpisana, czy nie, czy też nie ma to żadnego wpływu, o ile jest wirtualna.
Posiadanie funkcji wirtualnych spowalnia całą klasę o tyle, o ile trzeba zainicjować, skopiować,… kiedy mamy do czynienia z obiektem takiej klasy, jeszcze jeden element danych. W przypadku klasy liczącej mniej więcej pół tuzina członków różnica powinna być pomijalna. W przypadku klasy, która zawiera tylko jednego char
członka lub nie zawiera żadnych elementów członkowskich, różnica może być zauważalna.
Poza tym należy zauważyć, że nie każde wywołanie funkcji wirtualnej jest wywołaniem funkcji wirtualnej. Jeśli masz obiekt znanego typu, kompilator może wyemitować kod dla normalnego wywołania funkcji, a nawet może wbudować tę funkcję, jeśli ma na to ochotę. Tylko wtedy, gdy wykonujesz wywołania polimorficzne, za pomocą wskaźnika lub referencji, które mogą wskazywać na obiekt klasy bazowej lub obiekt jakiejś klasy pochodnej, potrzebujesz pośredniej vtable i płacisz za nią w kategoriach wydajności.
struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
Foo x; x.a(); // non-virtual: always calls Foo::a()
Bar y; y.a(); // non-virtual: always calls Bar::a()
arg.a(); // virtual: must dispatch via vtable
Foo z = arg; // copy constructor Foo::Foo(const Foo&) will convert to Foo
z.a(); // non-virtual Foo::a, since z is a Foo, even if arg was not
}
Kroki, które musi wykonać sprzęt, są zasadniczo takie same, niezależnie od tego, czy funkcja zostanie nadpisana, czy nie. Adres tabeli vtable jest odczytywany z obiektu, wskaźnik funkcji jest pobierany z odpowiedniego gniazda, a funkcja wywoływana przez wskaźnik. Jeśli chodzi o rzeczywistą wydajność, pewien wpływ mogą mieć prognozy branżowe. Na przykład, jeśli większość twoich obiektów odwołuje się do tej samej implementacji danej funkcji wirtualnej, to istnieje pewna szansa, że predyktor rozgałęzienia poprawnie przewidział, którą funkcję wywołać, nawet zanim wskaźnik zostanie pobrany. Ale nie ma znaczenia, która funkcja jest powszechna: może to być większość obiektów delegowanych do niepodpisanego przypadku podstawowego lub większość obiektów należących do tej samej podklasy, a zatem delegujących do tego samego nadpisanego przypadku.
jak są wdrażane na głębokim poziomie?
Podoba mi się pomysł jheriko, aby zademonstrować to za pomocą próbnej implementacji. Ale użyłbym C do zaimplementowania czegoś podobnego do powyższego kodu, aby niski poziom był łatwiejszy do zobaczenia.
klasa nadrzędna Foo
typedef struct Foo_t Foo; // forward declaration
struct slotsFoo { // list all virtual functions of Foo
const void *parentVtable; // (single) inheritance
void (*destructor)(Foo*); // virtual destructor Foo::~Foo
int (*a)(Foo*); // virtual function Foo::a
};
struct Foo_t { // class Foo
const struct slotsFoo* vtable; // each instance points to vtable
};
void destructFoo(Foo* self) { } // Foo::~Foo
int aFoo(Foo* self) { return 1; } // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
0, // no parent class
destructFoo,
aFoo
};
void constructFoo(Foo* self) { // Foo::Foo()
self->vtable = &vtableFoo; // object points to class vtable
}
void copyConstructFoo(Foo* self,
Foo* other) { // Foo::Foo(const Foo&)
self->vtable = &vtableFoo; // don't copy from other!
}
klasa pochodna Bar
typedef struct Bar_t { // class Bar
Foo base; // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { } // Bar::~Bar
int aBar(Bar* self) { return 2; } // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
&vtableFoo, // can dynamic_cast to Foo
(void(*)(Foo*)) destructBar, // must cast type to avoid errors
(int(*)(Foo*)) aBar
};
void constructBar(Bar* self) { // Bar::Bar()
self->base.vtable = &vtableBar; // point to Bar vtable
}
funkcja f wykonująca wywołanie funkcji wirtualnej
void f(Foo* arg) { // same functionality as above
Foo x; constructFoo(&x); aFoo(&x);
Bar y; constructBar(&y); aBar(&y);
arg->vtable->a(arg); // virtual function call
Foo z; copyConstructFoo(&z, arg);
aFoo(&z);
destructFoo(&z);
destructBar(&y);
destructFoo(&x);
}
Jak więc widać, vtable to tylko statyczny blok w pamięci, zawierający głównie wskaźniki do funkcji. Każdy obiekt klasy polimorficznej będzie wskazywał na tabelę vtable odpowiadającą jego typowi dynamicznemu. To również sprawia, że połączenie między RTTI a funkcjami wirtualnymi jest wyraźniejsze: możesz sprawdzić, jakiego typu jest klasa, po prostu patrząc, na którą vtable wskazuje. Powyższe jest uproszczone na wiele sposobów, jak np. Dziedziczenie wielokrotne, ale ogólna koncepcja jest rozsądna.
Jeśli arg
jest typu Foo*
i bierzesz arg->vtable
, ale w rzeczywistości jest obiektem typu Bar
, nadal uzyskujesz poprawny adres vtable
. Dzieje się tak dlatego, że vtable
jest zawsze pierwszym elementem pod adresem obiektu, bez względu na to, czy jest wywoływany, vtable
czy base.vtable
w poprawnie wpisanym wyrażeniu.
Inside the C++ Object Model
wgStanley B. Lippman
. (Rozdział 4.2, strony 124-131)