Code Effizienz



  • Hallo, vllt hat jemand eine Idee für eine schnellere/elegantere Umsetzung?
    Kurz:
    User ruft eine Objektmethode auf, die abhängig von Zuständen weiter Funktionen aufruft.

    Methode 1 - Bedingung:

    void foo::do_anything()
    {
    	if(b>2)
    		do_that();
    	else
    	{
    		do_this();
    		b++;
    	}
    }
    

    Methode 2 - Funktionszeiger

    void foo::do_something()
    {
    	(this->*wherever)();
    }
    

    Wieso ist Methode 1 schneller als 2?

    Etwas ausführlicher:
    Beim Erstellen des Objekts soll zunächst solange die Funktion do_this() aufgerufen werden, bis eine bestimmte Bedingung erfüllt ist. Kann ineffizient sein, nicht zeitkritsch.
    Nachdem die Bedingung erfüllt ist soll nur noch die Funktion do_that() (zeitkritsch) aufgerufen werden. Die if-Bedingung scheint nicht wirklich die Performance zu verschlechtern, trotzdem würde ich sie gerne vermeiden. Geht es noch schneller ohne direkten Zugriff auf do_that() zu gewähren?
    Zeiten:
    `

    do_anything()---------> 6.275s Funktion->If->Funktion

    do_something()--------> 6.527s Funktion->Funktionszeiger

    do_that()-------------> 3.098s Funktion direkt

    *(test.wherever))()---> 3.289s Funktionszeiger direkt

    `

    Code zu den Zeiten:

    //main.cpp
    #include "foo.h"
    #include <ctime>
    #include <iostream>
    using namespace std;
    
    int main(void)
    {
    	foo test;
    	clock_t t;
    
    	t = clock();
    	for(unsigned int i=0;i<100000000;i++)
    	{
    		test.do_anything();
    	}
    	t = clock()-t;
    	cout << "Time 1:\t" << (double)t/CLOCKS_PER_SEC << "s\t\t FKT_IF_FKT"  << endl;
    
    	test.do_something();
    	t = clock();
    	for(unsigned int i=0;i<100000000;i++)
    	{
    		test.do_something();
    	}
    	t = clock()-t;
    	cout << "Time 2:\t" << (double)t/CLOCKS_PER_SEC << "s\t\t FKT_FKT_ADD" << endl;
    
    	t = clock();
    	for(unsigned int i=0;i<100000000;i++)
    	{
    		test.do_that();
    	}
    	t = clock()-t;
    	cout << "Time 3:\t" << (double)t/CLOCKS_PER_SEC << "s\t\t FKT" << endl;
    
    	t = clock();
    	for(unsigned int i=0;i<100000000;i++)
    	{
    		(test.*(test.wherever))();
    	}
    	t = clock()-t;
    	cout << "Time 4:\t" << (double)t/CLOCKS_PER_SEC << "s\t\t FKT_ADDRESS" << endl;
    
    	return 0;
    }
    
    foo.h
    #ifndef FOO_H
    #define FOO_H
    
    class foo{
    public:
    	foo();
    	void do_something();
    	void do_anything();
    	void do_that();		        //public only for time measure
    	void (foo::*wherever)();	//public only for time measure
    private:
    	void do_this();
    	int z;
    	int b;
    };
    
    foo::foo()
    {
    	wherever=&foo::do_this;
    	b=3;
    }
    
    void foo::do_something()
    {
    	(this->*wherever)();
    }
    
    void foo::do_this()
    {
    	static int a=0;
    	//
    	// ...SOME CODE...
    	//
    	if(a>=0)
    		wherever=&foo::do_that;
    	a++;	
    }
    void foo::do_that()
    {
    	z++;
    }
    
    void foo::do_anything()
    {
    	if(b>2)
    		do_that();
    	else
    	{
    		do_this();
    		b++;
    	}
    }
    
    #endif
    

  • Mod

    edit: Ich sehe gerade, dass das auch deine Ergebnisse sind. Hast du keine Optimierungen angeschaltet? Ich musst bei dem Code von dir dem Compiler absichtlich das Optimieren verbieten, weil sonst bei der Variante mit dem if der Aufruf wegoptimiert wurde. Wenn du Performance misst, dann mach das mit optimiertem Code. Niemand interessiert sich dafür, wie schnell unoptimierter Code läuft.
    Hier folgt nun mein Originalbeitrag:

    Ich befürchte, dass du hier vor allem gemessen hast, dass der Compiler einen direkten Funktionsaufruf wegoptimieren kann, wohingegen er einen Aufruf via Zeiger nicht optimieren kann. Wenn ich do_that mal vor dem Optimierer verstecke (in andere Übersetzungseinheit verlegen und keine Optimierung zur Linkzeit), dann bekomme ich Ergebnisse in der Art von:

    Time 1:	0.39031s		 FKT_IF_FKT
    Time 2:	0.386127s		 FKT_FKT_ADD
    Time 3:	0.202638s		 FKT
    Time 4:	0.201939s		 FKT_ADDRESS
    

    und ähnliches. Woran man vor allem eines erkennt: 100000000 Funktionaufrufe dauern bei mir ungefähr 0.2 Sekunden. Dabei ist es egal ob der Aufruf direkt im Code steht oder über einen Zeiger erfolgt. Das ist auch nicht verwunderlich, denn auf Maschinensprachenebene ist das praktisch identisch. Bei den ersten beiden Varianten hat man durch die Indirektion den doppelten Wert, da man erst die Proxyfunktion aufruft, die dann die eigentliche Funktion aufruft. Dass da noch ein if in der Funktion steckt ist für die Laufzeit unerheblich, das ist nichts im Vergleich zu den Kosten eines Funktionsaufrufs. Daher ist der Wert auch nicht groß anders zwischen den Varianten 1 und 2 (aber Variante 2 ist immer ein kleines bisschen schneller).

    Letztendlich können wie folgern, dass der Funktionsaufruf an sich zu vermeiden ist. Wenn du deine Kapselung beibehalten möchtest, bietet es sich an, entweder den Optimierungen des Compilers zu vertrauen (das würde ich machen) oder den Code direkt in do_anything() zu schreiben (also quasi manuelles inline). Letzteres würde ich nur im Notfall machen, da der Compiler das meistens schon richtig macht, wenn man ihn lässt. Dafür wird der Code unübersichtlich, wenn man es so macht.
    Falls du keine Optimierungen an hattest: Das war der Grund für die Unterschiede. Mit Optimierungen solltest du Werte in der Art von

    Time 1: Sehr wenig
    Time 2: Wie oben
    Time 3: Nix
    time 4: Wie oben
    

    erhalten. Wenn dies nicht so ist: Aktivier aggressivere Optimierungsoptionen 😃 . Guck mal, ob dein Compiler interpocedural optimization unterstützt (läuft bei verschiedenen Compilern unter unterschiedlichen Namen, z.B. Link-time optimization beim GCC). Und teste mit dem Originalprogramm. Da deine Funktion derzeit nichts tut, misst du vor allem die Fähigkeit des Optimizers inwieweit, er den Funktionsaufruf vollständig wegoptimieren kann. Wenn deine Funktion aber wirklich was tut, dann ist der Funktionsaufruf selber oft gar nicht von Belang und andere Aspekte bestimmen die Geschwindigkeit.



  • Entschuldige, dass ich mich nicht bedankt hatte, super Antwort, hat mich um einiges weitergebracht!
    Ich hatte die Compiler Optimierung deaktiviert, weil ich im Fehlglauben war, dass sich die Zeitersparnis proportional verhalten würde. Nun ja das Ergebnis spricht für sich. 😉

    Time 1: 0.116s		FKT_IF_FKT
    Time 2: 0.225s		FKT_FKT_ADD
    Time 3: 0s		    FKT 
    Time 4: 0.233s		FKT_ADDRESS
    

    Wie du gesagt hattest, dankeschön!


Log in to reply