Speicher und Performanz Overhead von Smart Pointern

C++ bietet vier verschiedene Smart Pointer an. Zwei davon will ich mir in diesem Artikel unter dem Aspekt Overhead in puncto Speicher und Performanz genauer anschauen. Während mein erster Kandidat std::unique_ptr den Lebenszyklus genau einer Ressource verwaltet, teilt sich mein zweiter Kandidat std::shared_ptr typischerweise seine Ressource mit anderen std::shared_ptr. Ich nehme das Ergebnis meines Tests gerne vorweg, bevor die nackten Zahlen kommen. Es gibt wenige Gründe in modernem C++, das Speichermanagment mit new und delete explizit in die Hand zu nehmen. 

 

Warum? Jetzt kommen die Fakten.

Speicher Overhead

std::unique_ptr

std::unique_ptr benötigt in der Regel keinen zusätzlichen Speicher. Das heißt, ein std::unique_ptr ist nicht größer als der Zeiger auf den Speicher, den er verwaltet. Warum sage ich in der Regel? Da ein std::unique_ptr mit einer Löschfunktion parametrisiert werden kann, benötigt diese natürlich Speicher. Das ist aber nicht der Standardfall.

Im Gegensatz dazu besitzt der std::shared_ptr einen kleinen Overhead.

std::shared_ptr

Mehrere std::shared_ptr teilen sich zusammen eine gemeinsame Variable. Dabei führen sie intern einen Referenzzähler mit. Das heißt, jedes Mal wenn der std::shared_ptr kopiert wird, wird der Referenzzähler erhöht, bzw. erniedrigt, wenn ein std::shared_ptr seinen Gültigkeitsbereich verlässt. Daher benötigt ein std::shared_ptr zusätzlichen Speicher für den Referenzzähler. Das ist auch schon der minimale Overhead, den ein std::shared_ptr gegenüber einem rohen Zeiger besitzt.

Performanz Overhead

Die Geschichte zur Performanz ist ein wenig komplizierter. Dazu lasse ich die Zahlen sprechen. Ein einfacher Performanztest soll Klarheit schaffen.

Der Performanztest

In dem Test fordere ich 100000000 Mal Speicher an und gebe ihn wieder frei. Genau die Zeit für all diese Operationen interessiert mich.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// all.cpp

#include <chrono>
#include <iostream>

static const long long numInt= 100000000;

int main(){

  auto start = std::chrono::system_clock::now();

  for ( long long i=0 ; i < numInt; ++i){
    int* tmp(new int(i));
    delete tmp;
    // std::shared_ptr<int> tmp(new int(i));
    // std::shared_ptr<int> tmp(std::make_shared<int>(i));
    // std::unique_ptr<int> tmp(new int(i));
    // std::unique_ptr<int> tmp(std::make_unique<int>(i));
  }

  std::chrono::duration<double> dur= std::chrono::system_clock::now() - start;
  std::cout << "time native: " << dur.count() << " seconds" << std::endl;

}

 

In meinem Test stelle ich den expliziten Umgang mit new und delete (Zeile 13 und 14)  dem Einsatz von std::shared_ptr (Zeile 15), std::make_shared (Zeile 16), std::unique_ptr (Zeile 17) und std::make_unique (Zeile 18) gegenüber. Selbst in dem kleinen Programm ist Umgang mit den Smart Pointern in Zeile 15 - Zeile 18 deutlich einfacher, da die Smart Pointer in Zeile 19 ihren Gültigkeitsbereich verlassen und somit automatisch der Speicher der dynamisch angelegten int Variable freigegeben wird.

Die zwei Funktionen std::make_shared (Zeile 16) und std::make_unique (Zeile 18) sind praktische Hilfsfunktionen, die einen entsprechenden Smart Pointer direkt erzeugen. Besonders interessant ist std::make_shared. Beim Anlegen eines std::shared_ptr sind zwei Speicherallokationen notwendig. Zum einen wird Speicher für die verwaltete Variable angefordert, zum anderen für den Referenzzähler. Aus diesen zwei teuren Speicheranforderungen macht std::make_shared eine. Die Performanz bedankt sich. std::make_unique gibt es erst seit C++14. Die ganze andere Funktionalität steht schon seit C++11 zur Verfügung.

Für meine Tests verwende ich zm einen den GCC 4.9.2 und den cl.exe, der Bestandteil von Microsoft Visual Studio 2015 ist. Obwohl dieser Compiler zum einen nur C++11 unterstützt, bietet er zum anderen std::make_unique schon an. Damit kann ich alle Tests mit maximaler und ohne Optimierung auf Linux und Windows durchführen. Zur Ehrenrettung von cl.exe muss ich sagen, dass mein Windows PC deutlich schwächer ist als meine Linux PC. Daher liegt der Schwerpunkt meiner Analyse darauf, die Performanz des expliziten Speichermanagements mit der der Smart Pointer zu vergleichen. Ich vergleiche nicht Windows mit Linux.

Hier kommen die nackten Zahlen in Sekunden.

Die nackten Zahlen

Der Einfachheit halber verzichte ich auf die Screenshots und habe die Ergebnisse bereits in einer Tabelle zusammengefasst.

comparison

Ein paar sehr interessante Erkenntnisse lassen sich aus der Tabelle ableiten.

  1. Optimierung zahlt sich aus. Im Falle von std::make_shared ist der Performanzunterschied nahezu 10 fach unter Linux. Aber auch im Falle der anderen Variationen ist die optimierte Variante um den Faktor 2 bis 3 schneller. Das gilt für Linux als auch für Windows. Wird hingegen new und delete verwendet, gibt es kein großes Optimierungspotential mehr für den Compiler.
  2. std::unique_ptr, std::make_unique und mit kleinen Abstrichen std::make_shared spielen in der gleichen Liga wie new und delete.
  3. std::shared_ptr und std::make_shared sollten nicht ohne Optimierung verwendet werden. std::shared_ptr ist selbst mit Optimierung um dem Faktor zwei langsamer als new und delete.

Mein Fazit

  • std::unique_ptr besitzt keinen Speicher und Performanz Overhead gegenüber dem expliziten Speichermanagement mit new und delete. Das ist großartig. Bietet std::unique_ptr doch einen deutlichen Mehrwert an, indem er automatisch den Lebenszyklus seiner Ressource verwaltet und dies, ohne irgendwelche zusätzliche Kosten zu verursachen.
  • Für std::shared_ptr ist mein Fazit schon ein bisschen schwieriger. Ja, ich gebe zu, dass std::shared_ptr ca. um den Faktor zwei langsamer ist als new und delete. Aber, diese Rechnung ist falsch, denn std::shared_ptr teilen sich ihre Objekte. Das heißt insbesondere, dass der erste std::shared_ptr zwar Speicher für das Objekt und den Referenzzähler einmalig anfordern muss, das heißt aber auch, dass jeder weitere std::shared_ptr die Verwaltungsstruktur für die geteilte Variable mit nutzen kann.

Daher will ich mich gerne wiederholen. Es gibt wenige Gründe in modernem C++, das Speichermanagment mit new und delete explizit in die Hand zu nehmen.

Wie geht's weiter?

Nach diesem Plädoyer für Smart Pointer folgen im nächsten Artikel die Details zu dem std::unique_ptr.

 

 

 

 

 

 

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.

 

Mentoring

Stay Informed about my Mentoring

 

Rezensionen

Tutorial

Besucher

Heute 1819

Gestern 2204

Woche 16689

Monat 35806

Insgesamt 3797899

Aktuell sind 179 Gäste und keine Mitglieder online

Kubik-Rubik Joomla! Extensions

Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare