Besonderheiten des std::shared_ptr

Inhaltsverzeichnis[Anzeigen]

Nachdem ich im letzten Artikel das große Bild zu std::shared_ptr beschrieben habe, komme ich heute zu zwei besonderen Aspekten der Smart Pointer. Zum einen zeige ich mit std::shared_from_this, wie sich ein std::shared_ptr von einem Objekt erzeugen lässt, zum anderen beschäftigt mich die Frage: Soll ein std::shared_ptr per Copy oder per Referenz übergeben werden. Insbesondere der zweite Punkt besitzt einiges an Überraschungspotential.

 

std::shared_ptr von this

Mit Hilfe der Klasse std::enable_shared_from_this lassen sich Objekte erzeugen, die einen std::shared_ptr auf sich zurückgeben. Dazu muss die Klasse der zu teilenden Objekte öffentlich von std::enable_shared_from_this abgeleitet werden. Somit steht in der Klasse die Methode shared_from_this zur Verfügung, mit der sich std::shared_ptr auf sich erzeugen lassen.

Das Beispiel zeigt die Theorie in der Anwendung.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// enableShared.cpp

#include <iostream>
#include <memory>

class ShareMe: public std::enable_shared_from_this<ShareMe>{
public:
  std::shared_ptr<ShareMe> getShared(){
    return shared_from_this();
  }
};

int main(){

  std::cout << std::endl;

  std::shared_ptr<ShareMe> shareMe(new ShareMe);
  std::shared_ptr<ShareMe> shareMe1= shareMe->getShared();
  {
    auto shareMe2(shareMe1);
    std::cout << "shareMe.use_count(): "  << shareMe.use_count() << std::endl;
  }
  std::cout << "shareMe.use_count(): "  << shareMe.use_count() << std::endl;
  
  shareMe1.reset();
  
  std::cout << "shareMe.use_count(): "  << shareMe.use_count() << std::endl;

  std::cout << std::endl;

}

 

Der Smart Pointer shareMe (Zeile 17) und seine Kopien shareMe1 (Zeile 18) und  shareMe2 (Zeile 20) verweisen auf die gleiche Ressource und in- und dekrementieren den Referenzzähler.

 enabledShared

Durch den Aufruf shareMe->getShared() in Zeile 18 wird ein neuer Smart Pointer erzeugt. Die Methode verwendet dabei die Funktion shared_from_this in Zeile 9.

Eine Besonderheit besitzt die Klasse ShareMe.

Curiously recurring template pattern 

 ShareMe ist die abgeleitete Klasse als auch Typargument (Zeile 6) der Basisklasse std::enabled_shared_from_this. Diese vermeintliche Rekursion ist unter dem Namen CRTP bekannt und steht für curiously recurring template pattern. Tatsächlich findet keine Rekursion statt, da die Methoden der Klasse, die von sich selber ableitet (in dem konkreten Fall ShareMe), erst dann instanziiert werden, wenn sie aufgerufen werden. CRTP ist ein beliebtes Idiom in C++, um statischen Polymorphismus umzusetzen. Im Gegensatz zum dynamischen Polymorphismus mit der Hilfe von virtuellen Methoden, findet beim statischen Polymorphismus der Dispatch des Methodenaufrufs zur Compilezeit statt. 

Nun ab zurück zu den std::shared_ptr.

std::shared_ptr als Funktionsargument

Damit kommen wir zu einer sehr interessanten Fragen. Soll eine Funktion einen std::shared_ptr per Referenz oder per Copy annehmen? Warum soll ich mir überhaupt dazu Gedanken machen. Ist es nicht egal, ob meine Funktion einen std::shared_ptr per Referenz oder per Copy annimmt? Unter der Decke ist alles eine Referenz. Die eindeutige Antwort ist jein. Semantisch macht es keinen Unterschied. Aus Performanzsicht aber unter Umständen schon.

Zuerst ein kleines Beispiel, auf der meine Performanzbetrachtung basieren.

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// refVersusCopySharedPtr.cpp

#include <memory>
#include <iostream>

void byReference(std::shared_ptr<int>& refPtr){
  std::cout << "refPtr.use_count(): " << refPtr.use_count() << std::endl;
}

void byCopy(std::shared_ptr<int> cpyPtr){
  std::cout << "cpyPtr.use_count(): " << cpyPtr.use_count() << std::endl;
}


int main(){

    std::cout <<  std::endl;

    auto shrPtr= std::make_shared<int>(2011);

    std::cout << "shrPtr.use_count(): " << shrPtr.use_count() << std::endl;

    byReference(shrPtr);
    byCopy(shrPtr);
    
    std::cout << "shrPtr.use_count(): " << shrPtr.use_count() << std::endl;
    
    std::cout << std::endl;
    
}

 

Die Funktionen byReference (Zeile 6 - 8)  und byCopy (Zeile 10 - 12) nehmen ihren std::shared_ptr per Referenz und per Copy an. Die Ausgabe des Programms bringt die entscheidende Beobachtung auf den Punkt.

 refVersusCopySharedPtr

Da die Funktion byCopy ihren std::shared_ptr kopiert, wird der Referenzzähler im Funktionskörper auf 2 erhöht. Die Frage ist nun natürlich. Wie teuer ist das In- und Dekrementieren des Referenzzählers. Da der C++-Standard zusichert, dass das Ändern des Referenzzählers eine atomare Operation ist, erwarte ich einen messbaren Unterschied. Was sagen die Zahlen?

Performanzvergleich 

Wer meine Performanzvergleiche kennt, weiß, dass mein Linux PC deutlich leistungsfähiger ist als mein Windows PC. Daher sind die absoluten Zahlen mit Vorsicht zu genießen. Bei meinem Test kommt der GCC 4.8 und Microsoft Visual Studio 15 zum Einsatz. Darüber hinaus übersetze ich die Programme ohne und mit maximaler Optimierung. Zuerst zu meinem kleinen Testprogramm.

In dem Testprogramm übergebe ich einen std::shared_ptr per Referenz und per Copy einer Funktion und verwende diesen in der Funktion, um einen lokalen std::shared_ptr zu initialisieren. Dies war das einfachste Szenario, mit dem ich den Optimierer überlisten konnte. Ach ja. Jede Funktionen rufe ich jeweils 100 Millionen Mal auf.

Das Programm

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// performanceRefCopyShared.cpp

#include <chrono>
#include <memory>
#include <iostream>

constexpr long long mill= 100000000;

void byReference(std::shared_ptr<int>& refPtr){
  volatile auto tmpPtr(refPtr);
}

void byCopy(std::shared_ptr<int> cpyPtr){
  volatile auto tmpPtr(cpyPtr);
}


int main(){

    std::cout <<  std::endl;
    
    auto shrPtr= std::make_shared<int>(2011);
   
    auto start = std::chrono::steady_clock::now();
  
    for (long long i= 0; i <= mill; ++i) byReference(shrPtr);    
    
    std::chrono::duration<double> dur= std::chrono::steady_clock::now() - start;
    std::cout << "by reference: " << dur.count() << " seconds" << std::endl;
    
    start = std::chrono::steady_clock::now();
    
    for (long long i= 0; i<= mill; ++i){
        byCopy(shrPtr);
    }
    
    dur= std::chrono::steady_clock::now() - start;
    std::cout << "by copy: " << dur.count() << " seconds" << std::endl;
    
    std::cout << std::endl;
    
}

 

Zuerst zu der Variante ohne Optimierung.

Ohne Optimierung

 performanceperformanceWindows

Und nun zur optimierten Variante.

Mit maximaler Optimierung

performanceOptimizationperformanceOptimizationWindows

Fazit

comparison

Die nackten Zahlen des Programms performanceCopyShared.cpp sprechen eine eindeutige Sprache.

  • Die perReference Funktion ist ca. um den Faktor 2 schneller als ihr perCopy Pendant. Im Falle der maximalen Optimierung unter Linux sogar um den Faktor 5.
  • Maximale Optimierung bringt unter Windows ein Performanzboost um den Faktor 3, unter Linux ca. um den Faktor 30 - 80. 
  • Ohne Optimierung ist die Windows Applikation für beide Funktionsaufrufe schneller. Bei maximaler Optimierung aber deutlich langsamer als Linux.

Wie geht's weiter?

Das klassische Problem von Smart Pointer, die ihre Ressourcen mittels Referenzzähler verwalten, sind zyklische Referenzen. Hier springt std::weak_ptr in die Bresche. Im nächsten Artikel werde ich mir den besonderen Smart Pointer std::weak_ptr genauer anschauen und mit seiner Hilfe zyklische Referenzen brechen. 

 

 

 

 

 

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Hole dir dein E-Book. Unterstütze meinen Blog.

 

 

 

 

Kommentar schreiben


Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare