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.
Antworten:
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.
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
IDisposable
Schnittstelle haben.Kann sein ... hängt von der Implementierung ab.
quelle
java.io.Closeable
Schnittstelle und „Try-mit-Ressourcen“ Blöcke), die ist vollständig deterministisch . Der Teil der Antwort über den Determinismus einer "Bereinigungsoperation" ist also falsch.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
struct
mit 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.
quelle
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?malloc
dass 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.FooWidgetManager
, die IhreFoo
Objekte verwaltet , werden registrierte Objekte höchstwahrscheinlichFoo
in einer unbegrenzt wachsenden Datenstruktur gespeichert . Ein solches "registriertesFoo
" Objekt liegt außerhalb der Reichweite Ihres GC, daFooWidgetManager
die interne Liste oder was auch immer einen Verweis darauf enthält . Um diesen Speicher freizugeben, müssen Sie darum bittenFooWidgetManager
, 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 .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). Darinstd::shared_ptr
befindet sich ein echter Referenzzähler. Der Speicher wird jedoch immer noch implizit manuell verwaltet (mitnew
unddelete
innerhalb von Konstruktoren und Destruktoren ausgelöst), aber dieses "implizite"delete
(in Destruktoren) gibt die Illusion einer automatischen Speicherverwaltung. Anrufe annew
unddelete
passieren 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.
quelle
shared_ptr
die 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_ptr
aber dies ist nur ein sehr begrenzter Unterraum, für den die GC-basierte Welt ähnliche Mittel wie C #using
oder Java verwendettry-with-resources
. Die reale Welt hat aber auch kompliziertere Szenarien.unique_ptr
behandelt 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 keineRAII 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-
with
Operator) für die Zeiten, auf die Sie wirklich achten möchten Sie.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).with
und GC Through ein Refcounting-System sowie einen Garbage CollectorIDisposable
undusing
und GC durch einen Garbage Collector der GenerationDie Muster tauchen in jeder Sprache auf.
quelle
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.
quelle
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 .
quelle
"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_ptr
ohne Grund für alles verwendet wird.quelle
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.
quelle
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.)
quelle
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).
quelle
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.
quelle
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).
quelle