Verlangsamt eine einzige virtuelle Funktion die gesamte Klasse?
Oder nur der Aufruf der virtuellen Funktion? Und wird die Geschwindigkeit beeinträchtigt, wenn die virtuelle Funktion tatsächlich überschrieben wird oder nicht, oder hat dies keine Auswirkung, solange sie virtuell ist?
Virtuelle Funktionen verlangsamen die gesamte Klasse, sofern ein weiteres Datenelement initialisiert, kopiert usw. werden muss, wenn es sich um ein Objekt einer solchen Klasse handelt. Für eine Klasse mit etwa einem halben Dutzend Mitgliedern sollte der Unterschied vernachlässigbar sein. Für eine Klasse, die nur ein einzelnes char
Mitglied oder überhaupt keine Mitglieder enthält , kann der Unterschied erheblich sein.
Abgesehen davon ist zu beachten, dass nicht jeder Aufruf einer virtuellen Funktion ein virtueller Funktionsaufruf ist. Wenn Sie ein Objekt eines bekannten Typs haben, kann der Compiler Code für einen normalen Funktionsaufruf ausgeben und diese Funktion sogar inline einbinden, wenn er dies wünscht. Nur wenn Sie polymorphe Aufrufe über einen Zeiger oder eine Referenz ausführen, die möglicherweise auf ein Objekt der Basisklasse oder auf ein Objekt einer abgeleiteten Klasse verweisen, benötigen Sie die Indirektion vtable und zahlen für die Leistung.
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
}
Die Schritte, die die Hardware ausführen muss, sind im Wesentlichen dieselben, unabhängig davon, ob die Funktion überschrieben wird oder nicht. Die Adresse der vtable wird aus dem Objekt gelesen, der Funktionszeiger aus dem entsprechenden Slot abgerufen und die Funktion vom Zeiger aufgerufen. In Bezug auf die tatsächliche Leistung können Branchenvorhersagen einige Auswirkungen haben. Wenn sich die meisten Ihrer Objekte beispielsweise auf dieselbe Implementierung einer bestimmten virtuellen Funktion beziehen, besteht eine gewisse Wahrscheinlichkeit, dass der Verzweigungsprädiktor korrekt vorhersagt, welche Funktion aufgerufen werden soll, noch bevor der Zeiger abgerufen wurde. Es spielt jedoch keine Rolle, welche Funktion die häufigste ist: Es können die meisten Objekte sein, die an den nicht überschriebenen Basisfall delegieren, oder die meisten Objekte, die zu derselben Unterklasse gehören und daher an denselben überschriebenen Fall delegieren.
Wie werden sie auf einer tiefen Ebene umgesetzt?
Ich mag die Idee von Jheriko, dies anhand einer Scheinimplementierung zu demonstrieren. Aber ich würde C verwenden, um etwas zu implementieren, das dem obigen Code ähnelt, damit der niedrige Pegel leichter erkennbar ist.
Elternklasse 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!
}
abgeleitete Klasse 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
}
Funktion f Ausführen eines virtuellen Funktionsaufrufs
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);
}
Sie sehen also, eine vtable ist nur ein statischer Block im Speicher, der hauptsächlich Funktionszeiger enthält. Jedes Objekt einer polymorphen Klasse zeigt auf die vtable, die ihrem dynamischen Typ entspricht. Dies macht auch die Verbindung zwischen RTTI und virtuellen Funktionen klarer: Sie können überprüfen, welcher Typ eine Klasse ist, indem Sie einfach auf die vtable schauen, auf die sie zeigt. Das Obige wird in vielerlei Hinsicht vereinfacht, wie zum Beispiel Mehrfachvererbung, aber das allgemeine Konzept ist solide.
Wenn arg
vom Typ ist Foo*
und Sie nehmen arg->vtable
, aber tatsächlich ein Objekt vom Typ ist Bar
, dann erhalten Sie immer noch die richtige Adresse des vtable
. Das liegt daran, dass das vtable
immer das erste Element an der Adresse des Objekts ist, egal ob es aufgerufen wird vtable
oder base.vtable
in einem korrekt eingegebenen Ausdruck.
Inside the C++ Object Model
von zu lesenStanley B. Lippman
. (Abschnitt 4.2, Seite 124-131)