it-swarm.dev

Warum Garbage Collection, wenn intelligente Zeiger vorhanden sind?

In diesen Tagen werden so viele Sprachen Müll gesammelt. Es ist sogar für C++ von Dritten verfügbar. Aber C++ hat RAII und intelligente Zeiger. Was bringt es also, Garbage Collection zu verwenden? Tut es etwas extra?

Und in anderen Sprachen wie C #, wenn alle Referenzen nach Spezifikation und Implementierung als intelligente Zeiger behandelt werden (abgesehen von RAII), werden dann immer noch Garbage Collectors benötigt? Wenn nein, warum ist das dann nicht so?

69
Gulshan

Was bringt es also, Garbage Collection zu verwenden?

Ich gehe davon aus, dass Sie intelligente Zeiger mit Referenzzählung meinen, und ich werde feststellen, 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 intelligenten Zeigern mit Referenzzählung?". stattdessen.

  • Genauigkeit. Allein die Referenzzählung leckt Zyklen, so dass intelligente Zeiger mit Referenzzählung im Allgemeinen Speicher verlieren, 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 außerdem, dass bereichsbezogene Referenzzähl- und Ablaufverfolgungs-GCs Werte zu unterschiedlichen Zeiten erfassen, manchmal wird die Referenzzählung 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 Zusammenhang mit Multithread-Anwendungen, bei denen Referenzzählungen atomar erhöht werden. Es gibt fortschrittliche Referenzzählungstechniken, um dies zu mildern, aber die Verfolgung von GCs ist in Produktionsumgebungen immer noch der Algorithmus der Wahl.

  • Latenz. Typische Smart-Pointer-Implementierungen ermöglichen es Destruktoren, Lawinen zu erzeugen, was zu unbegrenzten Pausenzeiten führt. Andere Formen der Speicherbereinigung sind viel inkrementeller und können sogar in Echtzeit erfolgen, z. Baker's Laufband.

71
Jon Harrop

Da niemand es aus diesem Blickwinkel betrachtet hat, werde ich Ihre Frage umformulieren: warum etwas in die Sprache einfügen, wenn Sie es in einer Bibliothek tun können? Ignorieren bestimmter Implementierungs- und syntaktischer Details, GC/smart Zeiger 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?

Auf diese Frage gibt es einige Antworten. Das Wichtigste zuerst:

  1. Sie stellen sicher, dass der gesamte Code zur Interaktion verwendet werden kann. Dies ist meiner Meinung nach der große Grund, warum Code wiederverwendet und codiert wird Das Teilen hat erst mit Java/C #/Python/Ruby richtig begonnen. Bibliotheken müssen kommunizieren, und die einzige zuverlässige gemeinsame Sprache, die sie haben, ist das, was in der Sprachspezifikation selbst (und bis zu einem gewissen Grad in der Standardbibliothek) enthalten ist. 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 lib übergeben. Übergebe ich eine Referenz? Zeiger? scoped_ptr? smart_ptr? Übergebe ich das Eigentum oder nicht? Gibt es eine Möglichkeit, dies anzuzeigen? Was ist, wenn die Bibliothek zuweisen muss? Muss ich ihm einen Allokator geben? Da die Speicherverwaltung nicht Teil der Sprache ist, zwingt C++ jedes Bibliothekspaar dazu, hier seine eigene spezifische Strategie auszuhandeln, und es ist wirklich schwierig, sie alle dazu zu bringen, sich zu einigen. GC macht dies zu einem völligen Problem.

  2. 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ücken kann. Sie haben Zeiger, Referenzen, const, Dereferenzierungsoperatoren, Indirektionsoperatoren, Adress-of usw. Wenn Sie die Speicherverwaltung in die Sprache selbst rollen, kann die Syntax darauf ausgelegt werden. Alle diese Operatoren verschwinden und die Sprache wird sauberer und einfacher.

  3. Sie erzielen eine hohe Kapitalrendite. Der Wert, den ein bestimmter 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 eine 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 werden über Millionen von Benutzern amortisiert.

66
munificent

Garbage Collection bedeutet im Grunde nur, dass Ihre zugewiesenen Objekte irgendwann automatisch freigegeben werden, nachdem 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, sonst niemals freigegeben würden.

Intelligente Zeiger beziehen sich nur auf eine Struktur, die sich wie ein gewöhnlicher Zeiger verhält , aber einige zusätzliche Funktionen enthält. Diese include sind jedoch nicht auf die Freigabe beschränkt, sondern auch auf das Schreiben beim Schreiben, gebundene Prüfungen, ...

Wie Sie bereits gesagt haben, können intelligente Zeiger verwendet werden , um eine Form der Speicherbereinigung zu implementieren.

Der Gedankengang geht aber folgendermaßen:

  1. Müllabfuhr ist eine coole Sache, da sie praktisch ist und ich mich um weniger Dinge kümmern muss
  2. Deshalb: Ich möchte eine Speicherbereinigung in meiner Sprache
  3. Wie kann ich GC in meine Sprache bringen?

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 Referenzen außerhalb des Gültigkeitsbereichs liegen. Wie dies gemacht wird, ist Sache des Compilers.

In C++ war jedoch keine Speicherbereinigung vorgesehen. Wenn wir einen Zeiger int* p = new int; Zuweisen und dieser außerhalb des Gültigkeitsbereichs liegt, wird p selbst vom Stapel entfernt, aber niemand kümmert sich um den zugewiesenen Speicher.

Jetzt haben Sie von Anfang an nur deterministische Destruktoren . Wenn ein Objekt den Bereich verlässt, in dem es erstellt wurde, wird sein Destruktor aufgerufen. In Kombination mit Vorlagen und Operatorüberladung können Sie ein Wrapper-Objekt entwerfen, das sich wie ein Zeiger verhält, jedoch die Destruktorfunktionalität verwendet, um die damit verbundenen Ressourcen (RAII) zu bereinigen. Sie nennen dies einen intelligenten Zeiger .

Dies ist alles sehr C++ - spezifisch: Ü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, ist dies lediglich 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 einer .Dispose() für bestimmte Objekte bereitstellt). Nicht referenzierte Ressourcen werden schließlich vom GC zurückgefordert, aber es ist nicht definiert, wann genau dies geschehen wird.

Dies wiederum kann es dem GC ermöglichen, seine Arbeit effizienter zu erledigen. Der .NET GC ist tiefer in die Sprache integriert als intelligente Zeiger, die darauf gesetzt sind, und kann z. Verzögern Sie Speicheroperationen und führen Sie sie in Blöcken aus, um sie billiger zu machen, oder verschieben Sie den Speicher , um die Effizienz zu erhöhen, je nachdem, wie oft auf Objekte zugegriffen wird.

36
Dario

Meiner Meinung nach gibt es zwei große Unterschiede zwischen der Speicherbereinigung und intelligenten Zeigern, die für die Speicherverwaltung verwendet werden:

  1. Intelligente Zeiger können keinen zyklischen Müll sammeln. Müllabfuhr kann
  2. Intelligente Zeiger erledigen die gesamte Arbeit in den Momenten des Referenzierens, Dereferenzierens und Freigebens im Anwendungsthread. Müllabfuhr muss nicht

Ersteres bedeutet, dass GC Müll sammelt, den intelligente Zeiger nicht sammeln. Wenn Sie intelligente Zeiger verwenden, müssen Sie die Erstellung dieser Art von Müll vermeiden oder darauf vorbereitet sein, manuell damit umzugehen.

Letzteres bedeutet, dass unabhängig davon, wie intelligent intelligente Zeiger sind, deren Betrieb die Arbeitsthreads in Ihrem Programm verlangsamt. Die Speicherbereinigung kann die Arbeit verschieben und in andere Threads verschieben. Dadurch ist es insgesamt effizienter (in der Tat sind die Laufzeitkosten eines modernen GC geringer als bei einem normalen malloc/freien System, auch ohne den zusätzlichen Aufwand an intelligenten Zeigern) und erledigen die Arbeit, die noch erforderlich ist, ohne in das System einzusteigen Art und Weise der Anwendung Threads.

Beachten Sie nun, dass intelligente Zeiger als programmatische Konstrukte verwendet werden können, um alle möglichen anderen interessanten Dinge zu tun - siehe Darios Antwort -, die völlig außerhalb des Bereichs der Speicherbereinigung liegen. Wenn Sie diese ausführen möchten, benötigen Sie intelligente Zeiger.

Für die Speicherverwaltung sehe ich jedoch keine Aussicht darauf, dass intelligente Zeiger die Speicherbereinigung ersetzen. Sie sind einfach nicht so gut darin.

4
Tom Anderson

Der Begriff Garbage Collection impliziert, dass Müll gesammelt werden muss. In C++ gibt es intelligente Zeiger in mehreren Varianten, vor allem als unique_ptr. Das unique_ptr ist im Grunde ein Einzelbesitz- und Gültigkeitsbereichskonstrukt. In einem gut gestalteten Code befinden sich die meisten Heap-zugewiesenen Inhalte normalerweise hinter den intelligenten Zeigern unique_ptr, und der Besitz dieser Ressourcen ist jederzeit genau definiert. Unique_ptr verursacht kaum Overhead, und unique_ptr beseitigt die meisten manuellen Speicherverwaltungsprobleme, die traditionell dazu geführt haben, dass Benutzer verwaltete Sprachen verwenden. Jetzt, da mehr Kerne gleichzeitig ausgeführt werden, werden die Entwurfsprinzipien, die den Code dazu veranlassen, zu jedem Zeitpunkt eindeutige und genau definierte Eigentumsrechte zu verwenden, für die Leistung wichtiger. Die Verwendung des Akteur-Berechnungsmodells ermöglicht die Erstellung von Programmen mit einem Mindestmaß an gemeinsam genutztem Status zwischen Threads. Die eindeutige Eigentümerschaft spielt eine wichtige Rolle, damit Hochleistungssysteme viele Kerne effizient nutzen können, ohne dass der Aufwand für die gemeinsame Nutzung von Threads besteht. Thread-Daten und die implizierten Mutex-Anforderungen.

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 erforderlich sind, müssen Threads kommunizieren. RAII in C++ funktioniert ziemlich gut für lebenslange Probleme in einem Single-Thread-Setup. In einem Multi-Thread-Setup ist die Lebensdauer von Objekten möglicherweise nicht vollständig hierarchisch gestapelt. In diesen Situationen bietet die Verwendung von shared_ptr einen großen Teil der Lösung. Sie erstellen einen gemeinsamen Besitz einer Ressource, und dies ist in C++ der einzige Ort, an dem Müll angezeigt wird, jedoch in so geringen Mengen, dass ein ordnungsgemäß entworfenes C++ - Programm eher als Implementierung einer 'Wurf'-Sammlung mit gemeinsam genutzten ptrs als als vollwertige Garbage Collection betrachtet werden sollte in anderen Sprachen implementiert. C++ hat einfach nicht so viel 'Müll' zu sammeln.

Wie von anderen angegeben, sind intelligente Zeiger mit Referenzzählung eine Form der Speicherbereinigung, und für diese gibt es ein Hauptproblem. Das Beispiel, das hauptsächlich als Nachteil von Formen der Speicherbereinigung mit Referenzzählung verwendet wird, ist das Problem bei der Erstellung verwaister Datenstrukturen, die mit intelligenten Zeigern miteinander verbunden sind und Objektcluster erstellen, die verhindern, dass sie sich gegenseitig erfassen. 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 breiten Ansatz gemeinsamer Daten für die Multithread-Programmierung verwenden, wie er überwiegend in einem großen Teil verwendet wird In der Branche können diese verwaisten Cluster schnell Realität werden.

Zusammenfassend lässt sich sagen, dass Sie mit der Verwendung gemeinsamer Zeiger die weit verbreitete Verwendung von unique_ptr in Kombination mit dem Akteurmodell des Berechnungsansatzes für die Multithread-Programmierung und die eingeschränkte Verwendung von shared_ptr meinen, als andere Formen der Speicherbereinigung Sie nicht kaufen zusätzliche Vorteile. Wenn Sie jedoch bei einem Shared-Everything-Ansatz überall mit shared_ptr enden würden, sollten Sie in Betracht ziehen, entweder Parallelitätsmodelle zu wechseln oder zu einer verwalteten Sprache zu wechseln, die mehr auf eine umfassendere Aufteilung der Eigentumsverhältnisse und den gleichzeitigen Zugriff auf Datenstrukturen ausgerichtet ist.

4
user1703394

Die meisten intelligenten Zeiger werden mithilfe der Referenzzählung implementiert. Das heißt, jeder intelligente Zeiger, der auf ein Objekt verweist, erhöht die Objektreferenzanzahl. Wenn dieser Zähler auf Null geht, wird das Objekt freigegeben.

Das Problem besteht darin, dass 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 & C verknüpften Speicher manuell freigeben Holen Sie sich dort eine "Pause" 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 Referenzen auf dem Stapel und diejenigen, auf die global zugegriffen werden kann, betrachtet und dann jedes Objekt verfolgt, auf das sich diese Referenzen beziehen, und Objekte sie beziehen sich auf usw. Alles andere ist Müll .

Auf diese Weise spielen Zirkelverweise keine Rolle mehr - solange weder A, B noch C erreichbar sind, kann der Speicher zurückgefordert werden.

Die "echte" Speicherbereinigung bietet weitere Vorteile. Zum Beispiel ist die Speicherzuweisung extrem billig: Erhöhen Sie einfach den Zeiger auf das "Ende" des Speicherblocks. Die Freigabe hat ebenfalls konstante fortgeführte Anschaffungskosten. Aber natürlich können Sie in Sprachen wie C++ die Speicherverwaltung so gut wie beliebig implementieren, sodass Sie eine Zuordnungsstrategie entwickeln können, die noch schneller ist.

Natürlich ist in C++ die Menge des Heap-zugewiesenen Speichers normalerweise geringer als in einer referenzintensiven Sprache wie C # /. NET. Aber das ist nicht wirklich ein Problem der Speicherbereinigung im Vergleich zu intelligenten Zeigern.

In jedem Fall ist das Problem nicht einfach, das eine ist besser als das andere. Sie haben jeweils Vor- und Nachteile.

2
Dean Harding

Es geht um Leistung . Das Aufheben der Zuweisung von Speicher erfordert viel Administration. Wenn die Nichtzuweisung im Hintergrund ausgeführt wird, erhöht sich die Leistung des Vordergrundprozesses. Leider kann die Speicherzuweisung nicht faul sein (die zugewiesenen Objekte werden im heiligen nächsten Moment verwendet), das Freigeben von Objekten jedoch.

Versuchen Sie in C++ (ohne GC), eine große Anzahl von Objekten zuzuweisen, drucken Sie "Hallo" und löschen Sie sie dann. Sie werden überrascht sein, wie lange es dauert, Objekte freizugeben.

Außerdem bietet GNU libc effektivere Tools zum Aufheben der Zuweisung von Speicher, siehe Hindernisse . Ich muss beachten, dass ich keine Erfahrung mit Hindernissen habe, ich habe sie nie verwendet.

2
ern0

Die Speicherbereinigung kann effizienter sein - sie erhöht im Wesentlichen 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 Ausbruch von Aufhebungsaktivitäten haben. Wenn der GC nicht richtig ausgelegt ist, kann dies für den Benutzer als "Pause" sichtbar werden, während der GC versucht, die Speicherzuordnung aufzuheben. 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 es vom Betrachten des Codes erwarten (intelligenter Zeiger verlässt den Gültigkeitsbereich, etwas wird gelöscht). Hier und da kommt es zu kleinen Aufhebungen. Insgesamt benötigen Sie möglicherweise mehr CPU-Zeit für die Aufhebung der Zuweisung. Da diese jedoch auf alle in Ihrem Programm vorkommenden Ereignisse verteilt ist, ist es weniger wahrscheinlich, dass sie für Ihren Benutzer sichtbar wird (wenn die Zuweisung einer bestimmten Monsterdatenstruktur aufgehoben wird).

Wenn Sie etwas tun, bei dem es auf Reaktionsfähigkeit ankommt, würde ich empfehlen, dass Sie durch intelligente Zeiger/Ref-Zählung genau wissen, wann etwas passiert, damit Sie beim Codieren wissen, was für Ihre Benutzer wahrscheinlich sichtbar wird. In einer GC-Einstellung haben Sie nur die kurzlebigste Kontrolle über den Garbage Collector und müssen einfach versuchen, das Problem zu umgehen.

Wenn andererseits der Gesamtdurchsatz Ihr Ziel ist, ist ein GC-basiertes System möglicherweise eine viel bessere Wahl, da es die für die Speicherverwaltung erforderlichen Ressourcen minimiert.

Zyklen: Ich halte das Problem der Zyklen nicht für signifikant. In einem System mit intelligenten Zeigern tendieren Sie zu Datenstrukturen ohne Zyklen, oder Sie achten einfach darauf, wie Sie solche Dinge loslassen. Bei Bedarf können Keeper-Objekte verwendet werden, die wissen, wie die Zyklen in den eigenen Objekten unterbrochen werden, um automatisch eine ordnungsgemäße Zerstörung sicherzustellen. In einigen Bereichen der Programmierung mag dies wichtig sein, aber für die meisten täglichen Arbeiten ist dies irrelevant.

2
Michael Kohne

Die größte Einschränkung bei intelligenten Zeigern besteht darin, dass sie nicht immer gegen Zirkelverweise helfen. Zum Beispiel haben Sie Objekt A, das einen intelligenten Zeiger auf Objekt B speichert, und Objekt B speichert einen intelligenten Zeiger auf Objekt A. Wenn sie zusammen bleiben, 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 ausgeführt wird, da beide Objekte für das Programm nicht erreichbar sind. Die Speicherbereinigung wird bewältigt - sie erkennt ordnungsgemäß, dass Objekte für das Programm nicht erreichbar sind, und sie werden erfasst.

1
sharptooth

Es ist ein Spektrum.

Wenn Sie keine engen Leistungsgrenzen haben und bereit sind, den Grind in Angriff zu nehmen, landen Sie bei Assembly oder c, mit der ganzen Verantwortung, die richtigen Entscheidungen zu treffen, und der ganzen Freiheit, dies zu tun, aber damit , all die Freiheit, es durcheinander zu bringen:

"Ich werde dir sagen, was zu tun ist, du tust es. Vertrau mir".

Die Speicherbereinigung ist das andere Ende des Spektrums. Sie haben sehr wenig Kontrolle, aber es ist für Sie erledigt:

"Ich werde dir sagen, was ich will, du machst es möglich".

Dies hat viele Vorteile, vor allem, dass Sie nicht so vertrauenswürdig sein müssen, um genau zu wissen, wann eine Ressource nicht mehr benötigt wird, aber (trotz einiger Antworten, die hier herumschwirren) nicht gut für die Leistung ist die Vorhersehbarkeit der Leistung. (Wie alle Dinge können Sie schlechtere Ergebnisse erzielen, wenn Sie die Kontrolle erhalten und etwas Dummes tun. Wenn Sie jedoch darauf hinweisen, dass Sie wissen, unter welchen Bedingungen Sie Speicher freigeben können, können Sie dies nicht als Leistungsgewinn nutzen jenseits von naiv).

RAII, Scoping, Ref Counting usw. sind alles Hilfsmittel, um Sie weiter entlang dieses Spektrums zu bewegen, aber es ist nicht ganz dort. All diese Dinge müssen noch aktiv genutzt werden. Sie lassen und verlangen weiterhin, dass Sie mit der Speicherverwaltung auf eine Weise interagieren, die die Garbage Collection nicht tut.

1
drjpizzle

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 Daten an einem bestimmten Ort im Speicher gespeichert werden müssen, und Sie haben Zeiger auf diese Daten. Das ist alles, was Sie auf der Grundstufe haben.

Alles, was darüber hinaus mit Garbage Collection, Verweisen auf möglicherweise verschobene Daten, Heap-Komprimierung usw. usw. geschieht, erledigt die Arbeit innerhalb der Einschränkungen, die durch das obige 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.

0
user1249