Theortische Frage zur Implementierung von virtuellen Methoden
-
Ich denke mal ich habe verstanden, wie virtuelle Methoden in C++ implementiert sind. Jedes Objekt hat einen Zeiger auf eine pro Klasse existierende Tabelle von Methoden. Bei einem Aufruf wird die Adresse der konkreten Funktion zur Laufzeit aus dieser Tabelle ausgelesen.
Jetzt hab ich einen alternativen "Vorschlag": große Zeiger bzw. große Referenzen (ich nenne sie einfach mal so, keine Ahnung, ob es irgendwelche Fachbegriffe für das gibt, was ich vorhabe).
Statt pro Objekt einen Zeiger auf jene Tabelle zu speichern werden doppelt so große Zeiger verwendet, die aus einem Zeiger auf das Objekt und einem auf die Tabelle besteht.Also statt
-------- ------------- |Zeiger| ---->|objekt|vptr| -------- ------------- | v --------- |tabelle| ---------jetzt
------------- -------- |Zeiger|vptr| ---->|objekt| ------------- -------- | v --------- |tabelle| ---------Dadurch spart man, wenn ich das richtig sehe, eine Indirektion beim Aufruf einer virtuellen Methode und die Objekte sind um die Größe eines Zeigers kleiner, was bei sehr kleinen Objekten durchaus ins gewicht fallen kann (IMHO).
Vielleicht könnte man solche großen Zeiger als Alternative zu normalen Zeigern + virtuellem Methodenzeiger anbieten.
/edit: den folgenden Teil habe ich leicht abgeändert.
Es ist unwarscheinlich, das die Sprache an sich soweit abgeändert wird. Deshalb würde ich lediglich eine Erweiterung vorschlagen.
Es gibt eine Möglichkeit Protokolle zu definieren (in anderen Sprachen auch Interfaces genannt).
Instanzen eines solchen Protokolltyps verhalten sich wie die von mir hier beschrieben großen Referenzen.protocoll Foo { void foo (); }; class Bar : public Foo { void foo () { ... } }; ... void func(Foo foo) { foo.foo (); // virtueller Aufruf } int main () { Bar bar; func (bar); }Ich hoffe das Prinzip ist verständlich.
Bar beinhaltet keinen Zeiger auf eine Methodentabelle, da Bar keine virtuellen Methoden besitzt und auch keine virtuellen oder abstrakten Methoden erbt. Dennoch erfolgt in 'func' ein virtueller Aufruf.Spricht etwas dagegen? Gibt's Verbesserungsvorschläge? Ist die ganze Idee unsinnig?
-
Dann müsstest du IMHO bei jeder Zuweisung an den Zeiger zur Laufzeit den dynamischen Typ ermitteln, um gegebenenfalls den Zeiger auf die Tabelle anzupassen.
-
Nein.
Foo foo; Foo bar = foo; // kopiert beide Teile!nur beim bestimmen der Adresse wäre es nötig (also bei &objek). Oder sehe ich das falsch?
-
Das 'Problem' das ich hier sehe ist: du kannst diesen 'doppelzeiger' nicht in ein register geben (wenn wir jetzt von einem normalen ia32 ausgehen). Somit könntest du einen methodenaufruf nicht so schön über ein register laufen lassen, da du ja 2 register brauchst. das sind pro aufruf doppelte kosten

Was jetzt natürlich schneller ist, weiss ich nicht. Aber bedenke dann mal folgendes:
class NonVirtual { public: void foo() {} }; class HasVirtual : public NonVirtual { public: virtual void bar() {} }; void downcast(NonVirtual* p) { dynamic_cast<HasVirtual*>(p)->bar(); } NonVirtual p=new HasVirtual(); downcast(p);Du musst NonVirtual jetzt als Doppelzeiger speichern. Somit wäre dein Ansatz sicher langsamer bei nicht virtuellen Funktionen (und genau darum geht es bei C++ ja: keine unnötigen kosten).
-
Das 'Problem' das ich hier sehe ist: du kannst diesen 'doppelzeiger' nicht in ein register geben (wenn wir jetzt von einem normalen ia32 ausgehen). Somit könntest du einen methodenaufruf nicht so schön über ein register laufen lassen, da du ja 2 register brauchst. das sind pro aufruf doppelte kosten
Angenommen ich habe folgendes:
class ABC_Foo { public: virtual void foo () = 0; }; protocol Protocol_Foo void foo (); }; class ABC_Bar : public ABC_Foo { public: void foo () { ... } }; class Protocol_Bar : public Protocol_Foo { public: void foo () { ... } }; int main { ABC_Bar abc_bar; Protocol_Bar protocol_bar; ABC_Foo * abc_foo = &abc_bar; // gewöhnlicher Zeiger Protocol_Foo * protocol_foo = &protocol_bar; // großer Zeiger abc_foo->foo(); // *1 protocol_foo->foo(); // *2 }Angenommen der Optimizer merkt nicht, das der Typ auch hier eigentlich schon zur Compilezeit feststeht.
In *1 habe ich eine Indirektion über den Zeiger 'abc_foo' und eine weitere über die Methodentabelle. Ich habe also also zwei Zeiger mit denen ich umgehen muss.
In *2 habe ich eine Indirektion über den Methodenzeiger. Die Adresse des Objekts brauche ich, damit Sie als this an die Methode übergeben kann. Ich hantiere also ebenfalls mit zwei Zeigern. Aber der Zeiger aufs Objekt muss gar nicht dereferenziert werden, da er lediglich als this an die Methode übergeben wird.Ein großer Zeiger ist ja nicht anderes als zwei Zeiger.
Alternative Frage: kann man mit den heutigen generischen Sprachbestandteilen etwas bauen, das dem großen Zeiger nahe kommt?
-
Du musst NonVirtual jetzt als Doppelzeiger speichern. Somit wäre dein Ansatz sicher langsamer bei nicht virtuellen Funktionen (und genau darum geht es bei C++ ja: keine unnötigen kosten).
Ich hab meinen Vorschlag ja schon so angepasst, das nur Instanzen der in dem zusammenhang eingefürten Protokolle große Zeiger bzw. Referenzen sind.
Dein Code würde folglich sich genau so verhalten, wie bisher.
Auch ABCs kann man ja weiterhin verwenden, sollten Sie Vorteile bieten.
-
a) die doppelte indizierung ist heutzutage hinreichend schnell, im idealfall genauso schnell wie einfache indizierung. Problematisch ist hier eher der Sprung an eine schwer vorhersagbare Adresse - und die hat man in beiden Fällen.
b) Jede Membermethode erhält (versteckt) einen "this"-Zeiger. Bei einer Klasse mit mindestens einer virtuellen Methode muß an *jede* Methode der Doppel-Zeiger weitergegeben werden. Kann höchstens für inline-Funktionen wegoptimiert werden.
c) hat mindestens genausoviele Zeiger auf (polymorphe) Objekte wie Objekte selbst - häufig jedoch mehr. Das Platzpar-Argument zieht also nicht.
(gut, es lassen sich Fälle konsttruieren, in denen man mehr polymorphe Instanzen als Zeiger hat. Ist aber hinreichend selten. Und wenn hier Platz wirklich eine Rolle spielt, könnte man das mit einer Proxy-Klasse erledigen, ist aber häßlich und aufwendig)
Wenn ich so drüber nachdenke, ist c) praktisch das Auskriterium. Aber trotzdem
d) Die Idee gefällt mir, und war auf jeden Fall eine gute frage
-
Helium schrieb:
nur beim bestimmen der Adresse wäre es nötig (also bei &objek). Oder sehe ich das falsch?
Ne, denke ich auch. Nur woher kriegen wir bei &objek die Info? Das objekt weiß es ja jetzt nicht mehr.
MfG Jester
-
Jester schrieb:
Helium schrieb:
nur beim bestimmen der Adresse wäre es nötig (also bei &objek). Oder sehe ich das falsch?
Ne, denke ich auch. Nur woher kriegen wir bei &objek die Info? Das objekt weiß es ja jetzt nicht mehr.
MfG Jester

Vergiss meine erste Aussage. Den Typ in deisem Fall kennt der Compiler:
Typ objekt; EinProtocol * foo = &objekt; // Welchen Typ hat objekt wohl.c) hat mindestens genausoviele Zeiger auf (polymorphe) Objekte wie Objekte selbst - häufig jedoch mehr. Das Platzpar-Argument zieht also nicht
Hat man? Wieso sollte man?
EinProtokol * foo = new ImplementiertEinProtokol[1000000];Hier habe ich schon 999999 Zeiger weniger. :p
Ich lege sehr viele Objekte auf dem Stack an. ERst durch die Übergabe an eine Funktion, die eine Referenz auf eine Basisklasse erwartet kommen die polymorphen Eigenschaften zu Zuge.
d) Die Idee gefällt mir, und war auf jeden Fall eine gute frage
Danke

Naja, war nur so eine spontane Idee. Ich habe mir nicht wirklich ernsthaft Gedanken gemacht. So wie es aussieht ist es nicht besonders sinnvoll. Schade.
-
EinProtokol * foo = new ImplementiertEinProtokol[1000000];
Mußt du genau den Fall nehmen, den ich als "hinreichend selten" aussortiert habe??

Die Polymorphie macht hier ja nur sinn, wenn man wenigstens zwei solche Arrays hat, und eine Funktion die immer ein Objekt davon ausgibt. In den meisten Fällen hat man dann ja doch ein Array von EinProtokol-Zeigern auf verschiedene Implementationen.
-
Wenn ich mehrere Zeiger auf das selbe Objekt habe zahle ich aber mehrfach...
Das Problem ist damit nur verlagert.MfG Jester
-
statt einer tabelle nen entscheidungsbaum nehmen. so wie in eiffel halt. macht viel oft schnell wegen inlining.
das problem an virtuellen funktionen ist ja minder die aufrufgeschwindigkeit, sondern daß der optimierer dort blind ist.
-
Entscheidungsbaum heißt über RTTI oder sonstwie die "richtige" Klasse rauskriegen und mit einem switch / if direkter Aufruf??
-
peterchen schrieb:
Entscheidungsbaum heißt über RTTI oder sonstwie die "richtige" Klasse rauskriegen und mit einem switch / if direkter Aufruf??
ja.