In diesen Tagen werden so viele Sprachen Müll gesammelt. Es ist sogar für C ++ von Drittanbietern erhältlich. Aber C ++ hat RAII und intelligente Zeiger. Wozu dient die Garbage Collection? Macht es etwas extra?
Und wenn in anderen Sprachen wie C # alle Verweise nach Spezifikation und Implementierung als intelligente Zeiger behandelt werden (RAII beiseite lassen), werden dann immer noch Garbage Collectors benötigt? Wenn nein, warum ist das nicht so?
garbage-collection
smart-pointer
Gulshan
quelle
quelle
Antworten:
Ich gehe davon aus, dass Sie Smart Pointer mit Referenzzähler meinen, und ich stelle fest, dass es sich um eine (rudimentäre) Form der Speicherbereinigung handelt. Daher beantworte ich die Frage: "Was sind die Vorteile anderer Formen der Speicherbereinigung gegenüber Smart Pointer mit Referenzzähler?" stattdessen.
Genauigkeit . Alleine die Referenzzählung führt zu einem Verlust von Zyklen, sodass bei intelligenten Zeigern mit Referenzzählung im Allgemeinen ein Verlust von Speicher auftritt, sofern keine anderen Techniken zum Abfangen von Zyklen hinzugefügt werden. Sobald diese Techniken hinzugefügt wurden, ist der Vorteil der Einfachheit der Referenzzählung verschwunden. Beachten Sie auch, dass auf dem Gültigkeitsbereich basierende Referenzzählungs- und Ablaufverfolgungs-GCs Werte zu unterschiedlichen Zeitpunkten erfassen, manchmal werden Referenzzählungen früher erfasst und manchmal werden Ablaufverfolgungs-GCs früher erfasst.
Durchsatz . Intelligente Zeiger sind eine der am wenigsten effizienten Formen der Speicherbereinigung, insbesondere im Kontext von Multithread-Anwendungen, bei denen die Referenzzahlen atomar erhöht werden. Es gibt fortschrittliche Referenzzähltechniken, mit denen dies gelindert werden soll. In Produktionsumgebungen sind Tracing-GCs jedoch immer noch der Algorithmus der Wahl.
Latenz . Typische Smart Pointer-Implementierungen ermöglichen Destruktoren eine Lawine, was zu unbegrenzten Pausenzeiten führt. Andere Arten der Müllabfuhr sind wesentlich inkrementeller und können sogar in Echtzeit erfolgen, z. B. das Laufband von Baker.
quelle
Da niemand es aus diesem Blickwinkel betrachtet hat, werde ich Ihre Frage umformulieren: Warum etwas in die Sprache bringen, wenn Sie es in einer Bibliothek tun können? Das Ignorieren spezifischer Implementierungs- und syntaktischer Details bei GC / Smart Pointern ist im Grunde ein Sonderfall dieser Frage. Warum einen Garbage Collector in der Sprache selbst definieren, wenn Sie ihn in einer Bibliothek implementieren können?
Es gibt ein paar Antworten auf diese Frage. Das Wichtigste zuerst:
Sie stellen sicher, dass der gesamte Code für die Interaktion verwendet werden kann. Dies ist meiner Meinung nach der Hauptgrund, warum sich die Wiederverwendung von Code und die gemeinsame Nutzung von Code erst mit Java / C # / Python / Ruby wirklich bemerkbar gemacht haben. Bibliotheken müssen kommunizieren, und die einzige zuverlässige gemeinsame Sprache, die sie haben, ist die Sprache selbst (und zu einem gewissen Grad die Standardbibliothek). Wenn Sie jemals versucht haben, Bibliotheken in C ++ wiederzuverwenden, haben Sie wahrscheinlich den entsetzlichen Schmerz erfahren, den keine Standard-Speichersemantik verursacht. Ich möchte eine Struktur an eine Bibliothek übergeben. Übergebe ich eine Referenz? Zeiger?
scoped_ptr
?smart_ptr
? Übergebe ich das Eigentum oder nicht? Gibt es eine Möglichkeit, das anzuzeigen? Was ist, wenn die Bibliothek etwas zuweisen muss? Muss ich ihm einen Allokator geben? Da die Speicherverwaltung nicht Teil der Sprache ist, muss jedes Bibliothekspaar in C ++ eine eigene Strategie aushandeln, und es ist wirklich schwierig, alle zur Übereinstimmung zu bringen. GC macht das zu einem absoluten No-Issue.Sie können die Syntax darum herum entwerfen. Da C ++ die Speicherverwaltung selbst nicht kapselt, muss es eine Reihe syntaktischer Hooks bereitstellen, damit Code auf Benutzerebene alle Details ausdrückt. Sie haben Zeiger, Verweise,
const
Dereferenzierungsoperatoren, Indirektionsoperatoren, Adressen usw. Wenn Sie die Speicherverwaltung in die Sprache selbst übertragen, kann die Syntax entsprechend angepasst werden. Alle diese Operatoren verschwinden und die Sprache wird übersichtlicher und einfacher.Sie erzielen einen hohen Return on Investment. Der Wert, den ein bestimmtes Stück Code generiert, wird mit der Anzahl der Benutzer multipliziert. Dies bedeutet, je mehr Benutzer Sie haben, desto mehr können Sie sich leisten, für ein Stück Software auszugeben. Wenn Sie ein Feature in die Sprache verschieben, wird es von allen Benutzern der Sprache verwendet. Dies bedeutet, dass Sie mehr Aufwand dafür aufwenden können als für eine Bibliothek, die nur von einer Teilmenge dieser Benutzer verwendet wird. Aus diesem Grund verfügen Sprachen wie Java und C # über absolut erstklassige VMs und fantastisch hochwertige Garbage Collectors: Die Kosten für deren Entwicklung amortisieren sich bei Millionen von Benutzern.
quelle
Dispose
ein Objekt aufruft, das eine Bitmap kapselt, ist jeder Verweis auf dieses Objekt ein Verweis auf ein entsorgtes Bitmap-Objekt. Wenn das Objekt vorzeitig gelöscht wurde, während es von einem anderen Code noch verwendet werden soll, kann die Bitmap-Klasse sicherstellen, dass der andere Code auf vorhersehbare Weise fehlschlägt . Im Gegensatz dazu ist die Verwendung eines Verweises auf den freigegebenen Speicher ein undefiniertes Verhalten.Speicherbereinigung bedeutet im Grunde nur, dass Ihre zugewiesenen Objekte automatisch freigegeben werden, sobald sie nicht mehr erreichbar sind.
Genauer gesagt, werden sie freigegeben, wenn sie für das Programm nicht mehr erreichbar sind , da Objekte, auf die zirkulär verwiesen wird, ansonsten niemals freigegeben würden.
Intelligente Zeiger beziehen sich nur auf jede Struktur, die sich wie ein gewöhnlicher Zeiger verhält, jedoch einige zusätzliche Funktionen enthält. Dazu gehören unter anderem die Freigabe, aber auch Copy-on-Write-Prüfungen, gebundene Schecks, ...
Wie Sie bereits ausgeführt haben, können jetzt intelligente Zeiger verwendet werden , um eine Form der Garbage Collection zu implementieren.
Aber der Gedankengang geht folgendermaßen:
Natürlich können Sie es von Anfang an so gestalten. C # wurde entwickelt , um Müll zu sammeln, also nur
new
Ihr Objekt und es wird freigegeben, wenn die Verweise außerhalb des Gültigkeitsbereichs fallen. Wie das gemacht wird, liegt beim Compiler.In C ++ war jedoch keine Garbage Collection vorgesehen. Wenn wir einen Zeiger zuweisen
int* p = new int;
und dieser außerhalb des Gültigkeitsbereichs liegt,p
wird er vom Stapel entfernt, aber niemand kümmert sich um den zugewiesenen Speicher.Jetzt haben Sie von Anfang an nur noch deterministische Destruktoren . Wenn ein Objekt den Bereich verlässt, in dem es erstellt wurde, wird sein Destruktor aufgerufen. In Kombination mit Vorlagen und Überladen von Operatoren können Sie ein Wrapper-Objekt entwerfen, das sich wie ein Zeiger verhält, jedoch Destruktorfunktionen zum Bereinigen der damit verbundenen Ressourcen (RAII) verwendet. Sie nennen dies einen intelligenten Zeiger .
Dies ist alles sehr spezifisch für C ++: Überladen von Operatoren, Vorlagen, Destruktoren, ... In dieser speziellen Sprachsituation haben Sie intelligente Zeiger entwickelt, um Ihnen den gewünschten GC bereitzustellen.
Wenn Sie jedoch von Anfang an eine Sprache mit GC entwerfen, handelt es sich lediglich um ein Implementierungsdetail. Sie sagen nur, dass das Objekt bereinigt wird und der Compiler dies für Sie erledigt.
Intelligente Zeiger wie in C ++ wären wahrscheinlich nicht einmal in Sprachen wie C # möglich, die überhaupt keine deterministische Zerstörung aufweisen (C # umgeht dies, indem es syntaktischen Zucker zum Aufrufen
.Dispose()
bestimmter Objekte bereitstellt ). Nicht referenzierte Ressourcen werden schließlich vom GC zurückgefordert, aber es ist nicht definiert, wann genau dies geschieht.Dies wiederum kann es dem GC ermöglichen, seine Arbeit effizienter zu erledigen. Wird in tiefer in die Sprache als intelligenter Zeiger gebaut, die oben drauf gesetzt werden, kann das .NET GC zB Speicheroperationen verzögern und sie in den Blöcken führen sie billiger zu machen oder sogar bewegen Speicher um zur Steigerung der Effizienz auf , wie oft Objekte abgerufen werden.
quelle
IDisposable
undusing
. Es erfordert jedoch ein wenig Programmieraufwand, weshalb es normalerweise nur für sehr knappe Ressourcen wie Datenbankverbindungshandles verwendet wird.IDisposable
Syntax, indem es nur konventionellelet ident = value
durchuse ident = value
... ersetztusing
hat überhaupt nichts mit Garbage Collection zu tun, sondern ruft nur eine Funktion auf, wenn eine Variable aus dem Geltungsbereich fällt, genau wie Destruktoren in C ++.Meiner Meinung nach gibt es zwei große Unterschiede zwischen Garbage Collection und Smart Pointern für die Speicherverwaltung:
Ersteres bedeutet, dass GC Müll sammelt, den intelligente Zeiger nicht sammeln. Wenn Sie intelligente Zeiger verwenden, müssen Sie diese Art von Müll vermeiden oder darauf vorbereitet sein, manuell damit umzugehen.
Letzteres bedeutet, dass die Funktionsweise von intelligenten Zeigern die Arbeitsthreads in Ihrem Programm verlangsamt, unabhängig davon, wie intelligent sie sind. Garbage Collection kann die Arbeit verschieben und in andere Threads verschieben. das macht es insgesamt effizienter (in der Tat sind die Laufzeitkosten eines modernen GC geringer als bei einem normalen malloc / free-System, auch ohne den zusätzlichen Aufwand von intelligenten Zeigern) und erledigen die Arbeit, die es noch tun muss, ohne in das System einzusteigen Weg der Anwendung Threads.
Beachten Sie nun, dass intelligente Zeiger als programmatische Konstrukte verwendet werden können, um alle möglichen anderen interessanten Dinge zu erledigen - siehe Darios Antwort -, die vollständig außerhalb des Bereichs der Garbage Collection liegen. Wenn Sie diese ausführen möchten, benötigen Sie intelligente Zeiger.
Zum Zwecke der Speicherverwaltung sehe ich jedoch keine Aussicht darauf, dass intelligente Zeiger die Speicherbereinigung ersetzen. Sie sind einfach nicht so gut darin.
quelle
using
Block in nachfolgenden Versionen von C # eingeführt hat. Darüber hinaus kann das nicht deterministische Verhalten von GCs in Echtzeitsystemen verbieten (weshalb GCs dort nicht verwendet werden). Vergessen wir auch nicht, dass GCs so komplex sind, dass tatsächlich ein Speicherverlust auftritt und ziemlich ineffizient sind (z. B. Boehm…).Der Begriff Müllabfuhr impliziert, dass Müll gesammelt werden muss. In C ++ gibt es intelligente Zeiger in verschiedenen Varianten, vor allem den unique_ptr. Der unique_ptr ist im Grunde genommen ein Konstrukt mit einem einzigen Eigentümer und Gültigkeitsbereich. In einem gut gestalteten Teil des Codes würden sich die meisten Heap-zugewiesenen Dinge normalerweise hinter den intelligenten Zeigern unique_ptr befinden, und der Besitz dieser Ressourcen ist jederzeit gut definiert. Es gibt kaum Overhead in unique_ptr und unique_ptr beseitigt die meisten manuellen Speicherverwaltungsprobleme, die die Benutzer traditionell zu verwalteten Sprachen geführt haben. Da immer mehr Kerne gleichzeitig ausgeführt werden, werden die Entwurfsprinzipien, die den Code dazu bringen, zu jedem Zeitpunkt eindeutige und genau definierte Eigentumsrechte zu verwenden, für die Leistung immer wichtiger.
Selbst in einem gut gestalteten Programm, insbesondere in Umgebungen mit mehreren Threads, kann nicht alles ohne gemeinsam genutzte Datenstrukturen ausgedrückt werden, und für die Datenstrukturen, die wirklich benötigt werden, müssen Threads kommunizieren. RAII in c ++ funktioniert in einem Single-Thread-Setup ziemlich gut, da in einem Multi-Thread-Setup die Lebensdauer von Objekten möglicherweise nicht vollständig hierarchisch gestapelt ist. In diesen Situationen bietet die Verwendung von shared_ptr einen großen Teil der Lösung. Sie erstellen ein gemeinsames Eigentum an einer Ressource, und dies ist in C ++ der einzige Ort, an dem wir Müll sehen. Bei so geringen Mengen sollte jedoch ein ordnungsgemäß gestaltetes c ++ - Programm eher als Implementierung einer 'Wurf'-Auflistung mit gemeinsam genutzten ptrs als als vollständige Garbage-Auflistung betrachtet werden in anderen Sprachen implementiert. C ++ hat einfach nicht so viel 'Müll'
Wie von anderen angegeben, sind intelligente Zeiger mit Referenzzählung eine Form der Speicherbereinigung, und eine für diese hat ein Hauptproblem. Das Beispiel, das hauptsächlich als Nachteil referenzierter Formen der Speicherbereinigung verwendet wird, ist das Problem bei der Erstellung verwaister Datenstrukturen, die mit intelligenten Zeigern miteinander verbunden sind und Objektcluster erstellen, die sich gegenseitig davon abhalten, gesammelt zu werden. Während in einem Programm, das nach dem Akteurmodell der Berechnung entworfen wurde, die Datenstrukturen normalerweise nicht zulassen, dass solche nicht sammelbaren Cluster in C ++ entstehen, wenn Sie den umfassenden Ansatz der geteilten Daten für die Multithread-Programmierung verwenden, wie er überwiegend verwendet wird In der Branche können diese verwaisten Cluster schnell Realität werden.
Zusammenfassend lässt sich sagen, dass Sie unter Verwendung gemeinsamer Zeiger die weit verbreitete Verwendung von unique_ptr in Kombination mit dem Rechenansatz des Actor-Modells für die Multithread-Programmierung und die eingeschränkte Verwendung von shared_ptr als andere Formen der Garbage Collection nicht kaufen zusätzliche Vorteile. Wenn Sie jedoch bei einem Ansatz, bei dem alles gemeinsam genutzt wird, überall auf shared_ptr stoßen, sollten Sie erwägen, entweder die Parallelitätsmodelle zu wechseln oder auf eine verwaltete Sprache zu wechseln, die stärker auf die gemeinsame Nutzung von Eigentümern und den gleichzeitigen Zugriff auf Datenstrukturen ausgerichtet ist.
quelle
Rust
keine Müllabfuhr benötigen?Die meisten intelligenten Zeiger werden mithilfe der Referenzzählung implementiert. Das heißt, jeder Smart Pointer, der sich auf ein Objekt bezieht, erhöht den Referenzzähler des Objekts. Wenn dieser Zähler auf Null geht, wird das Objekt freigegeben.
Das Problem ist, wenn Sie Zirkelverweise haben. Das heißt, A hat einen Verweis auf B, B hat einen Verweis auf C und C hat einen Verweis auf A. Wenn Sie intelligente Zeiger verwenden, müssen Sie den mit A, B und C verknüpften Speicher manuell freigeben Holen Sie sich dort ein "break" der Zirkelreferenz (zB mit
weak_ptr
in C ++).Die Speicherbereinigung funktioniert (normalerweise) ganz anders. Die meisten Müllsammler verwenden heutzutage einen Erreichbarkeitstest . Das heißt, es werden alle Verweise auf dem Stapel und die global zugänglichen Verweise betrachtet und anschließend alle Objekte, auf die sich diese Verweise beziehen, sowie die Objekte, auf die sie sich beziehen, usw. verfolgt. Alles andere ist Müll.
Auf diese Weise spielen Zirkelverweise keine Rolle mehr - solange weder A noch B oder C erreichbar sind , kann der Speicher zurückgewonnen werden.
Die "echte" Müllabfuhr bietet noch weitere Vorteile. Zum Beispiel ist die Speicherzuweisung extrem billig: Erhöhen Sie einfach den Zeiger auf das "Ende" des Speicherblocks. Die Aufhebung der Zuordnung hat ebenfalls konstante fortgeführte Anschaffungskosten. Natürlich können Sie in Sprachen wie C ++ die Speicherverwaltung so gut wie beliebig implementieren, sodass Sie eine noch schnellere Allokationsstrategie entwickeln können.
Natürlich ist in C ++ die Menge des Heap-zugewiesenen Speichers in der Regel geringer als in einer referenzlastigen Sprache wie C # /. NET. Aber das ist nicht wirklich ein Problem mit der Garbage Collection im Vergleich zu Smart Pointern.
In jedem Fall ist das Problem nicht einfach zu lösen, eines ist besser als das andere. Sie haben jeweils Vor- und Nachteile.
quelle
Es geht um Leistung . Das Aufheben der Speicherzuweisung erfordert viel Verwaltungsaufwand. Wenn die Aufhebung der Zuordnung im Hintergrund ausgeführt wird, erhöht sich die Leistung des Vordergrundprozesses. Leider kann die Speicherzuweisung nicht träge sein (die zugewiesenen Objekte werden im nächsten heiligen Moment verwendet), das Freigeben von Objekten jedoch.
Versuchen Sie in C ++ (ohne GC), eine große Menge von Objekten zuzuweisen, drucken Sie "Hallo" und löschen Sie sie dann. Sie werden überrascht sein, wie lange es dauert, Objekte freizugeben.
GNU libc bietet außerdem effektivere Tools zum Aufheben der Speicherzuweisung (siehe Hindernisse) . Muss beachten, ich habe keine Erfahrung mit Hindernissen, ich habe sie nie benutzt.
quelle
Die Speicherbereinigung kann effizienter sein - sie "stapelt" den Overhead der Speicherverwaltung und erledigt alles auf einmal. Im Allgemeinen führt dies dazu, dass insgesamt weniger CPU für die Aufhebung der Speicherzuweisung aufgewendet wird. Dies bedeutet jedoch, dass Sie irgendwann einen großen Stoß an Aktivitäten zur Aufhebung der Zuweisung haben. Wenn der GC nicht richtig ausgelegt ist, kann dies für den Benutzer als "Pause" sichtbar werden, während der GC versucht, den Speicher freizugeben. Die meisten modernen GCs sind sehr gut darin, dies für den Benutzer unsichtbar zu halten, außer unter den widrigsten Bedingungen.
Intelligente Zeiger (oder ein beliebiges Referenzzählschema) haben den Vorteil, dass sie genau dann auftreten, wenn Sie vom Anzeigen des Codes ausgehen (intelligente Zeiger verlieren den Gültigkeitsbereich, was gelöscht wird). Hier und da gibt es kleine Schübe von Aufhebungen. Sie können insgesamt mehr CPU-Zeit für die Aufhebung der Zuweisung verbrauchen, aber da dies auf alle Vorgänge in Ihrem Programm verteilt ist, ist es weniger wahrscheinlich, dass Ihr Benutzer die Aufhebung der Zuweisung von Monsterdatenstrukturen sieht.
Wenn Sie etwas tun, bei dem es auf Reaktionsfähigkeit ankommt, empfiehlt es sich, dass Sie durch intelligente Zeiger- / Referenzzählung genau wissen, wann etwas passiert, damit Sie beim Codieren wissen, was für Ihre Benutzer wahrscheinlich sichtbar wird. In einer GC-Umgebung haben Sie nur die kurzlebigste Kontrolle über den Garbage Collector und müssen einfach versuchen, die Sache zu umgehen.
Wenn der Gesamtdurchsatz Ihr Ziel ist, ist ein GC-basiertes System möglicherweise die bessere Wahl, da es die für die Speicherverwaltung erforderlichen Ressourcen minimiert.
Zyklen: Ich betrachte das Problem der Zyklen nicht als bedeutsam. In einem System mit intelligenten Zeigern tendieren Sie zu Datenstrukturen ohne Zyklen, oder Sie achten nur darauf, wie Sie solche Dinge loslassen. Bei Bedarf können Aufbewahrungsobjekte verwendet werden, die wissen, wie man die Zyklen in den eigenen Objekten unterbricht, um automatisch die ordnungsgemäße Zerstörung sicherzustellen. In einigen Bereichen der Programmierung mag dies wichtig sein, aber für die alltägliche Arbeit ist es irrelevant.
quelle
Die größte Einschränkung bei intelligenten Zeigern ist, dass sie nicht immer gegen Zirkelverweise helfen. Beispiel: Sie haben Objekt A, das einen intelligenten Zeiger auf Objekt B speichert, und Objekt B, das einen intelligenten Zeiger auf Objekt A speichert. Wenn sie zusammen gelassen werden, ohne einen der Zeiger zurückzusetzen, werden sie niemals freigegeben.
Dies liegt daran, dass ein intelligenter Zeiger eine bestimmte Aktion ausführen muss, die im obigen Szenario nicht ausgelöst wird, da beide Objekte für das Programm nicht erreichbar sind. Die Garbage Collection wird zurechtkommen - es wird korrekt erkannt, dass Objekte für das Programm nicht erreichbar sind, und sie werden gesammelt.
quelle
Es ist ein Spektrum .
Wenn Sie nicht an Ihre Leistung gebunden sind und bereit sind, die Weichen zu stellen, müssen Sie die richtigen Entscheidungen treffen und die Freiheit, dies zu tun , die ganze Freiheit, es zu vermasseln:
"Ich sage dir, was du tun sollst, du tust es. Vertrau mir."
Die Garbage Collection ist das andere Ende des Spektrums. Sie haben nur sehr wenig Kontrolle, aber für Sie ist gesorgt:
"Ich sage dir, was ich will, du machst es möglich".
Dies hat viele Vorteile, zumeist, dass Sie nicht so vertrauenswürdig sein müssen, wenn Sie genau wissen möchten, wann eine Ressource nicht mehr benötigt wird, aber (trotz einiger der hier schwebenden Antworten) nicht gut für die Leistung sind die Vorhersehbarkeit der Leistung. (Wie bei allen Dingen kann es zu schlechteren Ergebnissen kommen, wenn Sie die Kontrolle haben und etwas Dummes tun. Wenn Sie jedoch während der Kompilierung wissen, unter welchen Bedingungen Speicher freigegeben werden kann, kann dies nicht als Leistungsgewinn verwendet werden.) jenseits der Naivität).
RAII, Scoping, Ref Counting usw. sind alles Hilfsmittel, mit denen Sie sich weiter in diesem Spektrum bewegen können, aber es ist nicht der ganze Weg dorthin. All diese Dinge müssen noch aktiv genutzt werden. Sie erlauben und verlangen weiterhin, dass Sie mit der Speicherverwaltung auf eine Weise interagieren, wie dies bei der Garbage Collection nicht der Fall ist.
quelle
Bitte denken Sie daran, dass am Ende alles auf eine CPU hinausläuft, die Anweisungen ausführt. Meines Wissens verfügen alle Consumer-CPUs über Befehlssätze, bei denen Sie Daten an einem bestimmten Ort im Speicher ablegen müssen, und Sie haben Zeiger auf diese Daten. Das ist alles, was Sie auf der Grundstufe haben.
Alles darüber hinaus, mit Garbage Collection, Verweisen auf möglicherweise verschobene Daten, Heap-Komprimierung usw. usw., erledigt die Arbeit innerhalb der Einschränkungen, die durch das oben genannte Paradigma "Speicherblock mit Adresszeiger" vorgegeben sind. Das Gleiche gilt für intelligente Zeiger - Sie müssen den Code NOCH auf der tatsächlichen Hardware ausführen.
quelle