Ich habe kürzlich zwei wirklich nette und lehrreiche Sprachgespräche gesehen:
Diese erste von Herb Sutter zeigt alle netten und coolen Funktionen von C ++ 0x, warum die Zukunft von C ++ besser als je zuvor erscheint und wie M $ in diesem Spiel als guter Kerl gilt. Der Vortrag dreht sich um Effizienz und wie die Minimierung der Heap-Aktivität sehr oft die Leistung verbessert.
Das andere , von Andrei Alexandrescu, motiviert einen Übergang von C / C ++ zu seinem neuen Spiel-Wechsler D . Die meisten Sachen von D scheinen wirklich gut motiviert und gestaltet zu sein. Eines hat mich jedoch überrascht, nämlich dass D auf Garbage Collection drängt und dass alle Klassen nur durch Referenz erstellt werden . Noch verwirrender ist, dass das Buch The D Programming Language Ref Manual speziell im Abschnitt über Ressourcenmanagement Folgendes zitiert:
Durch die Speicherbereinigung entfällt der mühsame, fehleranfällige Speicherzuordnungs-Tracking-Code, der in C und C ++ erforderlich ist. Dies bedeutet nicht nur eine viel schnellere Entwicklungszeit und geringere Wartungskosten, sondern das resultierende Programm läuft häufig schneller !
Dies steht im Widerspruch zu Sutters ständigem Gerede über die Minimierung der Heap-Aktivität. Ich respektiere sowohl Sutters als auch Alexandrescous Erkenntnisse sehr und bin daher etwas verwirrt über diese beiden Schlüsselfragen
Führt das Erstellen von Klasseninstanzen nicht nur als Referenz zu einer Menge unnötiger Heap-Aktivitäten?
In welchen Fällen können wir die Garbage Collection verwenden, ohne die Laufzeitleistung zu beeinträchtigen?
quelle
Antworten:
So beantworten Sie Ihre beiden Fragen direkt:
Ja, das Erstellen von Klasseninstanzen als Referenz führt zu einer Menge Heap-Aktivität, aber :
ein. In D haben Sie
struct
ebenso wieclass
. Astruct
hat Wertesemantik und kann alles, was eine Klasse kann, außer Polymorphismus.b. Polymorphismus und Wertesemantik haben aufgrund des Schnittproblems nie gut zusammengearbeitet .
c. Wenn Sie in D wirklich eine Klasseninstanz auf dem Stapel in einem leistungskritischen Code zuweisen müssen und sich nicht um den Sicherheitsverlust kümmern, können Sie dies über die
scoped
Funktion ohne unangemessenen Aufwand tun .GC kann mit der manuellen Speicherverwaltung vergleichbar oder schneller sein, wenn:
ein. Sie ordnen den Stapel nach Möglichkeit weiterhin zu (wie dies normalerweise in D der Fall ist), anstatt sich bei allem auf den Heap zu verlassen (wie dies häufig in anderen GC-Sprachen der Fall ist).
b. Sie haben einen erstklassigen Garbage Collector (Ds aktuelle GC-Implementierung ist zugegebenermaßen etwas naiv, obwohl in den letzten Versionen einige wichtige Optimierungen vorgenommen wurden, sodass sie nicht so schlecht ist wie sie war).
c. Sie weisen hauptsächlich kleine Objekte zu. Wenn Sie hauptsächlich große Arrays zuweisen und die Leistung ein Problem darstellt, möchten Sie möglicherweise einige davon auf den C-Heap umstellen (Sie haben Zugriff auf Cs Malloc und frei in D) oder, wenn er eine bestimmte Lebensdauer hat, auf einen anderen Allokator wie RegionAllocator . (RegionAllocator wird derzeit diskutiert und verfeinert, um schließlich in die Standardbibliothek von D aufgenommen zu werden.)
d. Die Raumeffizienz ist Ihnen nicht so wichtig. Wenn Sie den GC zu häufig ausführen, um den Speicherbedarf extrem gering zu halten, leidet die Leistung.
quelle
Der Grund, warum das Erstellen eines Objekts auf dem Heap langsamer ist als das Erstellen auf dem Stapel, besteht darin, dass die Speicherzuweisungsmethoden beispielsweise die Heap-Fragmentierung berücksichtigen müssen. Das Zuweisen von Speicher auf dem Stapel ist so einfach wie das Inkrementieren des Stapelzeigers (eine Operation mit konstanter Zeit).
Mit einem komprimierenden Garbage Collector müssen Sie sich jedoch keine Gedanken über die Heap-Fragmentierung machen. Die Heap-Zuweisungen können so schnell sein wie die Stack-Zuweisungen. Auf der Seite Garbage Collection für die Programmiersprache D wird dies ausführlicher erläutert.
Die Behauptung, dass GC-Sprachen schneller ausgeführt werden, geht wahrscheinlich davon aus, dass viele Programme viel häufiger Speicher auf dem Heap zuweisen als auf dem Stapel. Angenommen, die Heap-Zuweisung könnte in einer GC-Sprache schneller sein, dann haben Sie gerade einen großen Teil der meisten Programme optimiert (Heap-Zuweisung).
quelle
Eine Antwort auf 1):
Solange Ihr Heap zusammenhängend ist , ist das Zuweisen auf ihm genauso billig wie das Zuweisen auf dem Stapel.
Während Sie außerdem Objekte zuweisen, die nebeneinander liegen, ist Ihre Speicher-Caching-Leistung großartig.
Solange Sie nicht den Garbage Collector laufen müssen, ist keine Leistung verloren , und die Haufen bleibt angrenzend.
Das sind die guten Nachrichten :)
Antwort auf 2):
Die GC-Technologie hat große Fortschritte gemacht. Heutzutage sind sie sogar in Echtzeit erhältlich. Das bedeutet, dass die Gewährleistung eines zusammenhängenden Speichers ein richtliniengesteuertes, implementierungsabhängiges Problem ist.
Also wenn
Möglicherweise erzielen Sie eine bessere Leistung.
Antwort auf die nicht gestellte Frage:
Wenn Entwickler von Speicherverwaltungsproblemen befreit sind, haben sie möglicherweise mehr Zeit, um sich mit Aspekten der tatsächlichen Leistung und Skalierbarkeit ihres Codes zu befassen . Das ist auch ein nicht technischer Faktor, der ins Spiel kommt.
quelle
n
Variablen auf dem Stapel in C ++ ist eine Maschinenanweisung. Aufräumen ist Null.Es handelt sich weder um "Garbage Collection" noch um "langwierigen fehleranfälligen" handgeschriebenen Code. Intelligente Zeiger, die wirklich intelligent sind, können Ihnen eine Stapelsemantik verleihen und bedeuten, dass Sie niemals "Löschen" eingeben, aber nicht für die Speicherbereinigung bezahlen. Hier ist ein weiteres Video von Herb , das den Punkt macht - sicher und schnell - genau das wollen wir.
quelle
mov
(insbesondere wenn der Destruktorcode inline ist). Wenn die Zeiger von Threads gemeinsam genutzt werden, benötigen Sie möglicherweise sogar zusätzlichen Code, um sicherzustellen, dass die Zählinkremente / -dekremente atomar sind.T*
und zu arbeitenscoped_ptr<T>
, von denen beide als Referenz gezählt werden.Ein weiterer zu berücksichtigender Punkt ist die 80: 20-Regel. Es ist wahrscheinlich, dass die überwiegende Mehrheit der von Ihnen zugewiesenen Plätze irrelevant ist und Sie nicht viel über einen GC gewinnen, selbst wenn Sie die Kosten dort auf Null drücken könnten. Wenn Sie dies akzeptieren, kann die Einfachheit, die Sie durch die Verwendung eines GC erzielen können, die Kosten für dessen Verwendung verdrängen. Dies gilt insbesondere dann, wenn Sie das Kopieren vermeiden können. Was D bietet, ist ein GC für die 80% -Fälle und der Zugriff auf die Stapelzuweisung und Malloc für die 20%.
quelle
Selbst wenn Sie einen idealen Garbage Collector hätten, wäre er langsamer gewesen als das Erstellen von Dingen auf einem Stapel. Sie müssen also eine Sprache haben, die beides gleichzeitig erlaubt. Darüber hinaus besteht die einzige Möglichkeit, mit Garbage Collector die gleiche Leistung wie mit manuell verwalteten Speicherzuordnungen zu erzielen (richtig ausgeführt), darin, die gleichen Funktionen mit dem Speicher auszuführen, die erfahrene Entwickler in vielen Fällen durchgeführt hätten erfordern, dass Entscheidungen eines Garbage Collectors zur Kompilierungszeit getroffen und zur Laufzeit ausgeführt werden. Normalerweise verlangsamt die Speicherbereinigung die Arbeit, Sprachen, die nur mit dynamischem Speicher arbeiten, sind langsamer und die Vorhersagbarkeit der Ausführung von Programmen, die in diesen Sprachen geschrieben sind, ist gering, während die Ausführungslatenz höher ist. Ehrlich gesagt verstehe ich persönlich nicht, warum man einen Müllsammler brauchen würde. Das manuelle Verwalten des Speichers ist nicht schwierig. Zumindest nicht in C ++. Natürlich macht es mir nichts aus, wenn der Compiler Code generiert, der alle Dinge für mich bereinigt, wie ich es getan hätte, aber dies scheint im Moment nicht möglich zu sein.
quelle
In vielen Fällen kann ein Compiler die Heap-Zuordnung zurück zur Stack-Zuordnung optimieren. Dies ist der Fall, wenn Ihr Objekt den lokalen Bereich nicht verlässt.
Ein anständiger Compiler wird
x
im folgenden Beispiel mit ziemlicher Sicherheit einen Stapel zuweisen:void f() { Foo* x = new Foo(); x->doStuff(); // Assuming doStuff doesn't assign 'this' anywhere // delete x or assume the GC gets it }
Was der Compiler tut, heißt Escape-Analyse .
Außerdem könnte D theoretisch einen sich bewegenden GC haben , was potenzielle Leistungsverbesserungen durch eine verbesserte Cache-Nutzung bedeutet, wenn der GC Ihre Heap-Objekte zusammenfasst. Es bekämpft auch die Haufenfragmentierung, wie in der Antwort von Jack Edmonds erläutert. Ähnliche Dinge können mit der manuellen Speicherverwaltung durchgeführt werden, aber es ist zusätzliche Arbeit.
quelle
Foo
der Konstruktor nochdoStuff
ein Verweis aufx
oder etwas Internes in ihm durchgesickert sind. In D würde der Compiler wissen, dass wenn beide Funktionen wärenpure
, da dies garantiert, dass auf kein veränderliches Modul oder statische Variablen zugegriffen wird, aber in den meisten Sprachen kann er dies nicht ohne Prüfung der Körper dieser Funktionen (was die meisten Compiler nicht tun) ), weil eine globale oder Klassenvariable in einer dieser Funktionen mit einem internen Wertx
- einschließlich sichx
selbst - versehen werden könnte.pure
, kann der D-Compiler erkennen, dass keine Referenzen entgehen, dapure
Funktionen nicht auf veränderbare statische oder Modulvariablen zugreifen können, diese Art der Optimierung jedoch trotzdem nicht durchführen würden. Theoretisch könnte es sein, aber ich glaube nicht, dass ein D-Compiler jemals Klassen als Optimierung auf den Stapel legt.Ein inkrementeller GC mit niedriger Priorität sammelt Müll, wenn keine Task mit hoher Priorität ausgeführt wird. Die Threads mit hoher Priorität werden schneller ausgeführt, da keine Speicherfreigabe erfolgt. Dies ist die Idee von Henrikssons RT Java GC, siehe http://www.oracle.com/technetwork/articles/javase/index-138577.html
quelle
Die Speicherbereinigung verlangsamt tatsächlich den Code. Es fügt dem Programm zusätzliche Funktionen hinzu, die zusätzlich zu Ihrem Code ausgeführt werden müssen. Es gibt auch andere Probleme damit, wie zum Beispiel, dass der GC erst ausgeführt wird, wenn tatsächlich Speicher benötigt wird. Dies kann zu kleinen Speicherlecks führen. Ein weiteres Problem ist, wenn eine Referenz nicht ordnungsgemäß entfernt wird, der GC sie nicht aufnimmt und erneut zu einem Leck führt. Mein anderes Problem mit GC ist, dass es die Faulheit bei Programmierern fördert. Ich bin ein Befürworter des Lernens der Konzepte der Speicherverwaltung auf niedriger Ebene, bevor ich auf eine höhere Ebene springe. Es ist wie in der Mathematik. Sie lernen, wie man nach den Wurzeln eines Quadrats löst oder wie man zuerst eine Ableitung von Hand nimmt, dann lernen Sie, wie man es auf dem Taschenrechner macht. Verwenden Sie diese Dinge als Werkzeug, nicht als Krücken.
Wenn Sie Ihre Leistung nicht beeinträchtigen möchten, sollten Sie sich über den GC und die Verwendung von Heap und Stack im Klaren sein.
quelle
Mein Punkt ist, dass GC Malloc unterlegen ist, wenn Sie normale prozedurale Programmierung durchführen. Sie gehen einfach von Prozedur zu Prozedur, ordnen sie zu und geben sie frei, verwenden globale Variablen und deklarieren einige Funktionen _inline oder _register. Dies ist C-Stil.
Sobald Sie jedoch eine höhere Abstraktionsschicht erreicht haben, müssen Sie mindestens die Referenzzählung durchführen. Sie können also als Referenz übergeben, zählen und freigeben, sobald der Zähler Null ist. Dies ist gut und Malloc überlegen, nachdem die Anzahl und Hierarchie der Objekte zu schwierig für die manuelle Verwaltung geworden ist. Dies ist im C ++ - Stil. Sie definieren Konstruktoren und Destruktoren, um die Zähler zu erhöhen. Sie kopieren und ändern, sodass das gemeinsam genutzte Objekt in zwei Teile geteilt wird, sobald ein Teil davon von einer Partei geändert wurde, eine andere Partei jedoch weiterhin den ursprünglichen Wert benötigt. So können Sie eine große Datenmenge von Funktion zu Funktion übergeben, ohne zu überlegen, ob Sie Daten hier kopieren oder nur einen Zeiger dorthin senden müssen. Das Nachzählen erledigt diese Entscheidungen für Sie.
Dann kommt die ganz neue Welt, Schließungen, funktionale Programmierung, Ententypisierung, Zirkelverweise, asynchrone Ausführung. Code und Daten beginnen sich zu vermischen. Sie übergeben die Funktion häufiger als normale Daten als Parameter. Sie erkennen, dass die Metaprogrammierung ohne Makros oder Vorlagen durchgeführt werden kann. Ihr Code fängt an, in den Himmel einzutauchen und festen Boden zu verlieren, weil Sie etwas in Rückrufen von Rückrufen von Rückrufen ausführen, Daten nicht mehr verwurzelt werden, Dinge asynchron werden und Sie von Schließungsvariablen abhängig werden. Hier ist also eine zeitgesteuerte, speicherwandelnde GC die einzig mögliche Lösung, da sonst Schließungen und Zirkelverweise überhaupt nicht möglich sind. Dies ist JavaScript-Methode.
Sie haben D erwähnt, aber D ist in C ++ immer noch verbessert. Daher wählen Sie wahrscheinlich Malloc- oder Ref-Counting in Konstruktoren, Stapelzuordnungen und globale Variablen (auch wenn es sich um komplizierte Bäume von Entitäten aller Art handelt).
quelle