shared_ptr performance & design Frage



  • Bevor du sowas bastelst, würd ich mir mal überlegen, ob ich das wirklich will. Shared-Ownership will man meiner Erfahrung nach nämlich eigentlich eher selten, wenn man ganz ehrlich ist...



  • ad 1) halte ich nicht für sinnvoll

    ad 2) "=" auf ein atomic mag schnell sein, "+=" aber sicher nicht. bei x86/AMD64 CPUs dauert das ca. 100x so lange wie ohne "atomic". zumindest in der grössenordnung, soll sein 25x oder 400x - auf jeden fall nicht 2x.
    und: ich würde den zeiger einfach auf NULL setzen.



  • dot schrieb:

    Shared-Ownership will man meiner Erfahrung nach nämlich eigentlich eher selten, wenn man ganz ehrlich ist...

    Wenn ich kein C++11 "move" habe, dann will ich zumindest das ziemlich oft.



  • hustbaer schrieb:

    dot schrieb:

    Shared-Ownership will man meiner Erfahrung nach nämlich eigentlich eher selten, wenn man ganz ehrlich ist...

    Wenn ich kein C++11 "move" habe, dann will ich zumindest das ziemlich oft.

    Ich nicht 😉



  • Seltsam
    😕


  • Administrator

    Wenn ihr beide (hustbear und dot) beide nicht so extrem wortkarg wärt, könnte daraus noch eine Interessante Diskussion werden! Also her mit den ausführlichen Antworten und Argumenten! Das mach dies noch explzit erwähnen muss 😃

    Grüssli



  • hustbaer schrieb:

    ad 1) halte ich nicht für sinnvoll

    ad 2) "=" auf ein atomic mag schnell sein, "+=" aber sicher nicht. bei x86/AMD64 CPUs dauert das ca. 100x so lange wie ohne "atomic". zumindest in der grössenordnung, soll sein 25x oder 400x - auf jeden fall nicht 2x.
    und: ich würde den zeiger einfach auf NULL setzen.

    Bei mir braucht += doppelt solange, wenn nur 1 thread gleichzeitig auf das atomic zugreift. Sonst steigt es sehr stark an auf die Größenordnungen die du genannt hast. Ich denke ich werde mal beides ausprobieren, ein kleines real-world Programm schreiben und dann mal schauen, was schneller ist.
    Warum hältst du 1) nicht für sinnvoll? Es würde das interface des pointers doch noch mehr an das eines normalen pointers annähern.



  • Dravere schrieb:

    Wenn ihr beide (hustbear und dot) beide nicht so extrem wortkarg wärt, könnte daraus noch eine Interessante Diskussion werden! Also her mit den ausführlichen Antworten und Argumenten! Das mach dies noch explzit erwähnen muss 😃

    Naja, ich find die meisten shared_ptr, die ich bisher so gesehen hab, waren eigentlich nur eine Art zu sagen: "Ich hab keine Ahnung was für einen Scope dieses Objekt eigentlich hat und will mir auch keine Gedanken drüber machen."
    shared_ptr widerspricht imo schon in seiner Natur ein wenig der essentiellen Idee von RAII, dessen Ausdruck ein Smartpointer doch eigentlich sein sollte!?
    Shared Ownership hat sicherlich ihren Platz, aber imo sind das eher Randbereiche und Ausnahmefälle. Ich find, dass es sehr wesentlich für gutes Design ist, sich darüber im Klaren zu sein, welche Objekte andere Objekte nur verwenden und welche Objekte sie tatsächlich besitzen und diese Beziehungen auch entsprechend im Design zu reflektieren. Und meiner Erfahrung nach lässt sich die Besitzbeziehung praktisch immer auf genau ein anderes Objekt festnageln.
    Wenn ich ein Objekt in meinem Programm hätte, dessen Scope ich nicht genau definieren kann, dann würd ich das zumindest mal gründlich hinterfragen, anstatt einfach shared_ptr und gut ist. Beispiel Dependency-Injection. Ich find, dass das, was heute unter "Dependency-Injection" läuft, eigentlich nix andres ist, als ein Aspekt der reinen Lehre von RAII. In so einem Design ergibt sich rein systematisch bedingt für jedes Objekt praktisch immer und völlig natürlich ein ganz eindeutiger Scope.
    Genau aus oben genannten Gründen halte ich auch Dinge wie z.B. Garbage Collection für überhaupt nicht förderlich für gutes Design. Garbage Collection belohnt schlechtes Design ohne gutes Design auch nur in irgendeiner Form zu unterstützen. In einem sehr guten Design wären die Objektbeziehungen imo von vornherein so klar, dass Garbage Collection sinnlos wird.

    EDIT: Die Tatsache, dass ich in meiner Philosophie eigentlich nicht wirklich zwischen Scope und Lifetime unterscheide, ist nur ein weiterer Ausdruck dessen, worums mir hier eigentlich geht 😉



  • @GorbGorb:
    Ich würde 1) nicht machen, weil ... weiss nicht. Stört mich einfach nicht dass ich .reset() schreiben muss.
    Zu 2x vs. 100x: mit was für einer CPU testest du das? P4 und Core2 brauchen
    , wenn mich meine Erinnerung jetzt nicht trügt, so ca. 400 Cycles für ein InterlockedIncrement (wohingegen ein normales so bei 0,3-1 Cycles liegt).

    @dot
    Wie machst du Ownership-Transfer ohne move und ohne shared_ptr?
    scoped_ptr& übergeben und swap?
    Das fände ich hässlich, aber immer noch akzeptabel.
    Oder .release() und rohe Zeiger übergeben? Sowas würde ich nichtmal mit der Kneifzange angreifen, weil man viel zu sehr aufpassen muss, dass der Code dann 100% Exception-safe ist (=nicht leakt, egal wann wo was rausfliegt).

    Und was machst du, wenn man eine Klasse mal mit Ownership-Transfer und mal ohne verwenden will?

    Was mich an der unique Ownership Variante auch stört: es gibt keine "checked" Variante. Oft genug muss man Zeiger "ausborgen", und wenn die dann verwendet werden nachdem das Objekt bereits zerstört wurde, dann ist die Kacke am dampfen. Dann crasht der Prozess. Oder auch nicht. UB vom feinsten. Wenn man da wenigstens ne Exception bekommen könnte, sähe die Sache schon ganz anders aus.

    Und noch eine Frage: kann es sein, dass du oft bis immer irgendwelche Business-Logik-Dingens implementierst, und selten bis nie mit z.B. GUI Anwendungen zu tun hast? 😉



  • hustbaer schrieb:

    @dot
    Wie machst du Ownership-Transfer ohne move und ohne shared_ptr?
    scoped_ptr& übergeben und swap?

    auto_ptr? Brauch ich aber auch erstaunlich selten, eigentlich nie. Nach ein paar Refactorings sind solche Konstrukte bei mir bisher praktisch immer irgendwie verschwunden...
    Ein scoped_ptr ist sicherlich bei weitem die häufigste Art von Smartpointer die ich verwend.

    hustbaer schrieb:

    Und was machst du, wenn man eine Klasse mal mit Ownership-Transfer und mal ohne verwenden will?

    Versteh das Problem nicht ganz. Wie ich meine Objekte instanzier, betrifft die Klasse doch in keiner Weise, das würde doch gegen das Single Responsibility Principle verstoßen!?

    hustbaer schrieb:

    Was mich an der unique Ownership Variante auch stört: es gibt keine "checked" Variante. Oft genug muss man Zeiger "ausborgen", und wenn die dann verwendet werden nachdem das Objekt bereits zerstört wurde, dann ist die Kacke am dampfen. Dann crasht der Prozess. Oder auch nicht. UB vom feinsten. Wenn man da wenigstens ne Exception bekommen könnte, sähe die Sache schon ganz anders aus.

    Das kann natürlich passieren, aber passiert mir eher selten und wenn, dann hab ich ja nen Debugger 😉
    Wenn du wirklich Angst davor hast, kannst du ja statt roher Zeiger irgendeine Art von Weak-Reference verwenden...

    hustbaer schrieb:

    Und noch eine Frage: kann es sein, dass du oft bis immer irgendwelche Business-Logik-Dingens implementierst, und selten bis nie mit z.B. GUI Anwendungen zu tun hast? 😉

    Nö, eigentlich nicht. Die meiste Zeit schreib ich irgendwelche Grafikanwendungen, mein letzter Freelance-Job war ein CAD-Tool mit GUI und gerade unlängst hab ich überhaupt ein kleines GUI-System an sich gebaut. Mir würd spontan jetzt kein Grund einfallen, wieso eine GUI so prädestiniert für Shared-Ownership sein sollte. Gerade bei einer GUI, die ja in ihrer Natur schon eine reine Baumstruktur ist, wo jedes Element genau einen Parent hat, sind die Besitzbeziehungen doch extrem eindeutig!?



  • hustbaer schrieb:

    @GorbGorb:
    Ich würde 1) nicht machen, weil ... weiss nicht. Stört mich einfach nicht dass ich .reset() schreiben muss.
    Zu 2x vs. 100x: mit was für einer CPU testest du das? P4 und Core2 brauchen
    , wenn mich meine Erinnerung jetzt nicht trügt, so ca. 400 Cycles für ein InterlockedIncrement (wohingegen ein normales so bei 0,3-1 Cycles liegt).

    Ich benutze einen Core i5. Das ist der code, mit dem ich es getestet habe:

    #include <iostream>
    #include <ctime>
    
    #include <atomic>
    #include <boost/thread.hpp>
    
    template< class A >
    void measure( const char *name , A &&functor_a )
    {
    	unsigned int t = std::clock();
    	boost::thread thread_a( functor_a );
    	thread_a.join();
    	std::cout << name << std::clock() - t << std::endl;
    }
    template< class A , class B >
    void measure( const char *name , A &&functor_a , B &&functor_b )
    {
    	unsigned int t = std::clock();
    	boost::thread thread_a( functor_a );
    	boost::thread thread_b( functor_b );
    	thread_a.join();
    	thread_b.join();
    	std::cout << name << std::clock() - t << std::endl;
    }
    
    std::atomic< int > a( 0 );
    volatile unsigned int n1 = 0;
    volatile unsigned int n2 = 0;
    volatile unsigned int volatile_test = 0;
    const unsigned int loop_number = 20000;
    
    int main()
    {
    	auto threadfunc_a = []()
    	{
    		for( unsigned int i = 0 ; i < loop_number ; ++i )
    		{
    			a.fetch_sub(1, std::memory_order_release);
    			a.fetch_sub(1, std::memory_order_release);
    			a.fetch_sub(1, std::memory_order_release);
    			a.fetch_sub(1, std::memory_order_release);
    			a.fetch_sub(1, std::memory_order_release);
    			a.fetch_sub(1, std::memory_order_release);
    			a.fetch_sub(1, std::memory_order_release);
    			a.fetch_sub(1, std::memory_order_release);
    		}
    	};
    
    	auto threadfunc_n1 = []()
    	{
    		for( unsigned int i = 0 ; i < loop_number ; ++i )
    		{
    			--n1;
    			--n1;
    			--n1;
    			--n1;
    			--n1;
    			--n1;
    			--n1;
    			--n1;
    		}
    	};
    
    	auto threadfunc_n2 = []()
    	{
    		for( unsigned int i = 0 ; i < loop_number ; ++i )
    		{
    			--n2;
    			--n2;
    			--n2;
    			--n2;
    			--n2;
    			--n2;
    			--n2;
    			--n2;
    		}
    	};
    
    	auto threadfunc_volatile_test = []()
    	{
    		for( unsigned int i = 0 ; i < loop_number ; ++i )
    			--volatile_test;
    	};
    
    	measure( "atomic 1 thread: " , threadfunc_a );
    	measure( "volatile 1 thread: " , threadfunc_n1 );
    	measure( "volatile test 1 thread: " , threadfunc_volatile_test );
    
    	measure( "atomic 2 threads: " , threadfunc_a , threadfunc_a );
    	measure( "volatile 2 threads: " , threadfunc_n1 , threadfunc_n2 );
    
    	std::cin.get();
    	return 0;
    }
    

    Was mich daran ernsthaft beunruhigt, ist, dass sich das Programm ab loopnumber ~ 10000 regelmäßig aufhängt... Woran könnte das liegen?

    Ausgabe mit loopnumber = 20000:
    atomic 1 thread: 4
    volatile 1 thread: 1
    single volatile 1 thread: 1
    atomic 2 threads: 7
    volatile 2 threads: 4

    Damit kann man leider nicht allzu viel anfangen, std::clock() ist ja nicht besonders genau...
    Die Ausgabe bei loopnumber = 10000000, soweit es eben gekommen ist:
    atomic 1 thread: 1018
    volatile 1 thread: 425
    volatile test 1 thread: 52


Anmelden zum Antworten