Virtuelle Methoden gar nicht so schlimm?



  • Oje, da bekomme ich ja einen rauhen Wind ins Gesicht geblasen 😞
    Also eines nach dem anderen:

    @camper
    Wenn ich das Ergebnis ignorieren würde, weil es mir nicht in den Kram passt, dann würde ich es wohl kaum in einem öffentlichen und für Jedermann zugänglichen Forum posten 🙄

    @ /rant/
    Ich habe ja in BEIDEN Mods getestet. Im DEBUG Modus habe ich es getestet, weil ich ansonsten Code schreiben müsste, der vom Compiler unter keinen Umständen wegoptimiert werden kann, um ein realistisches Resultat zu erhalten. Dieser Code allerdings würde das Resultat ja ebenfalls erheblich verfälschen, weil dadurch das Verhältnis des Aufwands des Codes innerhalb der Methode asymptotisch nicht mehr im Gleichgewicht zu dem Overhead des Methodenaufrufs selbst sein würde.

    Ich wünschte mir eine sachliche Diskussion. Was ich hier beobachte ist zum Grossteil sorry, eine einzige Rechthaberei und "Ach ich bin doch der Grösste und ihr habt alle keine Ahnung" herumgeschreie.

    Kommt mal runter Leute, ich habe bloss eine scheue Frage gestellt und nein, ich habe nicht vor, durch eine Frage in einem C++ Forum ins Fernsehen zu kommen 🙄



  • Ishildur schrieb:

    Im DEBUG Modus habe ich es getestet, weil ich ansonsten Code schreiben müsste, der vom Compiler unter keinen Umständen wegoptimiert werden kann, um ein realistisches Resultat zu erhalten.

    Messungen im Debugbuild sind aber sinnlos, eben weil der Compiler nicht optimiert - außerdem ist dort jede Menge Prüfcode enthalten, der deine Messung verfälscht. Das ist so, als würdest du einen Sportwagen mit platten Reifen, angezogener Handbremse und Bleigewichten im Kofferraum testen.

    'fps' ist übrigens keine Zeiteinheit.



  • @Registrierter Troll
    Ich danke dir für deinen Beitrag. Genau um über solche Dinge zu diskutieren habe ich diesen Thread eröffnet 😉 Ich habe den Benchmark in der Zwischenzeit angepasst:

    #include <iostream>
    #include <windows.h>
    
    #define ITERATION 10000000 // 10 Millionen
    
    volatile int g_a;
    
    class A{
    public:
     int a;
    
     A(void){this->a = 5;}
     virtual ~A(void){}
     virtual int MyVirtualMethod(void) = 0x00;
    };
    
    class B:public A{
    public:
     int MyVirtualMethod(void);
     int MyMethod(void);
    };
    
    int B::MyVirtualMethod(void){return this->a*this->a;}
    int B::MyMethod(void){return this->a*this->a;}
    
    void main(){
     __int64 tmFrq,tmSta,tmEnd;
     B *b = new B();
     QueryPerformanceFrequency((LARGE_INTEGER*)&tmFrq);
    
     QueryPerformanceCounter((LARGE_INTEGER*)&tmSta);
     for(int i=0;i<ITERATION;++i) g_a += b->a*b->a;
     QueryPerformanceCounter((LARGE_INTEGER*)&tmEnd);
     std::cout << "Ohne Methode:\t" << (double)tmFrq/(tmEnd-tmSta) << " fps" << std::endl;
    
     QueryPerformanceCounter((LARGE_INTEGER*)&tmSta);
     for(int i=0;i<ITERATION;++i) g_a += b->MyMethod();
     QueryPerformanceCounter((LARGE_INTEGER*)&tmEnd);
     std::cout << "Nicht Virtuell:\t" << (double)tmFrq/(tmEnd-tmSta) << " fps" << std::endl;
    
     QueryPerformanceCounter((LARGE_INTEGER*)&tmSta);
     for(int i=0;i<ITERATION;++i) g_a += b->MyVirtualMethod();
     QueryPerformanceCounter((LARGE_INTEGER*)&tmEnd);
     std::cout << "Virtuell:\t" << (double)tmFrq/(tmEnd-tmSta) << " fps" << std::endl;
    
     std::cout << "---------------------------------------" << std::endl;
    
     system("PAUSE");
     delete b;
    }
    

    Ohne Methode: 40.4848 fps
    Nicht Virtuell: 41.6062 fps
    Virtuell: 38.8941 fps
    ---------------------------------------
    Drücken Sie eine beliebige Taste . . .

    Nun sieht es irgendwie so aus, als würde es gar keine Rolle spielen?

    Ich habe fps genommen, weil die Zeiten soooo klein wahren, dass sie wahrscheinlich für die meissten nicht vorstellbar bzw. in ein Verhältnis gebracht werden können. Daher das fps.



  • ja bilder pro sekunde kann man sich natürlich viel schöner vorstellen 🙄



  • Ich hab mal gemacht

    class A{
    public:
    	virtual UInt32 MyVirtualMethod(UInt32 x)=0;
    };
    class B:public A{
    public:
    	UInt32 MyVirtualMethod(UInt32 x){
    		return x*x;
    	}
    };
    

    und

    B* b=new B;
    UInt32 f(){
    	UInt32 s=0;
    	for(UInt32 i=0;i!=10000;++i){
    		s+=b->MyVirtualMethod(i);
    	}
    	return s;
    }
    

    93363 Takte

    Mit

    A* b=new B;
    

    auch 93363 Takte.

    Mit

    class B{
    

    20020 Takte.

    Mit

    s+=i*i;
    

    auch 20020 Takte.

    Mit

    UInt32 MyVirtualMethod(UInt32 x)__attribute__ ((noinline)){
    

    60032 Takte.

    Fazit: Ob Funktionsaufruf oder nicht, ist fast egal. Kleine Funktionsaufrufe werden inline-optimiert. Funktionsaufruf kostet 4 Takte. Virtueller Funktionsaufruf kostet noch 2 Takte mehr. Das ist normalerweise völlig vernachlässigbar, weil man kaum eine so simple Funktion wie "return x*x" virtuell macht.

    Zum Messen verwende ich aus einem anderen Projekt die

    template<typename F>
    UInt64 measure(F f,UInt32 initCount,UInt32 goodResult){
    	os::lockProcessToProcessor();
    	UInt64 minTime=UInt64(-1);
    	int count=initCount;
    	while(count--){
    		UInt64 elapsed=-rdtsc();
    		UInt32 result=f();
    //		std::cout<<"r="<<result<<'\n';
    		elapsed+=rdtsc();
    		if(result!=goodResult)
    			return UInt64(-1);
    		if(elapsed<minTime){
    			minTime=elapsed;
    			count=initCount;
    			std::cout<<minTime<<'\n';
    		}
    	}
    	return minTime;
    }
    

    g++ -O3 -s -march=native
    mingw32
    gcc-4.4.0
    WinXP/32
    AMDSempron 3000+@1.8GHz



  • Ishildur schrieb:

    Oje, da bekomme ich ja einen rauhen Wind ins Gesicht geblasen 😞
    [...]
    Ich wünschte mir eine sachliche Diskussion. Was ich hier beobachte ist zum Grossteil sorry, eine einzige Rechthaberei und "Ach ich bin doch der Grösste und ihr habt alle keine Ahnung" herumgeschreie.

    Kommt mal runter Leute, ich habe bloss eine scheue Frage gestellt und nein, ich habe nicht vor, durch eine Frage in einem C++ Forum ins Fernsehen zu kommen 🙄

    Du solltest die Antworten nicht so persönlich nehmen. 😉

    Es wurde nichts gegen dich gesagt, man hat nur deine Messmethoden kritisiert, und das zu Recht. Manchmal klingt der Ton nicht halt nicht gerade freundlich, aber im Grunde ist das nicht böse gemeint. Teilweise soll dir einfach klar gemacht werden, dass ein bestimmtest Vorgehen Unsinn ist. Da ist der Effekt eben etwas anders als bei "wärst du so nett und könntest du im Release-Modus testen, das gefiele mir besser". 🙂

    Ich hoffe, du verstehst, was ich meine, und fasst diesen Beitrag nicht als weiteren Angriff auf. Auch wäre es gut, wenn du Funktionen (insbesondere virtuelle) nicht aufgrund potenzieller Performancenachteile meiden würdest.



  • So da bin ich wieder, war kurz im Training 😉

    @Nexus
    Naja, ich versuche mich in Zukunft an deine Worte zu erinnern 😉

    @volkard
    Herzlichen Dank für dein ausführliches Beispiel! Ich verstehe den Part:

    class B{ 20020 Takte.

    nicht so ganz. Könntest du das noch einmal erläutern?



  • Ishildur schrieb:

    Herzlichen Dank für dein ausführliches Beispiel! Ich verstehe den Part:

    class B{
    20020 Takte.

    nicht so ganz. Könntest du das noch einmal erläutern?

    Da habe ich die Vererbung weggenommen, also aus

    class B:public A{
    

    jetzt

    class B{
    

    und ab jetzt wird die Funktion statisch und nicht mehr virtuell aufgerufen.



  • ein sehr interessanter Beitrag von Ishildur, wollte eigentlich gerade anfangen selber einen Benchmark zu schreiben und dachte mir erst mal google zu behühen und siehe da - hier ist der benchmark. Es ist also so das der virtuelle Aufruf 50% länger als der statische Funktionsaufruf dauert und ein statischer Funktionsaufruf 400% länger dauert als gar kein Aufruf.
    Habe ich das so richtig verstanden.

    Ich habe nicht ganz nachvollziehen können wie man aus dem Beispiel von volkart die 4 Takte pro aufruf ablesen kann.

    Ich bin vom Beruf kein Programmierer und habe deswegen noch nie sehr komplexe Programme geschrieben. Mich würde aber interessieren ob jemand von euch ein Programm wegen Geschwindigkeitsvorteilen von virtuellen auf statische Funktionen umgeschrieben hat. Oder kann man in der Praxis den Geschwindigkeitsvorteil komplett vergessen.
    Immerhin haben virtuelle Funktionen ja auch andere nachteile und es scheint auch Ansätze zu geben virtuelle Funktionen zu vermeiden. (Ich lese gerade Scott Meyers)



  • socco schrieb:

    ein sehr interessanter Beitrag von Ishildur, wollte eigentlich gerade anfangen selber einen Benchmark zu schreiben und dachte mir erst mal google zu behühen und siehe da - hier ist der benchmark. Es ist also so das der virtuelle Aufruf 50% länger als der statische Funktionsaufruf dauert und ein statischer Funktionsaufruf 400% länger dauert als gar kein Aufruf.
    Habe ich das so richtig verstanden.

    das ist doch müll
    alles, was bei virtuellen fkt. länger dauert, ist doch der lookup beim fkt.-aufruf.
    also einmalig (lt volkard 6) nen paar takte.
    das gleiche gilt auch für keine fkt/eine fkt. auch hier sinds nur einmalig wenige takte mehr(ok, 2mal - einmal beim anspringen und einmal beim verlassen).
    wie er drauf kommt: er hat die takte dahingeschrieben - die differenz / anz. der iterationen(=10000) ist der unterschied...

    bb



  • Die Hauptfrage ist wohl, was in der jeweiligen Methode passiert. Die hier genommenen Definitionen sind wohl überwiegend akademischer Natur. Wenn in den Funktionen etwas mehr passiert (z.B. eine FFT gerechnet wird, oder irgendwelche Datenstrukturen durchgehangelt werden) und nicht nur irgendwelche Dummyoperationen damit der Compiler (hoffentlich) nicht optimiert, dann dürfte der Overhead für den Funktionsaufruf gegenüber der Funktionsausführungszeit klar zu vernachlässigen sein.



  • tut mir leid habe noch nie was von lookup gehört
    was geschieht denn beim Funktionsaufruf genau



  • unskilled schrieb:

    das ist doch müll
    alles, was bei virtuellen fkt. länger dauert, ist doch der lookup beim fkt.-aufruf.

    Nein, wie schon angemerkt wurde, können virtuelle Funktionen wesentlich teurer sein. Schlussendlich sind virtuelle Funktionen auch immer Grenzen, an denen der Compiler nicht rumoptimieren kann. Dagegen kann er eine normale Funktion inlinen, wodurch er sich das Rumschieben von Funktionsargumenten auf dem Stack spart, und danach können sogar noch Cacheoptimierungen etc kommen. Es gibt Spezialfälle, da kann man richtig viel rausholen.



  • wenn eine Virtuelle Funktion nur eine weitere Funktion aufruft gäbe es also kein inlining?



  • socco schrieb:

    wenn eine Virtuelle Funktion nur eine weitere Funktion aufruft gäbe es also kein inlining?

    Möglicherweise innerhalb der aufgerufenen Funktion, aber nicht über virtuelle Funktionsgrenzen hinweg. Aber auch da sollte man vorsichtig sein: Virtuelle Methoden sind nicht grundsätzlich langsamer, sondern nur, wenn man sie polymorph benutzt.

    struct MyClass
    {
        virtual void Foo();
    };
    
    int main()
    {
        MyClass a;
        a.Foo();
    }
    

    Hier kann der Aufruf von Foo() statisch gebunden werden, da bereits zur Kompilierzeit bekannt ist, welche Funktion aufgerufen werden muss. Dann fallen die erwähnten Probleme wieder weg.

    Allerdings habe ich teilweise wirklich das Gefühl, man macht sich zu viele Gedanken um solche Dinge. In den allerwenigsten Fällen kommt es vor, dass das Programm wegen virtuellen Funktionsaufrufen Probleme hat. Zumindest wenn man vernünftig programmiert und virtuelle Funktionen dort einsetzt, wo sie Sinn machen. Aber genau darum geht es: Einen Riesenaufwand zu betreiben, um virtuelle Funktionen loszuwerden, gleichzeitig aber die selbe Funktionalität manuell nachzubauen, ist etwas fragwürdig. Laufzeitpolymorphie ist ein mächtiges Sprachmittel, man sollte die Möglichkeiten nutzen.

    Klar gibt es vereinzelte Fälle, wo es wirklich kritisch sein kann. Teilweise sind auch Alternativen gar nicht so abwegig. Aber im Allgemeinen denke ich, Pauschalisierungen wie "virtuelle Funktionen sind langsam" haben sich schon in zu vielen Köpfen - besonders von weniger erfahrenen Programmierern - festgesetzt. Das ist eigentlich schade.



  • Mir ist das jetzt noch nie passiert, dass ich einen Engpass, wegen virtuellen Funktionen hatte. Das liegt aber wahrscheinlich hauptsächlich daran, dass virtuelle Funktionen in kritischen Punkten wahrscheinlich gar nie ins Konzept passen.

    Aber genau darum geht es: Einen Riesenaufwand zu betreiben, um virtuelle Funktionen loszuwerden, gleichzeitig aber die selbe Funktionalität manuell nachzubauen, ist etwas fragwürdig. Laufzeitpolymorphie ist ein mächtiges Sprachmittel, man sollte die Möglichkeiten nutzen.

    Jup, sehe ich auch so. Im Spieleprogrammierer.de Forum gabs mal wo einen Beitrag, dass er unbedingt virtuelle Funktionen vermeiden wollte, dann aber genau das gleiche selbst nachgebaut hat.. 🙂 (finds atm gerade nicht, schaue aber nachher nochmal, ob ich es finde.. ich fands recht amüsant.)



  • otze schrieb:

    Dagegen kann er eine normale Funktion inlinen, wodurch er sich das Rumschieben von Funktionsargumenten auf dem Stack spart

    das ist ja klar - aber wie gesagt: kein nachteil, der sich prozentual auf die laufzeit auswirken würde...
    (den nachteil hab ich übrigens schon zu fkt. hingeschrieben, weil ich virtuelle funktonsaufrufe mit funktionen verglichen habe und nicht mit ge-inline-tem code)
    und so extrem viel, wie du schreibst, sollte das auch nicht sein:
    durchschnittlich werden vll 2 parameter mit ner durchschnittsgröße von 4Byte(behaupte ich einfach mal so^^) übergeben:
    2xpush
    1xcall
    stack sichern (3 asm-befehle!?)
    //register sichern

    //register wiederherstellen
    1x add (stack wiederherstellen)

    register muss man logischerweise nur sichern und wiederherstellen, wenn man auch was in der fkt macht.
    bei trivialen fkt komm ich also auf 7 asm-befehle.
    virtuelle fkt. haben dann noch zusätzlich den lookup in der vtable als konstante kosten...
    kommen wir also auf max. 20 takte pro virtuellem fkt-aufruf im vergleich zu 0 takten bei inline-code.
    20 takte sind selbst auf nem rechenschieber (idR) noch zeitlich zu vernachlässigen...

    otze schrieb:

    Es gibt Spezialfälle, da kann man richtig viel rausholen.

    Japp - wie immer gibt es auch hier wieder eine Ausnahme - aber die Regel sieht eben anders aus...

    bb



  • Pauschalisierungen wie "virtuelle Funktionen sind langsam" haben sich schon in zu vielen Köpfen - besonders von weniger erfahrenen Programmierern - festgesetzt. Das ist eigentlich schade.

    Ich denke das liegt vor allem daran das die Anfänger noch nie ein großes Projekt realisiert haben und die Vererbung sowieso nie ganz richtig verstanden haben und deswegen sich auch dagegen streuben - ich spreche aus Erfahrung. Deswegen interessiert es mich jetzt auch so. Immerwieder sieht man das Erfahrene Programmierer interface Klassen usw. benutzen. Das erste was ich dann denke ist- wieso nimmt er nicht einfach ein konkretes Object und dessen interface, das wäre doch viel schneller und würde auch den Speicher schonen. Andrerseits ist dann der zweite Gedanke das der erfahrene Programmierer der vieleicht schon zich Bücher geschrieben hat, ja wissen muß was er da tut.
    Alles in allem scheint es so das man die Polymorphie ruhig nutzen kann, und erst wenn man ein konkretes Problem mit dem Speicher oder der Geschwindigkeit hat, überlegen sollte wo man das eine oder andere doch statisch bindet.

    Andrerseits hat auch die Polymorphie einige schwächen im Deisign, damit meine ich vor allem die "unreinen" virtuellen Funktionen, denn sie können sowohl als interface als auch als implementierung genutzt werden.
    Und man kann auch stark ins Schleudern kommen wenn man mit den Funktionsnamen und deren Gültigkeitsbereichen nicht aufpasst.



  • Die zu grunde liegende Frage ist falsch gestellt, bzw. es werden Äpfel mit Birnen verglichen.

    Natürlich ist der Aufruf einer virtuellen Methode zu Laufzeit aufwendiger als der einer statischen Methode. Jedoch erfüllt die virtuelle Methode gleichzeitig mehr Aufgaben die die statische Methode nicht leisten kann. Simples Beispiel:

    switch( this->TypeID )
    { 
       case ID1:  StaticMethod1(); break;
       case ID2:  StaticMethod2(); break;
       case ID3:  StaticMethod3(); break;
    ]
    

    gegen

    VirtualMethod();
    

    Um die Performance static vs virtuell vernünftig zu vergleichen müßte man das in den Tests berücksichtigen. Dabei ist der Mehraufwand den ein eigenes Typsystem mitbringt nicht zu unterschätzen.

    Ein Gedanke am Rande: Die "Kosten" einer virtuellen Funktion gemessen in Taktzyklen ist konstant. Die zur Verfügung stehende Rechenleistung steigert sich dagegen permanent(->siehe Mooresches Gesetz ) und drastisch. Die Diskussion über die "Kosten" virtueller Funktionen, setzt man sie in Relation zur Rechenleistung der Systeme wird daher zunehmend sinnloser 😉



  • unskilled schrieb:

    otze schrieb:

    Dagegen kann er eine normale Funktion inlinen, wodurch er sich das Rumschieben von Funktionsargumenten auf dem Stack spart

    das ist ja klar - aber wie gesagt: kein nachteil, der sich prozentual auf die laufzeit auswirken würde...

    Ich programmiere grad an einer Bibliothek, bei denen die Designer inzwischen davon ausgehen, dass sie ca Faktor 2 rausholen könnten, würden sie die Vererbungshierarchie weglassen. Polymorphie ist halt nicht so toll für Numbercrunching(mach mal eine svd auf einer 1000x1000 Matrix, bei der der Zugriff auf ein Element ein polymorpher Aufruf ist...)


Anmelden zum Antworten