Was sind die Komplexitäten der speicherunverwalteten Programmierung?

24

Oder mit anderen Worten, welche spezifischen Probleme hat die automatisierte Speicherbereinigung gelöst? Ich habe noch nie Low-Level-Programmierung durchgeführt, daher weiß ich nicht, wie kompliziert es sein kann, Ressourcen freizugeben.

Die Art von Fehlern, die GC anspricht, scheint (zumindest für einen externen Beobachter) die Art von Dingen zu sein, die ein Programmierer, der seine Sprache, Bibliotheken, Konzepte, Redewendungen usw. gut kennt, nicht tun würde. Aber ich könnte mich irren: Ist die manuelle Speicherverwaltung an sich kompliziert?

vemv
quelle
3
Erweitern Sie bitte, um uns mitzuteilen, dass Ihre Frage im Wikipedia-Artikel zur Garbace-Sammlung und insbesondere im Abschnitt zu den Vorteilen
yannis
Ein weiterer Vorteil ist die Sicherheit, z. B. sind Pufferüberläufe in hohem Maße ausnutzbar und viele andere Sicherheitslücken ergeben sich aus der Speicherverwaltung (Fehlerverwaltung).
StuperUser
7
@StuperUser: Das hat nichts mit dem Ursprung des Gedächtnisses zu tun. Sie können Overrun-Speicher, der von einem GC stammt, gut puffern. Die Tatsache, dass GC-Sprachen dies normalerweise verhindern, ist orthogonal, und Sprachen, die weniger als 30 Jahre hinter der GC-Technologie zurückliegen, werden verglichen, um auch Pufferüberlaufschutz zu bieten.
DeadMG

Antworten:

29

Ich habe noch nie Low-Level-Programmierung durchgeführt, daher weiß ich nicht, wie kompliziert es sein kann, Ressourcen freizugeben.

Komisch, wie sich die Definition von "Low-Level" im Laufe der Zeit ändert. Als ich das Programmieren zum ersten Mal lernte, wurde jede Sprache, die ein standardisiertes Heap-Modell bereitstellte, das ein einfaches Zuweisungs- / Freigabemuster ermöglicht, als hoch angesehen. Bei der Programmierung auf niedriger Ebene müssten Sie den Speicher selbst überwachen (nicht die Zuordnungen, sondern die Speicherorte selbst!) Oder Ihren eigenen Heap-Allokator schreiben, wenn Sie Lust dazu haben.

Davon abgesehen ist es überhaupt nicht beängstigend oder "kompliziert". Erinnerst du dich, als du ein Kind warst und deine Mutter hat dir gesagt, du sollst dein Spielzeug weglegen, wenn du damit fertig bist, dass sie nicht deine Magd ist und dein Zimmer nicht für dich aufräumen würde? Speicherverwaltung ist einfach dasselbe Prinzip, das auf Code angewendet wird. (GC ist wie eine Magd, die sich um Sie kümmert, aber sie ist sehr faul und leicht ahnungslos.) Das Prinzip ist einfach: Jede Variable in Ihrem Code hat einen und nur einen Eigentümer, und es liegt in der Verantwortung dieses Eigentümers Geben Sie den Speicher der Variablen frei, wenn diese nicht mehr benötigt wird. ( Das Prinzip des Alleineigentums) Dies erfordert einen Aufruf pro Zuordnung, und es gibt mehrere Schemata, die den Besitz und die Bereinigung auf die eine oder andere Weise automatisieren, sodass Sie diesen Aufruf nicht einmal in Ihren eigenen Code schreiben müssen.

Garbage Collection soll zwei Probleme lösen. Es macht immer einen sehr schlechten Job bei einem von ihnen, und abhängig von der Implementierung kann es bei dem anderen gut oder schlecht laufen. Die Probleme sind Speicherverluste (Festhalten am Speicher, nachdem Sie damit fertig sind) und baumelnde Referenzen (Freigeben von Speicher, bevor Sie damit fertig sind). Schauen wir uns beide Probleme an:

Baumelnde Referenzen: Diskutieren Sie diese zuerst, weil es die wirklich ernsthafte ist. Sie haben zwei Zeiger auf dasselbe Objekt. Sie befreien einen von ihnen und bemerken den anderen nicht. Dann versuchen Sie zu einem späteren Zeitpunkt, den zweiten zu lesen (oder zu schreiben oder freizugeben). Undefiniertes Verhalten folgt. Wenn Sie es nicht bemerken, können Sie Ihr Gedächtnis leicht beschädigen. Garbage Collection soll dieses Problem unmöglich machen, indem sichergestellt wird, dass niemals etwas freigegeben wird, bis alle Verweise darauf verschwunden sind. In einer vollständig verwalteten Sprache funktioniert dies fast, bis Sie sich mit externen, nicht verwalteten Speicherressourcen befassen müssen. Dann ist es gleich wieder Platz 1. Und in einer nicht verwalteten Sprache sind die Dinge noch schwieriger. (Stöbern Sie in Mozilla '

Glücklicherweise ist der Umgang mit diesem Problem im Grunde ein gelöstes Problem. Sie brauchen keinen Garbage Collector, Sie brauchen einen Debugging Memory Manager. Ich verwende zum Beispiel Delphi und kann mit einer einzigen externen Bibliothek und einer einfachen Compiler-Direktive den Allokator auf "Full Debug Mode" setzen. Dies fügt einen vernachlässigbaren (weniger als 5%) Leistungsaufwand hinzu, um einige Funktionen zu aktivieren, die den verwendeten Speicher verfolgen. Wenn ich ein Objekt freigebe, füllt es seinen Speicher mit0x80Bytes (im Debugger leicht erkennbar) und wenn ich jemals versuche, eine virtuelle Methode (einschließlich des Destruktors) für ein freigegebenes Objekt aufzurufen, bemerkt und unterbricht sie das Programm mit einer Fehlerbox mit drei Stapelspuren - als das Objekt erstellt wurde, Wann es befreit wurde und wo ich mich jetzt befinde - plus einige andere nützliche Informationen, löst dann eine Ausnahme aus. Dies ist offensichtlich nicht für Release-Builds geeignet, macht jedoch das Auffinden und Beheben von schwebenden Referenzproblemen trivial.

Das zweite Problem sind Speicherverluste. Dies ist der Fall, wenn Sie den zugewiesenen Speicher weiter belassen, wenn Sie ihn nicht mehr benötigen. Dies kann in jeder Sprache mit oder ohne Garbage Collection geschehen und kann nur durch Schreiben Ihres Codes behoben werden. Garbage Collection hilft dabei, eine bestimmte Form des Speicherverlusts zu verringern , die auftritt, wenn Sie keine gültigen Verweise auf ein noch nicht freigegebenes Speicherelement haben. Dies bedeutet, dass der Speicher bis zum Ende des Programms zugewiesen bleibt. Leider ist die einzige Möglichkeit, dies auf automatisierte Weise zu erreichen, die Umwandlung jeder Zuordnung in ein Speicherverlust!

Ich werde mich wahrscheinlich von GC-Befürwortern verletzen lassen, wenn ich so etwas zu sagen versuche. Lassen Sie mich das erklären. Denken Sie daran, dass die Definition eines Speicherverlusts den zugewiesenen Speicher beibehält, wenn Sie ihn nicht mehr benötigen. Sie haben nicht nur keine Verweise auf etwas, sondern können auch Speicher verlieren, indem Sie einen unnötigen Verweis darauf haben, z. B. indem Sie ihn in einem Containerobjekt halten, wenn Sie ihn hätten freigeben sollen. Ich habe einige Speicherlecks gesehen, die dadurch verursacht wurden, und die sehr schwierig zu finden sind, ob Sie einen GC haben oder nicht, da sie einen perfekt gültigen Verweis auf den Speicher enthalten und es keine eindeutigen "Bugs" für das Debuggen von Tools gibt Fang. Soweit ich weiß, gibt es kein automatisiertes Tool, mit dem Sie diese Art von Speicherverlust abfangen können.

Ein Garbage Collector befasst sich also nur mit der Vielzahl von Speicherlecks, die keine Referenzen enthalten, da dies der einzige Typ ist, der automatisiert behandelt werden kann. Wenn es alle Ihre Verweise auf alles überwachen und jedes Objekt freigeben könnte, sobald keine Verweise darauf verweisen, wäre es perfekt, zumindest in Bezug auf das Problem der Nichtverweise. Dies auf automatisierte Weise zu tun , wird als Referenzzählung bezeichnet und kann in einigen begrenzten Situationen durchgeführt werden, hat jedoch seine eigenen Probleme. (Beispiel: Objekt A enthält einen Verweis auf Objekt B, der einen Verweis auf Objekt A enthält. In einem Referenzzählschema kann kein Objekt automatisch freigegeben werden, auch wenn keine externen Verweise auf A oder B vorhanden sind.) Also Müllsammler verwenden die AblaufverfolgungStattdessen: Beginnen Sie mit einer Reihe von Objekten, die als funktionierend bekannt sind, suchen Sie alle Objekte, auf die sie verweisen, suchen Sie alle Objekte, auf die sie verweisen, usw., bis Sie alles gefunden haben. Was beim Aufspüren nicht gefunden wird, ist Müll und kann weggeworfen werden. (Für eine erfolgreiche Ausführung ist natürlich eine verwaltete Sprache erforderlich, die bestimmte Einschränkungen für das Typsystem enthält, um sicherzustellen, dass der Tracing-Garbage-Collector immer den Unterschied zwischen einer Referenz und einem zufälligen Teil des Speichers erkennen kann, der zufällig wie ein Zeiger aussieht.)

Es gibt zwei Probleme bei der Ablaufverfolgung. Erstens ist es langsam und währenddessen muss das Programm mehr oder weniger pausiert werden, um Rennbedingungen zu vermeiden. Dies kann zu spürbaren Ausführungsproblemen führen, wenn das Programm mit einem Benutzer interagieren soll, oder zu einer schlechten Leistung in einer Server-App. Dies kann durch verschiedene Techniken gemildert werden, z. B. das Aufteilen des zugewiesenen Speichers in "Generationen" nach dem Prinzip, dass es wahrscheinlich eine Weile dauern wird, bis eine Zuordnung beim ersten Versuch nicht erfasst wird. Sowohl das .NET Framework als auch die JVM verwenden generationsübergreifende Garbage Collectors.

Leider führt dies zu dem zweiten Problem: Speicher wird nicht freigegeben, wenn Sie damit fertig sind. Wenn die Ablaufverfolgung nicht unmittelbar nach dem Beenden eines Objekts ausgeführt wird, bleibt sie bis zur nächsten Ablaufverfolgung bestehen oder sogar noch länger, wenn die erste Generation überschritten wird. Tatsächlich erklärt eine der besten Erklärungen des .NET-Garbage Collectors , dass der GC die Erfassung so lange wie möglich verschieben muss, um den Prozess so schnell wie möglich zu gestalten. Das Problem der Speicherlecks wird also ziemlich seltsam gelöst, indem so lange wie möglich so viel Speicher wie möglich verloren geht! Dies ist, was ich meine, wenn ich sage, dass ein GC jede Zuordnung in einen Speicherverlust verwandelt. Tatsächlich gibt es keine Garantie dafür, dass ein bestimmtes Objekt jemals abgeholt wird.

Warum ist dies ein Problem, wenn der Speicher bei Bedarf immer noch zurückgefordert wird? Aus mehreren Gründen. Stellen Sie sich zunächst vor, Sie ordnen ein großes Objekt zu (z. B. eine Bitmap), das viel Speicher benötigt. Und kurz nachdem Sie damit fertig sind, benötigen Sie ein weiteres großes Objekt, das die gleiche (oder nahezu die gleiche) Speichermenge benötigt. Wurde das erste Objekt freigegeben, kann das zweite sein Gedächtnis wiederverwenden. Auf einem System mit Speicherbereinigung warten Sie möglicherweise immer noch auf die Ausführung der nächsten Ablaufverfolgung, sodass unnötigerweise Speicher für ein zweites großes Objekt verschwendet wird. Es ist im Grunde eine Rennbedingung.

Zweitens kann das unnötige Halten des Speichers, insbesondere in großen Mengen, in einem modernen Multitasking-System Probleme verursachen. Wenn Sie zu viel physischen Speicher belegen, muss Ihr Programm oder andere Programme möglicherweise paginieren (einen Teil ihres Speichers auf Disc auslagern), was die Geschwindigkeit erheblich senkt. Bei bestimmten Systemen, wie z. B. Servern, kann Paging nicht nur das System verlangsamen, sondern auch das Ganze zum Absturz bringen, wenn es unter Last steht.

Wie das Problem der baumelnden Referenzen kann das Problem der fehlenden Referenzen mit einem Debugging-Speichermanager gelöst werden. Ich erwähne noch einmal den vollständigen Debug-Modus von Delphis FastMM-Speichermanager, da er derjenige ist, mit dem ich am vertrautesten bin. (Ich bin sicher, dass ähnliche Systeme für andere Sprachen existieren.)

Wenn ein Programm, das unter FastMM ausgeführt wird, beendet wird, können Sie optional angeben, dass alle Zuordnungen vorhanden sind, die nie freigegeben wurden. Der vollständige Debug-Modus geht noch einen Schritt weiter: Er kann eine Datei auf einem Datenträger speichern, der nicht nur die Art der Zuweisung, sondern auch eine Stapelverfolgung von der Zuweisung an und andere Debug-Informationen für jede durchgesickerte Zuweisung enthält. Dies macht das Auffinden von Speicherlecks ohne Verweise trivial.

Wenn Sie sich das wirklich ansehen, kann die Garbage Collection möglicherweise nicht gut genug sein, um herabhängende Referenzen zu verhindern, und im Allgemeinen ist die Behandlung von Speicherlecks eine schlechte Sache. Tatsächlich ist die eine Tugend nicht die Speicherbereinigung selbst, sondern ein Nebeneffekt: Sie bietet eine automatisierte Möglichkeit zur Durchführung der Haufenverdichtung. Dies kann ein arkanes Problem (Speichererschöpfung durch Heap-Fragmentierung) verhindern, das Programme, die über einen langen Zeitraum ausgeführt werden und einen hohen Grad an Speicherabwanderung aufweisen, zum Erliegen bringen kann, und eine Heap-Komprimierung ist ohne Garbage Collection so gut wie unmöglich. Heutzutage verwendet ein guter Speicherzuordner jedoch Buckets, um die Fragmentierung zu minimieren, was bedeutet, dass die Fragmentierung nur unter extremen Umständen zu einem echten Problem wird. Bei einem Programm, bei dem die Heap-Fragmentierung wahrscheinlich ein Problem darstellt, Es wird empfohlen, einen Müllsammler zu verwenden. Aber in jedem anderen Fall ist die Verwendung der Speicherbereinigung eine vorzeitige Optimierung, und es gibt bessere Lösungen für die Probleme, die sie "löst".

Mason Wheeler
quelle
5
Ich liebe diese Antwort - ich lese sie immer wieder. Ich kann mir keine relevante Bemerkung einfallen lassen, also kann ich nur sagen - danke.
Vemv
3
Ich möchte darauf hinweisen, dass GCs dazu neigen, Speicher zu "verlieren" (zumindest für eine Weile), aber dies ist kein Problem, da der Speicher erfasst wird, wenn der Speicherzuordner vor der Erfassung keinen Speicher zuordnen kann. Bei einer Nicht-GC-Sprache bleibt ein Leck immer ein Leck, was bedeutet, dass Ihnen aufgrund von zu viel nicht gesammeltem Speicher der Speicher ausgeht. "Speicherbereinigung ist vorzeitige Optimierung" ... GC ist keine Optimierung und wurde nicht in diesem Sinne entwickelt. Ansonsten gute Antwort.
Thomas Eding
7
@ThomasEding: GC ist sicherlich eine Optimierung; Es optimiert für minimalen Programmieraufwand auf Kosten der Leistung und verschiedener anderer Programmqualitätsmetriken.
Mason Wheeler
5
Komisch, dass Sie an einer Stelle auf Mozillas Bug-Tracker zeigen, weil Mozilla zu einem ganz anderen Schluss gekommen ist. Firefox hatte und hat unzählige Sicherheitsprobleme, die auf Speicherverwaltungsfehler zurückzuführen sind. Beachten Sie, dass es nicht darum geht, wie einfach es war, den einmal erkannten Fehler zu beheben. In der Regel ist der Schaden bereits behoben, wenn die Entwickler auf das Problem aufmerksam werden. Mozilla finanziert die Programmiersprache Rust genau, um zu verhindern, dass solche Fehler überhaupt erst auftreten.
1
Rust verwendet jedoch keine Müllabfuhr, sondern die Referenzzählung, die Mason genau beschreibt, nur mit umfangreichen Überprüfungen zur Kompilierungszeit, anstatt einen Debugger verwenden zu müssen, um Fehler zur Laufzeit zu erkennen ...
Sean Burton
13

Betrachtet man eine Speicherverwaltungstechnik ohne Speicherbereinigung aus einer ähnlichen Zeit wie die Speicherbereinigung, die in gängigen Systemen wie C ++ RAII verwendet wird. Angesichts dieses Ansatzes sind die Kosten für die Nichtverwendung der automatischen Speicherbereinigung minimal, und GC führt zu zahlreichen eigenen Problemen. Daher würde ich vorschlagen, dass "Nicht viel" die Antwort auf Ihr Problem ist.

Denken Sie daran, wenn Menschen an Nicht-GC denken, denken sie an mallocund free. Aber dies ist ein riesiger logischer Irrtum - Sie würden das Management von Nicht-GC-Ressourcen der frühen 1970er Jahre mit den Müllsammlern der späten 90er Jahre vergleichen. Dies ist offensichtlich ein ziemlich unfairer Vergleich - die Garbage Collectors, die zu der Zeit verwendet wurden mallocund freeentworfen wurden, waren viel zu langsam, um ein sinnvolles Programm auszuführen, wenn ich mich richtig erinnere. unique_ptrViel aussagekräftiger ist beispielsweise der Vergleich von etwas aus einem vage äquivalenten Zeitraum .

Müllsammler können mit Referenzzyklen einfacher umgehen, obwohl dies nur sehr selten vorkommt. Darüber hinaus können GCs nur Code "ausgeben", da sich der GC um die gesamte Speicherverwaltung kümmert, was bedeutet, dass sie zu schnelleren Entwicklungszyklen führen können.

Auf der anderen Seite neigen sie dazu, massive Probleme beim Umgang mit Speicher zu bekommen, der von irgendwoher kam, außer von ihrem eigenen GC-Pool. Darüber hinaus verlieren sie einen Großteil ihres Nutzens, wenn es sich um Nebenläufigkeiten handelt, da Sie ohnehin den Besitz von Objekten berücksichtigen müssen.

Bearbeiten: Viele der Dinge, die Sie erwähnen, haben nichts mit GC zu tun. Sie verwechseln Speicherverwaltung und Objektorientierung. Sehen Sie, hier ist die Sache: Wenn Sie in einem vollständig nicht verwalteten System wie C ++ programmieren, können Sie so viele Grenzen prüfen, wie Sie möchten, und die Standard-Containerklassen bieten dies an. Es gibt zum Beispiel nichts, wenn Grenzen überprüft oder stark getippt werden.

Die von Ihnen genannten Probleme werden durch Objektorientierung und nicht durch GC gelöst. Der Ursprung des Array-Speichers und das Sicherstellen, dass Sie nicht außerhalb desselben schreiben, sind orthogonale Konzepte.

Bearbeiten: Es ist erwähnenswert, dass fortgeschrittenere Techniken die Notwendigkeit einer dynamischen Speicherzuweisung überhaupt vermeiden können. Betrachten Sie beispielsweise die Verwendung von this , das Y-Combination in C ++ ohne dynamische Zuweisung implementiert.

DeadMG
quelle
Die ausführliche Diskussion hier wurde aufgeräumt: Wenn jeder sich mit ihm unterhalten kann , um das Thema weiter zu diskutieren, wäre ich sehr dankbar dafür.
@DeadMG, weißt du, was der Kombinator tun soll? Es soll KOMBINIEREN. Kombinator ist per Definition eine Funktion ohne freie Variablen.
SK-logic
2
@ SK-logic: Ich hätte mich dafür entscheiden können, es nur als Vorlage zu implementieren und keine Mitgliedsvariablen zu haben. Aber dann könnten Sie nicht an Verschlüssen vorbeikommen, was die Nützlichkeit erheblich einschränkt. Möchtest du zum Chatten kommen?
DeadMG
@DeadMG, eine Definition ist glasklar. Keine freien Variablen. Ich halte jede Sprache für "funktionell genug", wenn es möglich ist, den Y-Kombinator zu definieren (richtig, nicht auf Ihre Art). Ein großes "+" ist, wenn es möglich ist, es über die Kombinatoren S, K und I zu definieren. Ansonsten ist die Sprache nicht ausdrucksstark genug.
SK-logic am
4
@ SK-logic: Wieso kommst du nicht zum Chat , wie der nette Moderator gefragt hat? Ein Y-Kombinator ist auch ein Y-Kombinator, er erledigt die Arbeit oder nicht. Die Haskell-Version des Y-Kombinators ist im Grunde genau dieselbe wie diese, nur dass der ausgedrückte Zustand vor Ihnen verborgen ist.
DeadMG
11

Die "Freiheit, sich keine Sorgen um die Freigabe von Ressourcen machen zu müssen", die müllsammelnde Sprachen angeblich bieten, ist in erheblichem Maße eine Illusion. Fügen Sie immer wieder Dinge zu einer Karte hinzu, ohne sie zu entfernen, und Sie werden bald verstehen, wovon ich spreche.

In der Tat treten in Programmen, die in GC-Sprachen geschrieben wurden, häufig Speicherverluste auf, da diese Sprachen die Programmierer fauler machen und ihnen ein falsches Gefühl der Sicherheit vermitteln, dass die Sprache jedes Objekt, das sie bearbeiten, immer irgendwie (magisch) pflegt Ich möchte nicht mehr darüber nachdenken müssen.

Die Speicherbereinigung ist einfach eine notwendige Einrichtung für Sprachen, die ein anderes, edleres Ziel haben: alles als Zeiger auf ein Objekt zu behandeln und gleichzeitig vor dem Programmierer die Tatsache zu verbergen, dass es sich um einen Zeiger handelt, so dass der Programmierer kein Commit durchführen kann Selbstmord durch Zeigerarithmetik und dergleichen. Alles, was ein Objekt ist, bedeutet, dass GCed-Sprachen Objekte viel häufiger zuordnen müssen als nicht-GCed-Sprachen. Wenn sie dem Programmierer die Last der Freigabe dieser Objekte auferlegen, wären sie immens unattraktiv.

Garbage Collection ist auch nützlich, um dem Programmierer die Möglichkeit zu geben, engen Code zu schreiben, Objekte in Ausdrücken auf eine funktionale Programmierweise zu manipulieren, ohne die Ausdrücke in separate Anweisungen aufteilen zu müssen, um die Zuordnung für alle aufzuheben einzelnes Objekt, das an dem Ausdruck beteiligt ist.

Ansonsten sei angemerkt, dass ich zu Beginn meiner Antwort geschrieben habe: "Es ist in erheblichem Maße eine Illusion." Ich habe nicht geschrieben, dass es eine Illusion ist. Ich habe nicht einmal geschrieben, dass es hauptsächlich eine Illusion ist. Garbage Collection ist nützlich, um dem Programmierer die grundlegende Aufgabe zu nehmen, sich um die Freigabe seiner Objekte zu kümmern. In diesem Sinne handelt es sich also um ein Produktivitätsmerkmal.

Mike Nakis
quelle
4

Der Garbage Collector behebt keine "Bugs". Es ist ein notwendiger Bestandteil einiger Hochsprachen-Semantik. Mit einem GC können höhere Abstraktionsebenen definiert werden, z. B. lexikalische Closures und dergleichen, während diese Abstraktionen bei einer manuellen Speicherverwaltung undicht sind und unnötigerweise an die niedrigeren Ebenen der Ressourcenverwaltung gebunden sind.

Ein in den Kommentaren erwähntes "Prinzip der einmaligen Inhaberschaft" ist ein gutes Beispiel für eine solche undichte Abstraktion. Ein Entwickler sollte sich überhaupt keine Gedanken über die Anzahl der Verknüpfungen zu einer bestimmten elementaren Datenstrukturinstanz machen, da sonst ein Codeteil ohne eine Vielzahl zusätzlicher (im Code selbst nicht direkt sichtbarer) Einschränkungen und Anforderungen nicht generisch und transparent wäre . Ein solcher Code kann nicht zu einem Code auf höherer Ebene zusammengesetzt werden, was einen unerträglichen Verstoß gegen das Prinzip der Aufteilung der Verantwortungsebenen darstellt (ein wesentlicher Baustein des Software-Engineerings, der von den meisten Entwicklern auf niedriger Ebene leider überhaupt nicht beachtet wird).

SK-Logik
quelle
1
Bei Mason Wheeler implementiert sogar C ++ eine sehr begrenzte Form von Closures. Ist aber bei weitem kein ordentlicher, allgemein verwendbarer Verschluss.
SK-logic
1
Du liegst falsch. Kein GC kann Sie davor schützen, dass Sie nicht auf Stapelvariablen verweisen können. Und es ist lustig - in C ++ können Sie auch den Ansatz "Kopieren eines Zeigers auf eine dynamisch zugewiesene Variable, die entsprechend und automatisch zerstört wird" verwenden.
DeadMG
1
@DeadMG, sehen Sie nicht, dass Ihr Code Entitäten auf niedriger Ebene durch eine andere Ebene verliert, auf der Sie aufbauen?
SK-logic am
1
@ SK-Logic: OK, wir haben ein Terminologieproblem. Was ist Ihre Definition von "realer Schließung" und was können sie tun, was Delphis Schließungen nicht können? (Und alles, was mit Speicherverwaltung zu tun hat, verschiebt die Zielpfosten. Sprechen wir über das Verhalten und nicht über Implementierungsdetails.)
Mason Wheeler
1
@ SK-Logic: ... und haben Sie ein Beispiel für etwas, das mit einfachen, untypisierten Lambda-Closures gemacht werden kann, die Delphi-Closures nicht leisten können?
Mason Wheeler
2

In der Tat ist die Verwaltung des eigenen Speichers nur eine weitere potenzielle Fehlerquelle.

Wenn Sie einen Aufruf von vergessen free(oder was auch immer die Entsprechung in der von Ihnen verwendeten Sprache ist), kann Ihr Programm alle Tests bestehen, aber Speicher lecken. Und in einem moderat komplexen Programm ist es ziemlich einfach, einen Anruf zu übersehen free.

Laut Dawood soll Monica wieder eingesetzt werden
quelle
3
Verpasst freeist nicht das Schlimmste. Früh freeist viel verheerender.
Herby
2
Und das Doppelte free!
quant_dev
Hehe! Ich würde mich den beiden obigen Kommentaren anschließen. Ich habe selbst nie eine dieser Übertretungen begangen (soweit ich weiß), aber ich kann sehen, wie schrecklich die Auswirkungen sein könnten. Die Antwort von quant_dev sagt schon alles - Fehler bei der Speicherzuweisung und -freigabe sind bekanntermaßen schwer zu finden und zu beheben.
Dawood sagt, Monica am
1
Das ist ein Irrtum. Sie vergleichen "Anfang 1970" mit "Ende 1990". Die GCs, die zu der Zeit existierten mallocund freedie nicht zum GC gehörten, waren viel zu langsam, um für irgendetwas von Nutzen zu sein. Sie müssen es mit einem modernen Nicht-GC-Ansatz wie RAII vergleichen.
DeadMG
2
@DeadMG RAII ist keine manuelle Speicherverwaltung
quant_dev
2

Manuelle Ressourcen sind nicht nur mühsam, sondern auch schwer zu debuggen. Mit anderen Worten, es ist nicht nur mühsam, es richtig zu machen, sondern auch, wenn Sie es falsch verstehen, ist es nicht offensichtlich, wo das Problem liegt. Dies liegt daran, dass sich die Auswirkungen des Fehlers im Gegensatz zu beispielsweise einer Division durch Null nicht auf die Fehlerquelle auswirken. Das Verbinden der Punkte erfordert Zeit, Aufmerksamkeit und Erfahrung.

quant_dev
quelle
1

Ich denke, dass die Garbage Collection eine Menge Anerkennung für Sprachverbesserungen erhält, die nichts mit GC zu tun haben, außer Teil einer großen Fortschrittswelle zu sein.

Der einzige Vorteil von GC, den ich kenne, ist, dass Sie ein Objekt in Ihrem Programm freigeben können und wissen, dass es verschwindet, wenn alle damit fertig sind. Sie können es an die Methode einer anderen Klasse übergeben, ohne sich darum zu kümmern. Es ist Ihnen egal, an welche anderen Methoden es übergeben wird oder welche anderen Klassen darauf verweisen. (Speicherverluste liegen in der Verantwortung der Klasse, die auf ein Objekt verweist, nicht der Klasse, die es erstellt hat.)

Ohne GC müssen Sie den gesamten Lebenszyklus des zugewiesenen Speichers verfolgen. Jedes Mal, wenn Sie eine Adresse von der Subroutine, die sie erstellt hat, nach oben oder unten übergeben, haben Sie einen unkontrollierten Verweis auf diesen Speicher. In der schlechten alten Zeit war es mir trotz nur eines Threads aufgrund der Rekursion und des Betriebssystems (Windows NT) nicht möglich, den Zugriff auf den zugewiesenen Speicher zu kontrollieren. Ich musste die freie Methode in meinem eigenen Zuordnungssystem manipulieren, um Speicherblöcke eine Zeit lang zu behalten, bis alle Referenzen gelöscht waren. Die Haltezeit war reine Vermutung, aber es hat funktioniert.

Das ist der einzige GC-Vorteil, den ich kenne, aber ich könnte nicht ohne ihn leben. Ich denke nicht, dass irgendeine Art von OOP ohne sie fliegen wird.

RalphChapin
quelle
1
Von Anfang an waren Delphi und C ++ als OOP-Sprachen ohne GC ziemlich erfolgreich. Alles, was Sie brauchen, um "außer Kontrolle geratene Referenzen" zu verhindern, ist ein wenig Disziplin. Wenn Sie das Prinzip des Alleineigentums verstehen (siehe meine Antwort), werden die Probleme, von denen Sie hier sprechen, zu völligen Nicht-Problemen.
Mason Wheeler
@MasonWheeler: Wenn das Besitzerobjekt freigegeben werden soll, muss es alle Stellen kennen, an denen auf seine eigenen Objekte verwiesen wird. Das Verwalten dieser Informationen und das Entfernen der Verweise scheint mir eine Menge Arbeit zu sein. Ich habe oft festgestellt, dass die Referenzen noch nicht ganz gelöscht werden konnten. Ich musste den Besitzer als gelöscht markieren und ihn dann regelmäßig zum Leben erwecken, um zu sehen, ob er sich sicher selbst befreien konnte. Ich habe Delphi noch nie benutzt, aber für ein kleines Opfer an Ausführungseffizienz hat mir C # / Java gegenüber C ++ einen großen Schub an Entwicklungszeit eingebracht. (Nicht alles wegen GC, aber es hat geholfen.)
RalphChapin
1

Physische Lecks

Die Art von Fehlern, die GC anspricht, scheint (zumindest für einen externen Beobachter) die Art von Dingen zu sein, die ein Programmierer, der seine Sprache, Bibliotheken, Konzepte, Redewendungen usw. gut kennt, nicht tun würde. Aber ich könnte mich irren: Ist die manuelle Speicherverwaltung an sich kompliziert?

Wenn ich von der C-Seite komme, die die Speicherverwaltung so manuell und ausgeprägt wie möglich macht, damit wir Extreme vergleichen (C ++ automatisiert die Speicherverwaltung meist ohne GC), würde ich sagen, "nicht wirklich" im Sinne eines Vergleichs mit GC, wenn dies der Fall ist kommt zu undichtigkeiten . Ein Anfänger und manchmal sogar ein Profi kann vergessen, freefür eine gegebene zu schreiben malloc. Es passiert definitiv.

Es gibt jedoch Tools wie die valgrindLecksuche, die bei der Ausführung des Codes sofort erkennen, wann / wo solche Fehler bis auf die genaue Codezeile auftreten. Wenn dies in das CI integriert ist, ist es fast unmöglich, solche Fehler zusammenzuführen und einfach zu korrigieren. Es ist also in keinem Team / Prozess eine große Sache mit vernünftigen Standards.

Zugegeben, es kann einige exotische Fälle von Ausführung geben, die unter dem Radar des Testens freeauftauchen, wenn sie nicht aufgerufen werden, möglicherweise wenn ein obskurer externer Eingabefehler wie eine beschädigte Datei auftritt. In diesem Fall kann das System 32 Bytes oder etwas verlieren. Ich denke, das kann definitiv auch unter recht guten Teststandards und Leckerkennungswerkzeugen passieren, aber es wäre auch nicht ganz so kritisch, ein bisschen Speicher für etwas zu verlieren, das so gut wie nie passiert. Es wird ein weitaus größeres Problem geben, bei dem wir auch in den unten aufgeführten allgemeinen Ausführungspfaden massive Ressourcen verlieren können, was GC nicht verhindern kann.

Es ist auch schwierig, ohne etwas zu tun, das einer Pseudoform von GC ähnelt (z. B. Referenzzählung), wenn die Lebensdauer eines Objekts für eine Form von verzögerter / asynchroner Verarbeitung verlängert werden muss, z. B. durch einen anderen Thread.

Baumelnde Zeiger

Das eigentliche Problem mit mehr manuellen Formen der Speicherverwaltung ist für mich nicht zu übersehen. Wie viele native Anwendungen, die in C oder C ++ geschrieben wurden, sind wirklich undicht? Ist der Linux-Kernel undicht? MySQL? CryEngine 3? Digitale Audio-Workstations und Synthesizer? Leckt Java VM (im nativen Code implementiert)? Photoshop?

Ich denke, wenn wir uns umschauen, sind die am wenigsten geeigneten Anwendungen diejenigen, die mit GC-Schemata geschrieben wurden. Bevor dies jedoch als Slam-on-Garbage-Collection betrachtet wird, tritt bei systemeigenem Code ein erhebliches Problem auf, das überhaupt nicht mit Speicherverlusten zusammenhängt.

Das Thema war für mich immer Sicherheit. Selbst wenn wir uns freedurch einen Zeiger erinnern, werden andere Zeiger auf die Ressource zu baumelnden (ungültigen) Zeigern.

Wenn wir versuchen, auf die Spitzen dieser baumelnden Zeiger zuzugreifen, geraten wir letztendlich in ein undefiniertes Verhalten, obwohl fast immer eine Segfault- / Zugriffsverletzung zu einem harten, sofortigen Absturz führt.

Alle diese nativen Anwendungen, die ich oben aufgeführt habe, weisen möglicherweise ein oder zwei unklare Randbedingungen auf, die in erster Linie aufgrund dieses Problems zu einem Absturz führen können, und es gibt definitiv einen angemessenen Anteil von fehlerhaften Anwendungen, die in nativem Code geschrieben sind, der sehr absturzintensiv und häufig ist zum großen Teil aufgrund dieses Problems.

... und das liegt daran, dass das Ressourcenmanagement schwierig ist, unabhängig davon, ob Sie GC verwenden oder nicht. Der praktische Unterschied besteht häufig darin, dass der Computer aufgrund eines Fehlers, der zu einem Missmanagement der Ressourcen führt, leckt (GC) oder abstürzt (ohne GC).

Ressourcenverwaltung: Garbage Collection

Ein komplexes Ressourcenmanagement ist in jedem Fall ein schwieriger manueller Prozess. GC kann hier nichts automatisieren.

Nehmen wir ein Beispiel, wo wir dieses Objekt haben, "Joe". Auf Joe wird von einer Reihe von Organisationen verwiesen, denen er angehört. Jeden Monat oder so ziehen sie einen Mitgliedsbeitrag von seiner Kreditkarte ab.

Bildbeschreibung hier eingeben

Wir haben auch einen Hinweis auf Joe, um sein Leben zu kontrollieren. Nehmen wir an, als Programmierer brauchen wir Joe nicht mehr. Er fängt an, uns zu belästigen, und wir brauchen diese Organisationen nicht mehr, denen er angehört, und verschwenden ihre Zeit damit, sich um ihn zu kümmern. Also versuchen wir, ihn vom Erdboden zu wischen, indem wir seinen Lebenslinienbezug entfernen.

Bildbeschreibung hier eingeben

... aber warte, wir verwenden Garbage Collection. Jeder starke Hinweis auf Joe wird ihn in der Nähe halten. Deshalb entfernen wir auch Verweise auf ihn aus den Organisationen, denen er angehört (ihn abbestellen).

Bildbeschreibung hier eingeben

... außer Hoppla, wir haben vergessen, sein Zeitschriftenabonnement zu kündigen! Jetzt bleibt Joe im Gedächtnis, belästigt uns und verbraucht Ressourcen, und das Zeitschriftenunternehmen arbeitet jeden Monat an der Bearbeitung von Joes Mitgliedschaft.

Dies ist der Hauptfehler, der dazu führen kann, dass viele komplexe Programme, die mit Garbage Collection-Schemata geschrieben wurden, auslaufen und mehr und mehr Speicher verbrauchen, je länger sie ausgeführt werden und möglicherweise mehr und mehr verarbeitet werden (das Abonnement für wiederkehrende Magazine). Sie vergaßen, einen oder mehrere dieser Verweise zu entfernen, was es dem Müllsammler unmöglich machte, seine Magie zu entfalten, bis das gesamte Programm heruntergefahren wurde.

Das Programm stürzt jedoch nicht ab. Es ist absolut sicher. Es wird nur die Erinnerung auffrischen und Joe wird immer noch verweilen. Für viele Anwendungen ist diese Art von undichtem Verhalten, bei dem immer mehr Speicher / Verarbeitung zum Problem wird, einem harten Absturz weitaus vorzuziehen, insbesondere angesichts der Größe des Arbeitsspeichers und der Rechenleistung, über die unsere Maschinen heutzutage verfügen.

Ressourcenverwaltung: Manuell

Betrachten wir nun die Alternative, bei der wir Zeiger auf Joe und manuelle Speicherverwaltung verwenden, wie folgt:

Bildbeschreibung hier eingeben

Diese blauen Links verwalten nicht Joes Leben. Wenn wir ihn vom Erdboden entfernen wollen, fordern wir ihn manuell auf, wie folgt zu zerstören:

Bildbeschreibung hier eingeben

Nun, das würde uns normalerweise überall mit baumelnden Zeigern zurücklassen, also lasst uns die Zeiger auf Joe entfernen.

Bildbeschreibung hier eingeben

... hoppla, wir haben genau den gleichen Fehler gemacht und vergessen, Joes Abonnement zu kündigen!

Außer jetzt haben wir einen baumelnden Zeiger. Wenn das Zeitschriftenabonnement versucht, Joes monatliche Gebühr zu verarbeiten, explodiert die ganze Welt - normalerweise bekommen wir den schweren Absturz sofort.

Derselbe grundlegende Fehler beim Missmanagement von Ressourcen, bei dem der Entwickler vergessen hat, alle Zeiger / Verweise auf eine Ressource manuell zu entfernen, kann in nativen Anwendungen zu zahlreichen Abstürzen führen. Sie belasten den Speicher nicht, je länger sie normalerweise ausgeführt werden, da sie in diesem Fall häufig regelrecht abstürzen.

Echte Welt

Das obige Beispiel verwendet nun ein lächerlich einfaches Diagramm. Für eine reale Anwendung sind möglicherweise Tausende von Bildern erforderlich, die zusammengefügt werden, um ein vollständiges Diagramm abzudecken. In einem Szenendiagramm sind Hunderte verschiedener Ressourcentypen gespeichert, einige davon mit GPU-Ressourcen verknüpft, andere mit Beschleunigern verknüpft und Beobachter auf Hunderte von Plugins verteilt Beobachten Sie eine Reihe von Entitätstypen in der Szene auf Änderungen, Beobachter, Beobachter, Audios, die mit Animationen synchronisiert sind usw. Es scheint also, als wäre es einfach, den oben beschriebenen Fehler zu vermeiden, aber in der realen Welt ist es im Allgemeinen nicht annähernd so einfach Produktionscodebasis für eine komplexe Anwendung, die Millionen von Codezeilen umfasst.

Die Wahrscheinlichkeit, dass jemand eines Tages die Ressourcen irgendwo in dieser Codebasis falsch verwaltet, ist recht hoch, und diese Wahrscheinlichkeit ist mit oder ohne GC gleich. Der Hauptunterschied besteht darin, was als Ergebnis dieses Fehlers passieren wird. Dies wirkt sich auch potenziell darauf aus, wie schnell dieser Fehler entdeckt und behoben wird.

Crash vs. Leak

Welches ist nun schlimmer? Ein sofortiger Absturz oder ein stilles Gedächtnisleck, in dem Joe auf mysteriöse Weise herumlungert?

Die meisten antworten vielleicht auf Letzteres, aber nehmen wir an, diese Software ist so konzipiert, dass sie stundenlang, möglicherweise tagelang ausgeführt wird, und jeder dieser von uns hinzugefügten Joes und Janes erhöht die Speichernutzung der Software um ein Gigabyte. Es ist keine geschäftskritische Software (Abstürze töten Benutzer nicht wirklich), sondern eine leistungskritische.

In diesem Fall ist ein schwerer Absturz, der beim Debuggen sofort auftritt und auf den von Ihnen begangenen Fehler hinweist, möglicherweise einer undichten Software vorzuziehen, die möglicherweise sogar unter dem Radar Ihres Testverfahrens läuft.

Auf der anderen Seite, wenn es sich um eine unternehmenskritische Software handelt, bei der die Leistung nicht das Ziel ist und die auf keinen Fall abstürzt, ist eine Leckage möglicherweise sogar vorzuziehen.

Schwache Referenzen

Es gibt eine Art Hybrid dieser Ideen in GC-Schemata, die als schwache Referenzen bekannt sind. Mit schwachen Referenzen können wir alle diese Organisationen so einrichten, dass sie Joe schwach referenzieren, ihn jedoch nicht daran hindern, entfernt zu werden, wenn die starke Referenz (Joes Besitzer / Lebensader) wegfällt. Trotzdem haben wir den Vorteil, durch diese schwachen Referenzen feststellen zu können, wann Joe nicht mehr in der Nähe ist, und so eine Art leicht reproduzierbarer Fehler erhalten zu können.

Leider werden schwache Referenzen bei weitem nicht so oft verwendet, wie sie wahrscheinlich verwendet werden sollten, so dass viele komplexe GC-Anwendungen möglicherweise für Lecks anfällig sind, selbst wenn sie möglicherweise weitaus weniger abstürzen als eine komplexe C-Anwendung, z

In jedem Fall hängt es davon ab, wie wichtig es ist, dass Ihre Software Leckagen vermeidet, und ob es sich um ein komplexes Ressourcenmanagement dieser Art handelt oder nicht.

In meinem Fall arbeite ich in einem leistungskritischen Bereich, in dem Ressourcen Hunderte von Megabyte bis Gigabyte umfassen, und wenn Benutzer aufgrund eines Fehlers wie des oben genannten das Entladen anfordern, wird es einem Absturz weniger vorgezogen, diesen Speicher nicht freizugeben . Abstürze sind leicht zu erkennen und zu reproduzieren, was sie häufig zu den beliebtesten Fehlern des Programmierers macht, auch wenn sie die am wenigsten bevorzugten Fehler des Benutzers sind. Viele dieser Abstürze treten mit einem vernünftigen Testverfahren auf, bevor sie den Benutzer überhaupt erreichen.

Das sind jedenfalls die Unterschiede zwischen GC und manueller Speicherverwaltung. Zur Beantwortung Ihrer unmittelbaren Frage würde ich sagen, dass die manuelle Speicherverwaltung schwierig ist, aber nur sehr wenig mit Lecks zu tun hat. Sowohl die GC- als auch die manuelle Speicherverwaltung sind nach wie vor sehr schwierig, wenn die Ressourcenverwaltung nicht trivial ist. Der GC hat hier wohl ein kniffligeres Verhalten, bei dem das Programm anscheinend einwandfrei funktioniert, aber immer mehr Ressourcen verbraucht. Die manuelle Form ist weniger knifflig, wird aber mit Fehlern wie dem oben gezeigten sehr schnell zum Absturz gebracht.


quelle
-1

Hier ist eine Liste der Probleme, mit denen C ++ - Programmierer beim Umgang mit Speicher konfrontiert sind:

  1. Das Gültigkeitsbereichsproblem tritt im Stapelspeicher auf, der zugewiesen wurde: Die Lebensdauer des Speichers erstreckt sich nicht über die Funktion hinaus, in der er zugewiesen wurde. Es gibt drei Hauptlösungen für dieses Problem: Heapspeicher und Verschieben des Zuweisungspunkts im Aufrufstapel nach oben oder Zuweisen aus Objekten heraus .
  2. Das Problem liegt in der Stapelzuweisung und der Zuweisung von in einem Objekt und einem Teil des Heapspeichers zugewiesenem Speicher: Die Größe des Speicherblocks kann sich zur Laufzeit nicht ändern. Lösungen sind Heapspeicher-Arrays, Zeiger sowie Bibliotheken und Container.
  3. Das Problem bei der Reihenfolge der Definitionen besteht darin, dass die Klassen innerhalb des Programms in der richtigen Reihenfolge angeordnet sein müssen. Lösungen beschränken Abhängigkeiten auf einen Baum, ordnen die Klassen neu an und verwenden keine Forward-Deklarationen, Zeiger und Heap-Speicher sowie Forward-Deklarationen.
  4. Das Inside-Outside-Problem befindet sich im Objektspeicher. Der Speicherzugriff innerhalb von Objekten ist in zwei Teile unterteilt, ein Teil des Speichers befindet sich innerhalb eines Objekts und ein anderer Teil außerhalb des Speichers, und Programmierer müssen auf der Grundlage dieser Entscheidung entweder die Komposition oder Referenzen korrekt auswählen. Lösungen treffen die Entscheidung richtig, oder Zeiger und Heapspeicher.
  5. Das Problem mit rekursiven Objekten liegt im dem Objekt zugewiesenen Speicher. Die Größe von Objekten wird unendlich, wenn dasselbe Objekt in sich selbst platziert wird und Lösungen Referenzen, Heapspeicher und Zeiger sind.
  6. Das Ownership-Tracking-Problem liegt im Heap-zugewiesenen Speicher vor. Der Zeiger, der die Adresse des Heap-zugewiesenen Speichers enthält, muss vom Zuweisungspunkt zum Freigabepunkt übergeben werden. Die Lösungen sind stapelzugeordneter Speicher, objektzugeordneter Speicher, auto_ptr, shared_ptr, unique_ptr und stdlib-Container.
  7. Das Problem mit der Duplizierung von Inhabern liegt im Heap-reservierten Speicher vor: Die Freigabe kann nur einmal erfolgen. Die Lösungen sind stapelzugeordneter Speicher, objektzugeordneter Speicher, auto_ptr, shared_ptr, unique_ptr und stdlib-Container.
  8. Das Nullzeigerproblem liegt im Heap-reservierten Speicher vor: Die Zeiger dürfen NULL sein, wodurch viele Operationen zur Laufzeit abstürzen. Lösungen sind Stapelspeicher, objektzugeordneter Speicher und sorgfältige Analyse von Heap-Bereichen und Referenzen.
  9. Das Problem mit dem Speicherverlust liegt im zugewiesenen Heap-Speicher vor: Vergessen Sie, delete für jeden zugewiesenen Speicherblock aufzurufen. Lösungen sind Werkzeuge wie valgrind.
  10. Das Stapelüberlaufproblem tritt bei rekursiven Funktionsaufrufen auf, die Stapelspeicher verwenden. Normalerweise wird die Größe des Stapels zur Kompilierungszeit vollständig bestimmt, mit Ausnahme von rekursiven Algorithmen. Das falsche Festlegen der Stapelgröße des Betriebssystems führt häufig zu diesem Problem, da die erforderliche Größe des Stapelspeichers nicht gemessen werden kann.

Wie Sie sehen, löst der Heap-Speicher sehr viele bestehende Probleme, verursacht jedoch zusätzliche Komplexität. GC wurde entwickelt, um einen Teil dieser Komplexität zu bewältigen. (Entschuldigung, wenn einige Problemnamen nicht die richtigen Namen für diese Probleme sind - manchmal ist es schwierig, den richtigen Namen zu finden)

tp1
quelle
1
-1: Keine Antwort auf die Frage.
Sjoerd