Virtuelle Methoden gar nicht so schlimm?



  • Hallo zusammen
    Ich habe in letzter Zeit viel über statische Polymorphie und den Geschwindigkeitsvorteil von nicht virtuellen gegenüber virtuellen Methoden gelesen. Ich habe nun diesbezüglich einen ganz einfachen Benchmark geschrieben und möchte euch nun fragen, ob ich mich da vielleicht ein wenig "vertippt" habe:

    #include <iostream>
    #include <windows.h>
    
    #define ITERATION 10000000 // 10 Millionen
    
    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;}
    int B::MyMethod(void){return 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) 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) 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) 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;
    }
    

    Das Resultat auf meinem PC sieht folgendermassen aus:

    Ohne Methode: 35.3882 fps
    Nicht Virtuell: 3.13059 fps
    Virtuell: 2.9843 fps
    ---------------------------------------
    Drücken Sie eine beliebige Taste . . .

    Kompilliert habe ich diese mit Hilfe von Visual Studio 2008 im Debug Mode. Der Test funktioniert nicht im Release mode weil er dort IMHO die nicht virtuelle Methode inline macht und somit die gleiche Performance wie bei KEINE Methode aufweist:

    Ohne Methode: 1.23392e+006 fps
    Nicht Virtuell: 1.23392e+006 fps
    Virtuell: 34.6996 fps
    ---------------------------------------
    Drücken Sie eine beliebige Taste . . .

    Mein persönliches Fazit sieht folgendermassen aus:
    Es ist deutlich effizienter, auf Methoden zu verzichten, sofern dies möglich ist. Ist man jedoch gezwungen, eine Methode zu verwenden, dann spielt es auch keine Rolle mehr, ob diese nun virtuell ist oder nicht...

    Ich bitte um eure Meinung bzw. Stellungsnahme 🙂

    Mfg Samuel


  • Mod

    Ishildur schrieb:

    Kompilliert habe ich diese mit Hilfe von Visual Studio 2008 im Debug Mode. Der Test funktioniert nicht im Release mode weil er dort IMHO die nicht virtuelle Methode inline macht und somit die gleiche Performance wie bei KEINE Methode aufweist:

    Also ignorierst du das Ergebnis, weil es dir nicht in den Kram passt?



  • Ishildur schrieb:

    Virtuelle Methoden gar nicht so schlimm?

    Nein, definitiv nicht. Du solltest sie nicht wegen der Geschwindigkeit meiden. Benutze virtuelle Funktionen, wenn sie Sinn machen. Falls du tatsächlich einmal Probleme deswegen haben solltest (was recht unwahrscheinlich ist), kannst du dir das immer noch genauer überlegen (Stichwort Premature Optimization). Bedenke einfach, dass dann oft eine andere Dispatch-Möglichkeit notwendig ist (z.B. if , switch ), was in etwas auf das Gleiche hinausläuft.

    Im Debug-Modus Zeitmessungen durchzuführen ist übrigens Schwachsinn. Wundert mich nicht, dass Funktionsaufrufe viel ineffizienter sind. Warum willst du das Inlining nicht berücksichtigen? Im Anwendungscode gehören Optimierungen auch dazu; es wäre realitätsfern, diese zu unterdrücken.



  • Oh really? for(int i=0;i<ITERATION;++i) b->a; kann der Compiler im Prinzip gleich ganz wegoptimieren, da dort nichts gemacht wird und vermutlich macht er dies dann auch. Weil beim Debugging die Calls jedoch stattfinden müssen, bekommst du den Overhead dafür obwohl dort auch nichts gemacht wird. Überhaupt hast du etwas vollkommen sinnloses selbst schon angemerkt: Warum testest du im Debug Modus? 🙄

    MfG



  • Du mißt Mist. Zu messender Code muß immer etwas berechnen! Und das Rechenergebnis muß in einer Weise Einfluß auf die Programmausgabe haben, daß kein einziger Aufruf komplett wegoptimiert werden kann. Hier wäre es vielleicht gut,

    int B::MyVirtualMethod(int x){return x*x;}
    

    zu machen und die Summe der Quadratzahlen von 1^2 bis 1000^2 zuz berechnen.

    Willst Du mit dem hex-Literal in Fernsehen kommen oder was soll der Quatsch?

    virtual int MyVirtualMethod(void) = 0x00;
    


  • @volkard

    wenn der Rückgabewert der Funktion nicht verwendet wird, dann wird das auch alles wegoptimiert.



  • schmuessla schrieb:

    @volkard

    wenn der Rückgabewert der Funktion nicht verwendet wird, dann wird das auch alles wegoptimiert.

    ja, deswegen "und die Summe der Quadratzahlen von 1^2 bis 1000^2 zu berechnen" und "Und das Rechenergebnis muß in einer Weise Einfluß auf die Programmausgabe haben, daß kein einziger Aufruf komplett wegoptimiert werden kann". Am einfachsten, man gibt die Summe mit cout aus.



  • schmuessla schrieb:

    @volkard

    wenn der Rückgabewert der Funktion nicht verwendet wird, dann wird das auch alles wegoptimiert.

    Nicht zwingend, da Seiteneffekte auftreten koennen. Schreiben auf eine globale volatile Variable sollte nicht wegoptimiert werden.



  • 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


Anmelden zum Antworten