RAII gegen Garbage Collector


75

Ich habe kürzlich einen großartigen Vortrag von Herb Sutter über "Leak Free C ++ ..." auf der CppCon 2016 gesehen, in dem er über die Verwendung intelligenter Zeiger zur Implementierung von RAII (Ressourcenerfassung ist Initialisierung) - Konzepte und deren Lösung der meisten Probleme mit Speicherlecks sprach.

Jetzt habe ich mich gefragt. Wenn ich mich strikt an die RAII-Regeln halte, was eine gute Sache zu sein scheint, warum sollte sich das von einem Garbage Collector in C ++ unterscheiden? Ich weiß, dass der Programmierer mit RAII die volle Kontrolle darüber hat, wann die Ressourcen wieder freigegeben werden. Aber ist das auf jeden Fall vorteilhaft, wenn man nur einen Garbage Collector hat? Wäre es wirklich weniger effizient? Ich habe sogar gehört, dass ein Garbage Collector effizienter sein kann, da er größere Speicherblöcke gleichzeitig freigeben kann, anstatt kleine Speicherelemente im gesamten Code freizugeben.


24
Das deterministische Ressourcenmanagement ist in allen möglichen Fällen von entscheidender Bedeutung, insbesondere wenn Sie mit nicht verwalteten Ressourcen (z. B. Dateihandles, Datenbanken usw.) arbeiten. Außerdem hat die Speicherbereinigung immer einen gewissen Overhead, während RAII nicht mehr Overhead hat, als den Code überhaupt richtig zu schreiben. Das "Freigeben kleiner Speicherelemente im gesamten Code" ist im Allgemeinen wesentlich effizienter, da es die Ausführung der Anwendung weniger stört.
Cody Gray

32
Hinweis: Sie sprechen über Ressourcen , aber es gibt mehr als eine Art von Ressource. Ein Garbage Collector wird aufgerufen, wenn Speicherplatz freigegeben werden soll. Er wird jedoch nicht aufgerufen, wenn Dateien geschlossen werden müssen.
Solomon Slow

20
Alles ist besser als Müllabfuhr
GuidoG

11
@Veedrac Wenn Sie sich voll und ganz für RAII engagieren und überall intelligente Zeiger verwenden, sollten Sie auch keine Fehler nach der Verwendung haben. Aber selbst wenn GC (oder neu gezählte Smart Pointer) Sie vor Fehlern nach der Verwendung bewahrt hätte, könnte dies ein Szenario maskieren, in dem Sie unabsichtlich länger als erwartet auf Ressourcen verwiesen haben.
Jamesdlin

7
@Veedrac: Von Natürlich ist es unfair. Sie haben mir zwei Programme zum Vergleichen gegeben, eines, das Speicher frei macht, eines, das dies nicht tut. Um einen fairen Vergleich anstellen zu können, müssen Sie eine realistische Arbeitsbelastung ausführen, bei der der GC tatsächlich aktiv werden muss. Nicht im Leerlauf sitzen. Und Sie müssen ein dynamisches und realistisches Speicherzuweisungsmuster haben, nicht FIFO oder LIFO oder eine Variation davon. Es ist nicht gerade umwerfend zu behaupten, dass ein Programm, das niemals Speicher freigibt, schneller ist als eines, das dies tut, oder dass ein auf LIFO-Freigaben zugeschnittener Heap schneller ist als einer, der nicht freigegeben wird. Nun, duh, natürlich wäre es das.
user541686

Antworten:


62

Wenn ich mich strikt an die RAII-Regeln halte, was eine gute Sache zu sein scheint, warum sollte sich das von einem Garbage Collector in C ++ unterscheiden?

Während beide sich mit Zuweisungen befassen, tun sie dies auf völlig unterschiedliche Weise. Wenn Sie auf einen GC wie den in Java verweisen, der seinen eigenen Overhead hinzufügt, einen Teil des Determinismus aus dem Ressourcenfreigabeprozess entfernt und Zirkelverweise verarbeitet.

Sie können GC jedoch für bestimmte Fälle mit sehr unterschiedlichen Leistungsmerkmalen implementieren. Ich habe einmal eine zum Schließen von Socket-Verbindungen in einem Hochleistungs- / Hochdurchsatz-Server implementiert (das Aufrufen der Socket-Schließ-API hat zu lange gedauert und die Durchsatzleistung beeinträchtigt). Dies beinhaltete keinen Speicher, sondern Netzwerkverbindungen und keine zyklische Abhängigkeitsbehandlung.

Ich weiß, dass der Programmierer mit RAII die volle Kontrolle darüber hat, wann die Ressourcen wieder freigegeben werden. Aber ist das auf jeden Fall vorteilhaft, wenn man nur einen Garbage Collector hat?

Dieser Determinismus ist eine Funktion, die GC einfach nicht zulässt. Manchmal möchten Sie wissen können, dass nach einem bestimmten Zeitpunkt ein Bereinigungsvorgang ausgeführt wurde (Löschen einer temporären Datei, Schließen einer Netzwerkverbindung usw.).

In solchen Fällen schneidet GC es nicht, weshalb Sie in C # (zum Beispiel) die IDisposableSchnittstelle haben.

Ich habe sogar gehört, dass ein Garbage Collector effizienter sein kann, da er größere Speicherblöcke gleichzeitig freigeben kann, anstatt kleine Speicherelemente im gesamten Code freizugeben.

Kann sein ... hängt von der Implementierung ab.


6
Beachten Sie, dass es auch Algorithmen gibt, die auf einem GC basieren und nicht mit RAII implementiert werden können. Zum Beispiel einige gleichzeitige sperrfreie Algorithmen, bei denen mehrere Threads um die Veröffentlichung einiger Daten rennen. Zum Beispiel gibt es nach meinem besten Wissen keine C ++ - Implementierung von Cliffs nicht blockierender Hashmap .
Voo

3
fügt seinen eigenen Overhead hinzu - otoh Sie zahlen keine Kosten für malloc und kostenlos. Sie handeln im Grunde genommen mit kostenlosem Listenmanagement und Referenzzählung für das Scannen von Lebendigkeit.
The8472

5
Bei GC in Java und .NET geht es nur darum, den Speicher freizugeben, der noch von nicht erreichbaren Objekten zugewiesen wird. Dies ist nicht vollständig deterministisch , jedoch Ressourcen wie Datei - Handles und Netzwerkverbindungen durch einen völlig anderen Mechanismus (in Java, die geschlossen sind java.io.CloseableSchnittstelle und „Try-mit-Ressourcen“ Blöcke), die ist vollständig deterministisch . Der Teil der Antwort über den Determinismus einer "Bereinigungsoperation" ist also falsch.
Rogério

3
@Voo in diesem Fall könnte man argumentieren, dass es nicht wirklich sperrenfrei ist, weil der Garbage Collector das Sperren für Sie übernimmt.
user253751

2
@Voo Hat Ihr Algorithmus verlassen auf dem Scheduler - Thread eine Sperre mit?
user253751

38

Die Speicherbereinigung löst bestimmte Klassen von Ressourcenproblemen, die RAII nicht lösen kann. Grundsätzlich läuft es auf zirkuläre Abhängigkeiten hinaus, bei denen Sie den Zyklus nicht vorher identifizieren.

Dies gibt ihm zwei Vorteile. Erstens wird es bestimmte Arten von Problemen geben, die RAII nicht lösen kann. Diese sind meiner Erfahrung nach selten.

Das größere Problem ist, dass der Programmierer dadurch faul ist und sich nicht um die Lebensdauer der Speicherressourcen und bestimmte andere Ressourcen kümmert , bei denen es Ihnen nichts ausmacht, die Bereinigung zu verzögern. Wenn Sie sich nicht um bestimmte Arten von Problemen kümmern müssen , können Sie sich mehr um andere Probleme kümmern . Auf diese Weise können Sie sich auf die Teile Ihres Problems konzentrieren, auf die Sie sich konzentrieren möchten.

Der Nachteil ist, dass es ohne RAII schwierig ist, Ressourcen zu verwalten, deren Lebensdauer Sie einschränken möchten. GC-Sprachen reduzieren Sie im Grunde genommen auf extrem einfache, bereichsgebundene Lebensdauern oder erfordern eine manuelle Ressourcenverwaltung wie in C, wobei manuell angegeben wird, dass Sie mit einer Ressource fertig sind. Ihr Objektlebensdauersystem ist stark an GC gebunden und eignet sich nicht für ein straffes Lebensdauermanagement großer komplexer (und dennoch zyklusfreier) Systeme.

Um fair zu sein, erfordert das Ressourcenmanagement in C ++ viel Arbeit, um es in solch großen komplexen (und dennoch zyklusfreien) Systemen richtig zu machen. C # und ähnliche Sprachen machen es nur ein bisschen schwieriger, im Gegenzug machen sie den einfachen Fall einfach.

Die meisten GC-Implementierungen erzwingen auch vollwertige Klassen außerhalb der Lokalität. Das Erstellen zusammenhängender Puffer allgemeiner Objekte oder das Zusammensetzen allgemeiner Objekte zu einem größeren Objekt ist für die meisten GC-Implementierungen nicht einfach. Auf der anderen Seite können Sie mit C # Werttypen structmit etwas eingeschränkten Funktionen erstellen . In der gegenwärtigen Ära der CPU-Architektur ist die Cache-Freundlichkeit der Schlüssel, und das Fehlen lokaler GC-Kräfte ist eine schwere Belastung. Da diese Sprachen größtenteils eine Bytecode-Laufzeit haben, könnte die JIT-Umgebung theoretisch häufig verwendete Daten zusammen verschieben, aber meistens kommt es aufgrund häufiger Cache-Fehler im Vergleich zu C ++ nur zu einem einheitlichen Leistungsverlust.

Das letzte Problem bei GC ist, dass die Freigabe unbestimmt ist und manchmal Leistungsprobleme verursachen kann. Moderne GCs machen dies weniger problematisch als in der Vergangenheit.


2
Ich bin mir nicht sicher, ob ich Ihr Argument über die Lokalität verstehe. Die meisten modernen GCs in ausgereiften Umgebungen (Java, .Net) führen eine Komprimierung durch und erstellen neue Objekte aus einem kontinuierlichen Speicherblock, der jedem Thread zugewiesen ist. Daher erwarte ich, dass Objekte, die ungefähr zur gleichen Zeit erstellt werden, relativ lokal sind. AFAIK gibt es in Standardimplementierungen nichts Vergleichbares malloc. Eine solche Logik kann zu einer falschen Freigabe führen, was ein Problem für die Multithread-Umgebung darstellt, aber es ist eine andere Geschichte. In C können Sie explizite Tricks ausführen, um die Lokalität zu verbessern, aber wenn Sie dies nicht tun, erwarte ich, dass GC besser ist. Was vermisse ich?
SergGr

8
@SergGr Ich kann in C ++ ein zusammenhängendes Array von nicht einfachen alten Datenobjekten erstellen und diese der Reihe nach iterieren. Ich kann sie explizit verschieben, damit sie nebeneinander liegen. Wenn ich über einen zusammenhängenden Wertecontainer iteriere, werden diese garantiert nacheinander im Speicher gespeichert. Knotenbasiscontainern fehlt diese Garantie, und gc-Sprachen unterstützen einheitlich nur knotenbasierte Container (bestenfalls haben Sie einen zusammenhängenden Referenzpuffer, nicht von Objekten). Mit einigen Arbeiten in C ++ kann ich dies sogar mit polymorphen Laufzeitwerten (virtuelle Methoden usw.) tun.
Yakk - Adam Nevraumont

2
Yakk, es sieht so aus, als würden Sie sagen, dass die Nicht-GC-Welt es Ihnen ermöglicht , für die Lokalität zu kämpfen und bessere Ergebnisse zu erzielen als die GC-Welt. Dies ist jedoch nur die Hälfte der Geschichte, da Sie standardmäßig wahrscheinlich schlechtere Ergebnisse erzielen als in der GC-Welt. Es ist tatsächlich so, mallocdass die Nichtlokalität erzwungen wird, gegen die Sie kämpfen müssen, und nicht die GC. Daher denke ich, dass die Behauptung in Ihrer Antwort, dass "die meisten GC-Implementierungen auch die Nichtlokalität erzwingen ", nicht wirklich wahr ist.
SergGr

2
@ Rogério Ja, das nenne ich Restricted Scope Based oder C-Style Object Lifetime Management. Wo Sie manuell definieren, wann Ihre Objektlebensdauer endet, oder dies mit einem einfachen Bereichsfall.
Yakk - Adam Nevraumont

4
Tut mir leid, aber nein, ein Programmierer kann nicht "faul" sein und sich nicht um die Lebensdauer der Speicherressourcen kümmern. Wenn Sie eine haben FooWidgetManager, die Ihre FooObjekte verwaltet , werden registrierte Objekte höchstwahrscheinlich Fooin einer unbegrenzt wachsenden Datenstruktur gespeichert . Ein solches "registriertes Foo" Objekt liegt außerhalb der Reichweite Ihres GC, da FooWidgetManagerdie interne Liste oder was auch immer einen Verweis darauf enthält . Um diesen Speicher freizugeben, müssen Sie darum bitten FooWidgetManager, die Registrierung dieses Objekts aufzuheben. Wenn Sie vergessen, ist dies im Wesentlichen "neu ohne Löschen"; Nur die Namen haben sich geändert ... und der GC kann das Problem nicht beheben .
H Walters

14

Beachten Sie, dass RAII eine Programmiersprache ist, während GC eine Speicherverwaltungstechnik ist. Also vergleichen wir Äpfel mit Orangen.

Aber wir können RAH auf seine Speicherverwaltung Aspekte beschränken nur und vergleichen Sie das mit GC - Techniken.

Der wesentliche Unterschied zwischen sogenannten RAII basierten Speicher - Management - Techniken (was eigentlich bedeutet Referenzzählung , zumindest wenn Sie Speicherressourcen betrachten und ignorieren die andere , die wie Dateien) und Original- Garbage - Collection - Techniken ist die Handhabung von Kreisreferenzen (für zyklische Graphen ) .

Bei der Referenzzählung müssen Sie speziell für sie codieren (unter Verwendung schwacher Referenzen oder anderer Dinge).

In vielen nützlichen Fällen (denken Sie daran std::vector<std::map<std::string,int>>) ist die Referenzzählung implizit (da sie nur 0 oder 1 sein kann) und wird praktisch weggelassen, aber die Konstruktor- und Destruktorfunktionen (für RAII wesentlich) verhalten sich so, als ob es ein Referenzzählbit gäbe (welches fehlt praktisch). Darin std::shared_ptrbefindet sich ein echter Referenzzähler. Der Speicher wird jedoch immer noch implizit manuell verwaltet (mit newund deleteinnerhalb von Konstruktoren und Destruktoren ausgelöst), aber dieses "implizite" delete(in Destruktoren) gibt die Illusion einer automatischen Speicherverwaltung. Anrufe an newund deletepassieren jedoch immer noch (und sie kosten Zeit).

Übrigens kann (und tut) die GC- Implementierung die Zirkularität auf besondere Weise behandeln, aber Sie überlassen diese Belastung dem GC (z. B. lesen Sie mehr über den Cheney-Algorithmus ).

Einige GC-Algorithmen (insbesondere der Garbage Collector zum Kopieren von Generationen) machen sich nicht die Mühe, Speicher für einzelne Objekte freizugeben, sondern werden massenweise nach dem Kopieren freigegeben . In der Praxis kann der Ocaml GC (oder der SBCL GC) schneller sein als ein echter C ++ RAII-Programmierstil (für einige , nicht alle Arten von Algorithmen).

Einige GC bieten Finalisierung (meistens zum Verwalten von externen Ressourcen außerhalb des Speichers wie Dateien), aber Sie werden sie selten verwenden (da die meisten Werte nur Speicherressourcen verbrauchen). Der Nachteil ist, dass die Finalisierung keine zeitliche Garantie bietet. In der Praxis verwendet ein Programm, das die Finalisierung verwendet, diese als letzten Ausweg (z. B. sollte das Schließen von Dateien immer noch mehr oder weniger explizit außerhalb der Finalisierung und auch mit diesen erfolgen).

Mit GC (und auch mit RAII, zumindest bei unsachgemäßer Verwendung) können immer noch Speicherverluste auftreten, z. B. wenn ein Wert in einer Variablen oder einem Feld gespeichert wird, aber in Zukunft nie mehr verwendet wird. Sie kommen nur seltener vor.

Ich empfehle, das Handbuch zur Speicherbereinigung zu lesen .

In Ihrem C ++ - Code können Sie Böhms GC oder Ravenbrooks MPS verwenden oder Ihren eigenen Tracing-Garbage-Collector codieren . Natürlich ist die Verwendung eines GC ein Kompromiss (es gibt einige Unannehmlichkeiten, z. B. Nichtdeterminismus, fehlende Zeitgarantien usw.).

Ich denke nicht, dass RAII in allen Fällen die ultimative Art ist, mit dem Gedächtnis umzugehen. In mehreren Fällen kann das Codieren Ihres Programms in einer wirklich und effizienten GC-Implementierung (denken Sie an Ocaml oder SBCL) einfacher (zu entwickeln) und schneller (auszuführen) sein als das Codieren mit einem ausgefallenen RAII-Stil in C ++ 17. In anderen Fällen ist es nicht. YMMV.

Wenn Sie beispielsweise einen Scheme-Interpreter in C ++ 17 mit dem ausgefallensten RAII-Stil codieren, müssen Sie dennoch einen expliziten GC darin codieren (oder verwenden) (da ein Scheme-Heap Zirkularitäten aufweist). Und die meisten Proof-Assistenten sind aus guten Gründen in GC-ed-Sprachen codiert, häufig in funktionalen Sprachen (die einzige, die ich kenne und die in C ++ codiert ist, ist Lean ).

Übrigens bin ich daran interessiert, eine solche C ++ 17-Implementierung von Scheme zu finden (aber weniger daran interessiert, sie selbst zu codieren), vorzugsweise mit einigen Multithreading-Fähigkeiten.


15
RAII bedeutet nicht Referenzzählung, das ist nur std :: shared_ptr. In C ++ fügt der Compiler Aufrufe an den Destruktor ein, wenn er den Beweis hat, dass Variablen nicht mehr erreichbar sind, d. H. wenn die Variable den Gültigkeitsbereich verlässt.
Csiz

6
@ BasileStarynkevitch Die meisten RAII beziehen sich nicht auf die Zählung, da die Zählung immer nur 1 sein würde
Caleth

3
RAII ist absolut keine Referenzzählung.
Jack Aidley

1
@csiz, @JackAidley, ich denke, Sie haben Basiles Standpunkt falsch verstanden. Was er sagt, ist, dass jede referenzzählähnliche Implementierung (selbst eine so einfache, shared_ptrdie keinen expliziten Zähler hat) Probleme mit der Behandlung von Szenarien haben wird, die Zirkelreferenzen beinhalten. Wenn Sie nur über einfache Fälle sprechen, in denen eine Ressource nur innerhalb einer einzelnen Methode verwendet wird, benötigen Sie nicht einmal, shared_ptraber dies ist nur ein sehr begrenzter Unterraum, für den die GC-basierte Welt ähnliche Mittel wie C # usingoder Java verwendet try-with-resources. Die reale Welt hat aber auch kompliziertere Szenarien.
SergGr

2
@SergGr: Wer hat jemals gesagt, dass Zirkelverweise unique_ptrbehandelt werden? Diese Antwort behauptet ausdrücklich, dass "sogenannte RAII-Techniken" "wirklich Referenzzählung bedeuten". Wir können (und ich auch) diese Behauptung ablehnen - und daher einen Großteil dieser Antwort (sowohl in Bezug auf die Richtigkeit als auch in Bezug auf die Relevanz) bestreiten -, ohne notwendigerweise jede einzelne Behauptung in dieser Antwort abzulehnen. (Übrigens gibt es reale Müllsammler, die auch keine
Zirkelverweise verarbeiten

13

RAII und GC lösen Probleme in völlig unterschiedliche Richtungen. Sie sind völlig anders, trotz dessen, was manche sagen würden.

Beide befassen sich mit dem Problem, dass die Verwaltung von Ressourcen schwierig ist. Garbage Collection löst das Problem, indem es so gestaltet wird, dass der Entwickler der Verwaltung dieser Ressourcen nicht so viel Aufmerksamkeit schenken muss. RAII löst dieses Problem, indem es Entwicklern erleichtert, auf ihr Ressourcenmanagement zu achten. Jeder, der sagt, dass er dasselbe tut, hat Ihnen etwas zu verkaufen.

Wenn Sie sich die jüngsten Trends bei Sprachen ansehen, sehen Sie, dass beide Ansätze in derselben Sprache verwendet werden, da Sie ehrlich gesagt wirklich beide Seiten des Puzzles benötigen. Sie sehen viele Sprachen, die eine Art Speicherbereinigung verwenden, sodass Sie nicht auf die meisten Objekte achten müssen, und diese Sprachen bieten auch RAII-Lösungen (wie den Python- withOperator) für die Zeiten, auf die Sie wirklich achten möchten Sie.

  • C ++ bietet RAII durch Konstruktoren / Destruktoren und GC durch shared_ptr(Wenn ich das Argument vorbringen darf, dass Refcounting und GC zur selben Klasse von Lösungen gehören, da beide so konzipiert sind, dass Sie nicht auf die Lebensdauer achten müssen).
  • Python bietet RAII Through withund GC Through ein Refcounting-System sowie einen Garbage Collector
  • C # bietet RAII durch IDisposableund usingund GC durch einen Garbage Collector der Generation

Die Muster tauchen in jeder Sprache auf.


10

Eines der Probleme bei Garbage Collectors ist, dass es schwierig ist, die Programmleistung vorherzusagen.

Mit RAII wissen Sie, dass die Ressourcen in genau der Zeit außerhalb des Bereichs liegen. Sie werden Speicher löschen und es wird einige Zeit dauern. Wenn Sie jedoch kein Meister der Garbage Collector-Einstellungen sind, können Sie nicht vorhersagen, wann die Bereinigung stattfinden wird.

Beispiel: Das Reinigen einer Reihe kleiner Objekte kann mit GC effektiver durchgeführt werden, da dadurch große Teile freigesetzt werden können, der Betrieb jedoch nicht schnell ist. Es ist schwer vorherzusagen, wann er eintreten wird, und aufgrund der "Bereinigung großer Teile" Nehmen Sie sich etwas Zeit für den Prozessor und können Sie die Programmleistung beeinträchtigen.


3
Ich bin nicht sicher, ob es möglich ist, die Programmleistung selbst mit den stärksten RAII-Ansätzen vorherzusagen. Herb Sutter gab einige interessante Videos darüber, wie wichtig der CPU-Cache ist und macht die Leistung überraschend unvorhersehbar.
Basile Starynkevitch

9
@BasileStarynkevitch GC-Stalls sind um Größenordnungen größer als Cache-Fehlschläge.
Dan spielt am Feuer

2
Es gibt keine "Bereinigung großer Teile". Tatsächlich ist GC eine Fehlbezeichnung, da die meisten Implementierungen "Non-Garbage Collectors" sind. Sie bestimmen die Überlebenden, verschieben sie an einen anderen Ort, aktualisieren die Zeiger und es bleibt freier Speicherplatz. Es funktioniert am besten, wenn die meisten Objekte vor dem Einsetzen des GC sterben. Normalerweise ist es ziemlich effizient, aber lange Pausen zu vermeiden ist schwierig.
Maaartinus

1
Beachten Sie, dass gleichzeitig Garbage Collectors in Echtzeit vorhanden sind, sodass eine vorhersehbare Leistung erzielt werden kann. Normalerweise ist der "Standard" -GC für eine bestimmte Sprache jedoch auf Effizienz gegenüber Konsistenz ausgelegt.
8bittree

5
Referenzgezählte Objektgraphen können auch sehr lange Freigabezeiten haben, wenn der letzte RC, der den Graphen am Leben hält, Null erreicht und alle Dekonstruktoren ausgeführt werden.
The8472

9

Grob gesagt. Die RAII-Sprache ist möglicherweise besser für die Latenz und den Jitter . Ein Garbage Collector ist möglicherweise besser für den Systemdurchsatz .


Warum sollte RAII im Vergleich zu GC unter Durchsatz leiden?
LyingOnTheSky

5

"Effizient" ist ein sehr weit gefasster Begriff. Im Sinne der Entwicklungsbemühungen ist RAII normalerweise weniger effizient als GC, aber in Bezug auf die Leistung ist GC normalerweise weniger effizient als RAII. Es ist jedoch möglich, für beide Fälle Gegenbeispiele anzugeben. Der Umgang mit generischem GC, wenn Sie sehr klare Muster für die Zuweisung von Ressourcen (de) in verwalteten Sprachen haben, kann ziemlich mühsam sein, genau wie der Code, der RAII verwendet, überraschend ineffizient sein kann, wenn er shared_ptrohne Grund für alles verwendet wird.


5
"Im Sinne der Entwicklungsbemühungen ist RAII normalerweise weniger effizient als GC" Nachdem ich sowohl in C # als auch in C ++ programmiert habe, wo Sie eine ziemlich gute Auswahl beider Strategien erhalten, muss ich dieser Behauptung stark widersprechen. Wenn Benutzer das RAII-Modell von C ++ als weniger effizient empfinden, ist dies mehr als wahrscheinlich, weil sie es nicht richtig verwenden. Was streng genommen kein Fehler des Modells ist. Meistens ist dies ein Zeichen dafür, dass Leute in C ++ programmieren, als wäre es Java oder C #. Es ist nicht schwieriger, ein temporäres Objekt zu erstellen und es durch Scoping automatisch freizugeben, als auf GC zu warten.
Cody Gray

5

Garbage Collection und RAII unterstützen jeweils ein gemeinsames Konstrukt, für das das andere nicht wirklich geeignet ist.

In einem Müllsammelsystem kann Code Verweise auf unveränderliche Objekte (wie Zeichenfolgen) effizient als Proxys für die darin enthaltenen Daten behandeln. Das Weitergeben solcher Referenzen ist fast so billig wie das Weitergeben von "dummen" Zeigern und schneller als das Erstellen einer separaten Kopie der Daten für jeden Eigentümer oder der Versuch, den Besitz einer gemeinsam genutzten Kopie der Daten zu verfolgen. Darüber hinaus machen es müllsammelnde Systeme einfach, unveränderliche Objekttypen zu erstellen, indem eine Klasse geschrieben wird, die ein veränderliches Objekt erstellt, es wie gewünscht auffüllt und Zugriffsmethoden bereitstellt, während keine Verweise auf irgendetwas verloren gehen, das es nach dem Konstruktor mutieren könnte endet. In Fällen, in denen Verweise auf unveränderliche Objekte weitgehend kopiert werden müssen, die Objekte selbst jedoch nicht, schlägt GC RAII zweifellos.

Andererseits eignet sich RAII hervorragend für Situationen, in denen ein Objekt exklusive Dienste von externen Entitäten erhalten muss. Während viele GC-Systeme es Objekten ermöglichen, "Finalize" -Methoden zu definieren und eine Benachrichtigung anzufordern, wenn festgestellt wird, dass sie abgebrochen werden, und solche Methoden manchmal externe Dienste freigeben, die nicht mehr benötigt werden, sind sie selten zuverlässig genug, um eine zufriedenstellende Methode bereitzustellen Sicherstellung der rechtzeitigen Freigabe externer Dienste. Für die Verwaltung nicht fungibler externer Ressourcen schlägt RAII GC zweifellos.

Der Hauptunterschied zwischen den Fällen, in denen GC gewinnt, und denen, in denen RAII gewinnt, besteht darin, dass GC gut in der Lage ist, fungibles Gedächtnis zu verwalten, das nach Bedarf freigegeben werden kann, aber schlecht im Umgang mit nicht fungiblen Ressourcen. RAII ist gut im Umgang mit Objekten mit klarem Eigentum, aber schlecht im Umgang mit inhaberlosen unveränderlichen Dateninhabern, die außer den darin enthaltenen Daten keine wirkliche Identität haben.

Da weder GC noch RAII alle Szenarien gut handhaben, wäre es für Sprachen hilfreich, beide gut zu unterstützen. Leider neigen Sprachen, die sich auf das eine konzentrieren, dazu, das andere als nachträglichen Gedanken zu behandeln.


5

Der Hauptteil der Frage, ob der eine oder der andere "nützlich" oder "effizienter" ist, kann nicht beantwortet werden, ohne viel Kontext anzugeben und über die Definitionen dieser Begriffe zu streiten.

Darüber hinaus können Sie im Grunde die Spannung der alten "Ist Java oder C ++ die bessere Sprache?" Flammenkrieg knistert in den Kommentaren. Ich frage mich, wie eine "akzeptable" Antwort auf diese Frage aussehen könnte, und bin neugierig, sie irgendwann zu sehen.

Ein Punkt über einen möglicherweise wichtigen konzeptionellen Unterschied wurde jedoch noch nicht herausgestellt: Mit RAII sind Sie an den Thread gebunden, der den Destruktor aufruft. Wenn Ihre Anwendung ist Single - Threaded (und obwohl es Herb Sutter war, der erklärt , dass der Free Lunch Is Over : Die meisten Software heute effektiv noch ist single-threaded), dann ein einzelner Kern mit der Handhabung der Bereinigungen von Objekten beschäftigt sein können , die keine sind länger relevant für das eigentliche Programm ...

Im Gegensatz dazu läuft der Garbage Collector normalerweise in einem eigenen Thread oder sogar in mehreren Threads und ist somit (bis zu einem gewissen Grad) von der Ausführung der anderen Teile entkoppelt.

(Hinweis: In einigen Antworten wurde bereits versucht, Anwendungsmuster mit unterschiedlichen Merkmalen, Effizienz, Leistung, Latenz und Durchsatz aufzuzeigen. Dieser spezielle Punkt wurde jedoch noch nicht erwähnt.)


4
Wenn Sie die Umgebung einschränken, wenn Ihr Computer auf einem einzelnen Kern ausgeführt wird oder Multitasking ausgiebig verwendet, werden Ihre Haupt- und GC-Threads zwangsläufig auf demselben Kern ausgeführt. Glauben Sie mir, Kontextwechsel haben viel mehr Aufwand als das Bereinigen Ihrer Ressourcen :)
Abhinav Gauniyal

@AbhinavGauniyal Wie ich zu betonen versuchte: Dies ist ein konzeptioneller Unterschied. Andere wiesen bereits auf die Verantwortlichkeiten hin , konzentrierten sich jedoch auf die Sichtweise eines Benutzers (~ "der Benutzer ist für die Bereinigung verantwortlich"). Mein Punkt war, dass dies auch einen wichtigen technischen Unterschied macht: Ob das Hauptprogramm für die Bereinigung verantwortlich ist oder ob es dafür einen (unabhängigen) Teil der Infrastruktur gibt. Ich dachte jedoch nur, dass dies angesichts der zunehmenden Anzahl von Kernen (die in Single-Thread-Programmen häufig inaktiv sind) erwähnenswert sein könnte.
Marco13

Ja, ich habe dich dafür gestimmt. Ich habe nur die andere Seite Ihres Standpunkts vorgestellt.
Abhinav Gauniyal

@ Marco13: Außerdem sind die Kosten für die Bereinigung zwischen RAII und GC völlig unterschiedlich. RAII impliziert im schlimmsten Fall das Durchlaufen einer gerade freigegebenen komplexen Referenzzähldatenstruktur. GC bedeutet im schlimmsten Fall, alle lebenden Objekte zu durchlaufen, was umgekehrt ist.
Ninjalj

@ninjalj Ich bin kein Experte für Details - Garbage Collection ist buchstäblich ein eigener Forschungszweig. Um über die Kosten zu streiten, müsste man wahrscheinlich die Schlüsselwörter auf eine bestimmte Implementierung festlegen (für RAII gibt es nicht viel Raum für verschiedene Optionen, aber zumindest weiß ich, dass es einige GC-Implementierungen mit sehr unterschiedlichen Strategien gibt). .
Marco13

4

RAII behandelt einheitlich alles, was als Ressource beschrieben werden kann. Dynamische Zuweisungen sind eine solche Ressource, aber sie sind keineswegs die einzige und wohl nicht die wichtigste. Dateien, Sockets, Datenbankverbindungen, GUI-Feedback und mehr können mit RAII deterministisch verwaltet werden.

GCs befassen sich nur mit dynamischen Zuweisungen, wodurch der Programmierer sich keine Sorgen mehr über das Gesamtvolumen der zugewiesenen Objekte während der Laufzeit des Programms machen muss (sie müssen sich nur um die Anpassung des maximalen gleichzeitigen Zuordnungsvolumens kümmern).


1

RAII und Garbage Collection sollen verschiedene Probleme lösen.

Wenn Sie RAII verwenden, belassen Sie ein Objekt auf dem Stapel, dessen einziger Zweck darin besteht, alles zu bereinigen, was verwaltet werden soll (Sockets, Speicher, Dateien usw.), wenn Sie den Bereich der Methode verlassen. Dies dient der Ausnahmesicherheit und nicht nur der Speicherbereinigung. Aus diesem Grund erhalten Sie Antworten zum Schließen von Sockets und zum Freigeben von Mutexen und dergleichen. (Okay, außer mir hat niemand Mutexe erwähnt.) Wenn eine Ausnahme ausgelöst wird, werden durch das Abwickeln des Stapels auf natürliche Weise die von einer Methode verwendeten Ressourcen bereinigt.

Garbage Collection ist die programmatische Verwaltung des Speichers, obwohl Sie andere knappe Ressourcen "garbage-sammeln" können, wenn Sie möchten. In 99% der Fälle ist es sinnvoller, sie explizit freizugeben. Der einzige Grund, RAII für eine Datei oder einen Socket zu verwenden, besteht darin, dass Sie erwarten, dass die Verwendung der Ressource abgeschlossen ist, wenn die Methode zurückgegeben wird.

Die Garbage Collection behandelt auch Objekte, die Heap-zugewiesen sind , wenn beispielsweise eine Factory eine Instanz eines Objekts erstellt und zurückgibt. Persistente Objekte in Situationen zu haben, in denen die Kontrolle einen Bereich verlassen muss, macht die Speicherbereinigung attraktiv. Sie können RAII jedoch ab Werk verwenden. Wenn vor Ihrer Rückkehr eine Ausnahme ausgelöst wird, gehen keine Ressourcen verloren.


0

Ich habe sogar gehört, dass ein Garbage Collector effizienter sein kann, da er größere Speicherblöcke gleichzeitig freigeben kann, anstatt kleine Speicherelemente im gesamten Code freizugeben.

Das ist perfekt machbar - und wird tatsächlich tatsächlich gemacht - mit RAII (oder mit einfachem malloc / free). Sie sehen, Sie verwenden nicht unbedingt immer den Standardzuweiser, der nur stückweise die Zuordnung aufhebt. In bestimmten Kontexten verwenden Sie benutzerdefinierte Allokatoren mit unterschiedlichen Funktionen. Einige Allokatoren haben die eingebaute Fähigkeit, alles in einem Allokatorbereich auf einmal freizugeben, ohne einzelne zugewiesene Elemente iterieren zu müssen.

Natürlich stellen Sie sich dann die Frage, wann Sie die Zuordnung aufheben müssen - ob und wie die Verwendung dieser Zuweiser (oder der Speicherplatte, mit der sie verknüpft sind, RAIIed werden muss oder nicht).

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.