Der Blinde Fleck in C++: Garbage Collection

19. März 2012

“C++ is the best language for garbage collection
principally because it creates less garbage.”
— Bjarne Stroustrup

Ein Zitat dem man immer wieder begegnet, und das gerne als Beleg für die Effizienz von C++ gegenüber .NET herangezogen wird – gerne auch als versteckter Seitenhieb, wie ich gerade wieder erfahren musste. (Ein immerhin rhetorisch sehr geschicktes Beispiel auch hier ab 30:50.)

Ich finde das immer etwas ärgerlich. Zum einen hat C++ solche Seitenhiebe nicht nötig, zum anderen ist die Schlussfolgerung leider so falsch wie die Aussage an sich richtig ist.

cpp C++ hat gemeinhin den Ruf schnelleren und effizienteren Code zu produzieren, als C# (wobei hier in der Regel MSIL gemeint ist), Java oder andere Sprachen. Prinzipiell ist das schon richtig, und zwar aus verschiedenen Gründen: Keine Laufzeitumgebung, die sich zwischen Anwendung und Betriebssystem klemmt; kein JIT-Compiler, der der Programmstart verzögert; keine Garbage Collection die… ja, was eigentlich?

Bei der Garbage Collection (GC) geht es zuerst und vor allem um die Freigabe von Speicher.

Hier höre ich schon die ersten Widersprüche: Was ist mit unmanaged Ressourcen? Datenbank connections, File handles, etc.? Genau die werden in .NET nicht über die GC entsorgt, sondern über das Dispose-Pattern – ebenso deterministisch wie unter C++. Der Finalizer ist hier nur ein zusätzliches Sicherheitsnetz.

In C++ gibt man Speicher auf dem Heap direkt über delete über Destruktoren frei, etwa in Smartpointer-Klassen. Der Code hat das unmittelbare Wissen um die Ressource, kann sie also auch ohne zusätzlichen Verwaltungsaufwand entsorgen. Eine GC hat im Vergleich dazu nicht nur die eigentliche Speicherfreigabe zu bewältigen, sondern muss darüber hinaus zunächst mühsam die Ressourcen zusammensuchen. Dazu kommt, dass noch nicht freigegebener Speicher “zusammengeschoben”, also mühsam kopiert werden muss.

Dass davon abgesehen die GC zu einem undefinierten Zeitpunkt losläuft und dann womöglich – zumindest potentiel – kurzzeitig die eigentliche Programmausführung anhält, ist ein Kontrollverlust, der einem C++-Entwickler sauer aufstößt. Bei Licht betrachtet ist es aber nur eine Verschiebung der Arbeit, die der Destruktor sofort ausführt.

So gesehen ist das Zitat und die Schlussfolgerung völlig korrekt: Wer gleich aufräumt hat in Summe weniger Arbeit.

Aber!

Garbage Collection ist hier gar nicht das Thema. Das Thema ist Speicherverwaltung bzw. Management und die Freigabe von Speicher ist hier nur ein Teilausschnitt. Der andere Aspekt ist das Anfordern von Speicher, und hier kippt die Bilanz dann wieder zugunsten von .NET (wobei zu beachten ist, dass das nicht pauschal für jede GC-Implementierung gilt, sondern für die in .NET umgesetzte Strategie).

Zunächst einmal geht .NET davon aus, dass der Heap bis zu einem bestimmten Punkt gefüllt und ab diesem Punkt frei ist. Eine Speicheranforderung reduziert sich dann in der Regel darauf, den Zeiger, der diesen Punkt markiert, um den Umfang des angeforderten Speichers zu verschieben. Was könnte effizienter sein?

Verglichen damit muss ein C++ heap manager damit zurechtkommen, dass Speicheranforderungen und Freigaben ungeordnet auftreten, und so über die Zeit der Speicherplatz auf dem Heap fragmentiert wird. Das erfordert den entsprechenden Aufwand um die genutzten und freien Abschnitte zu verwalten, um benachbarte Blöcke zusammenzufassen und bei Speicheranforderungen eine möglichst optimale Auswahl eines freien Blockes zu gewährleisten. Dieser Aufwand kommt nicht zum Null-Tarif.

Es gibt einen Grund, warum hochgezüchtete heap manager wie SmartHeap oder heap++ eine Auswirkung auf die Performance einer Anwendung haben.

Im Bezug auf Performance und Effizienz geht der Punkt Speicherallokation also ganz klar an .NET.

Neben diesem Effekt kann der .NET Garbage Collector aufgrund des gewählten Algorithmus weitere Vorteile für sich verbuchen: Das Zusammenschieben des noch verwendeten Speichers mag zunächst Arbeit sein, es führt aber zu höherer Lokalität der Zugriffe. Das heißt daß zumindest potentiell weniger Speicher commited werden muss, weniger Page faults auftreten und Prozessor Caches besser genutzt werden. Diese Effekte und weitere Optimierungen in der Implementierung der GC (Generationen, Large Object Heap) basieren aber auf statistischen Erwägungen – sie können zu Buche schlagen, aber ob und in welchem Maße ist im Einzelfall kaum vorhersehbar.

In Summe stehen sich also die Verfahren in C++ – geprägt von Determinismus – und in .NET – geprägt von statistischen Erwägungen – wie Hase und Igel gegenüber. Wobei sie allerdings im Laufe der Zeit ihre Rolle wechseln. Welches Verfahren im konkreten Einzelfall effizienter ist wird viel stärker von den spezifischen Eigenschaften einer Anwendung abhängen (und der Fähigkeit des Entwicklers mit und nicht gegen die Strategie zu arbeiten), als von der Frage Garbage Collection oder nicht.

C++ Code ist in vielen Aspekten effizienter als managed code: Dedizierte Kontrolle über das Speicherlayout ermöglicht effizientere Nutzung von Prozessor Caches; was über Templates typsicher als Iteration formuliert werden kann reduziert der Compiler auf simple Pointer-Inkrements ohne Bereichsprüfung bei jedem Zugriff; der Compiler kann deutlich besser optimieren, als ein JIT-Compiler; neue Möglichkeiten zur Optimierung werden traditionell zunächst in C++-Compilern umgesetzt; … .

Nur: Die Garbage Collection hat in dieser Liste nichts zu suchen.

Anhang: Weitere Informationen zu Garbage Collection…

Die GC in .NET basiert auf einem “mark and compact” Ansatz, kombiniert mit Generationen und einer Sonderbehandlung für große Speicherblöcke (Large Object Heap, LOH). Außerdem gibt es eine Workstation- und eine Server-Variante der GC.

Details dazu im MSDN Magazine: