Ich bin gespannt, warum Haskell-Implementierungen einen GC verwenden.
Ich kann mir keinen Fall vorstellen, in dem GC in einer reinen Sprache notwendig wäre. Ist es nur eine Optimierung, um das Kopieren zu reduzieren, oder ist es tatsächlich notwendig?
Ich suche nach Beispielcode, der auslaufen würde, wenn kein GC vorhanden wäre.
haskell
garbage-collection
Pubby
quelle
quelle
Antworten:
Wie andere bereits betont haben, erfordert Haskell eine automatische , dynamische Speicherverwaltung: Eine automatische Speicherverwaltung ist erforderlich, da die manuelle Speicherverwaltung nicht sicher ist. Eine dynamische Speicherverwaltung ist erforderlich, da bei einigen Programmen die Lebensdauer eines Objekts nur zur Laufzeit bestimmt werden kann.
Betrachten Sie beispielsweise das folgende Programm:
In diesem Programm muss die Liste
[1..1000]
gespeichert bleiben, bis der Benutzer "clear" eingibt. Daher muss die Lebensdauer dynamisch bestimmt werden. Aus diesem Grund ist eine dynamische Speicherverwaltung erforderlich.In diesem Sinne ist eine automatisierte dynamische Speicherzuweisung erforderlich. In der Praxis bedeutet dies: Ja , Haskell benötigt einen Garbage Collector, da die Garbage Collection der leistungsstärkste automatische dynamische Speichermanager ist.
Jedoch...
Obwohl ein Garbage Collector erforderlich ist, versuchen wir möglicherweise, einige Sonderfälle zu finden, in denen der Compiler ein billigeres Speicherverwaltungsschema als die Garbage Collection verwenden kann. Zum Beispiel gegeben
Wir können hoffen, dass der Compiler
x2
erkennt, dass die Zuordnung bei derf
Rückgabe sicher aufgehoben werden kann (anstatt darauf zu warten, dass der Garbage Collector die Zuordnung aufhebtx2
). Im Wesentlichen bitten wir den Compiler, eine Escape-Analyse durchzuführen , um Zuweisungen in Zuordnungen auf dem Stapel zu konvertieren , wo immer dies möglich ist.Dies ist nicht zu unangemessen, um danach zu fragen: Der jhc-Hashkell-Compiler tut dies, GHC jedoch nicht. Simon Marlow sagt, dass der Garbage Collector von GHC die Fluchtanalyse größtenteils unnötig macht.
jhc verwendet tatsächlich eine ausgeklügelte Form der Fluchtanalyse, die als Regionsinferenz bekannt ist . Erwägen
In diesem Fall würde eine vereinfachte Escape-Analyse ergeben, dass Escape
x2
vonf
(weil es im Tupel zurückgegeben wird) und daherx2
auf dem durch Müll gesammelten Heap zugewiesen werden muss. Die Regionsinferenz hingegen kann erkennen, dassx2
die Zuordnung bei derg
Rückkehr aufgehoben werden kann . Die Idee hier ist, dassx2
die Zuordnung eher ing
der Region als inf
der Region erfolgen sollte.Jenseits von Haskell
Während die Inferenz von Regionen in bestimmten Fällen hilfreich ist, wie oben erläutert, scheint es schwierig zu sein, sie effektiv mit einer verzögerten Bewertung in Einklang zu bringen (siehe die Kommentare von Edward Kmett und Simon Peyton Jones ). Betrachten Sie zum Beispiel
Man könnte versucht sein, die Liste
[1..n]
auf dem Stapel zuzuweisen und sie nach derf
Rückgabe freizugeben , aber dies wäre katastrophal: Sie würde sichf
von der Verwendung von O (1) -Speicher (unter Garbage Collection) zu O (n) -Speicher ändern .In den 1990er und frühen 2000er Jahren wurden umfangreiche Arbeiten zur regionalen Inferenz für die strenge funktionale Sprache ML durchgeführt. Mads Tofte, Lars Birkedal, Martin Elsman und Niels Hallenberg haben eine gut lesbare Retrospektive über ihre Arbeit zur Regionsinferenz geschrieben , von der sie einen Großteil in den MLKit-Compiler integriert haben . Sie experimentierten mit einer rein region-basierten Speicherverwaltung (dh ohne Garbage Collector) sowie einer hybriden region-basierten / Garbage-Collected-Speicherverwaltung und berichteten, dass ihre Testprogramme "zwischen 10-mal schneller und 4-mal langsamer" liefen als reiner Garbage- gesammelte Versionen.
quelle
Nothing
) an den rekursiven Aufruf von übergebenloop
und die alte freigeben - keine unbekannte Lebensdauer. Natürlich möchte niemand eine nicht gemeinsam genutzte Implementierung von Haskell, da dies für große Datenstrukturen schrecklich langsam ist.Nehmen wir ein triviales Beispiel. Angesichts dessen
Sie müssen das Paar
(x, y)
irgendwo zuweisen, bevor Sie anrufenf
. Wann können Sie das Paar freigeben? Sie haben keine Ahnung. Es kann nicht freigegeben werden, wennf
zurückgegeben wird, daf
das Paar möglicherweise in eine Datenstruktur (z. B.f p = [p]
) eingefügt wurde , sodass die Lebensdauer des Paares möglicherweise länger sein muss als die Rückgabe vonf
. Angenommen, das Paar wurde in eine Liste aufgenommen. Kann jemand, der die Liste auseinander nimmt, das Paar freigeben? Nein, da das Paar möglicherweise gemeinsam genutzt wird (zlet p = (x, y) in (f p, p)
. B. ). Es ist also sehr schwer zu sagen, wann das Paar freigegeben werden kann.Gleiches gilt für fast alle Zuteilungen in Haskell. Es ist jedoch möglich, eine Analyse (Regionsanalyse) durchzuführen, die eine Obergrenze für die Lebensdauer angibt. Dies funktioniert ziemlich gut in strengen Sprachen, aber weniger in faulen Sprachen (faule Sprachen neigen dazu, in der Implementierung viel mehr Mutationen zu bewirken als strenge Sprachen).
Also möchte ich die Frage umdrehen. Warum braucht Haskell Ihrer Meinung nach keine GC? Wie würden Sie die Speicherzuweisung vorschlagen?
quelle
Ihre Intuition, dass dies etwas mit Reinheit zu tun hat, hat etwas Wahres.
Haskell wird teilweise als rein angesehen, da Nebenwirkungen von Funktionen in der Typensignatur berücksichtigt werden. Wenn eine Funktion den Nebeneffekt hat, etwas zu drucken, muss sich
IO
irgendwo in ihrem Rückgabetyp eine befinden.Es gibt jedoch eine Funktion, die implizit überall in Haskell verwendet wird und deren Typensignatur in gewisser Weise keinen Nebeneffekt berücksichtigt. Nämlich die Funktion, die einige Daten kopiert und Ihnen zwei Versionen zurückgibt. Unter der Haube kann dies entweder buchstäblich funktionieren, indem die Daten im Speicher dupliziert werden, oder "virtuell", indem eine Schuld erhöht wird, die später zurückgezahlt werden muss.
Es ist möglich, Sprachen mit noch restriktiveren Typsystemen (rein "lineare") zu entwerfen, die die Kopierfunktion nicht zulassen. Aus der Sicht eines Programmierers in einer solchen Sprache sieht Haskell etwas unrein aus.
Tatsächlich hat Clean , ein Verwandter von Haskell, lineare (genauer: eindeutige) Typen, und das kann eine Vorstellung davon geben, wie es wäre, das Kopieren zu verbieten. Clean ermöglicht jedoch weiterhin das Kopieren für "nicht eindeutige" Typen.
In diesem Bereich gibt es viele Recherchen. Wenn Sie genug googeln, finden Sie Beispiele für reinen linearen Code, für den keine Speicherbereinigung erforderlich ist. Sie finden alle Arten von Typsystemen, die dem Compiler signalisieren können, welcher Speicher verwendet werden kann, sodass der Compiler einen Teil des GC eliminieren kann.
In gewisser Weise sind Quantenalgorithmen auch rein linear. Jeder Vorgang ist umkehrbar, sodass keine Daten erstellt, kopiert oder zerstört werden können. (Sie sind auch linear im üblichen mathematischen Sinne.)
Es ist auch interessant, mit Forth (oder anderen stapelbasierten Sprachen) zu vergleichen, die explizite DUP-Operationen haben, die deutlich machen, wann eine Duplizierung stattfindet.
Eine andere (abstraktere) Art, darüber nachzudenken, besteht darin, festzustellen, dass Haskell aus einer einfach getippten Lambda-Rechnung aufgebaut ist, die auf der Theorie der kartesischen geschlossenen Kategorien basiert, und dass solche Kategorien mit einer diagonalen Funktion ausgestattet sind
diag :: X -> (X, X)
. Eine Sprache, die auf einer anderen Klassenklasse basiert, hat möglicherweise keine solche Sprache.Im Allgemeinen ist eine rein lineare Programmierung jedoch zu schwierig, um nützlich zu sein. Deshalb entscheiden wir uns für GC.
quelle
Die auf Haskell angewendeten Standardimplementierungstechniken erfordern tatsächlich mehr GC als die meisten anderen Sprachen, da sie niemals vorherige Werte mutieren, sondern neue, modifizierte Werte basierend auf den vorherigen erstellen. Da dies bedeutet, dass das Programm ständig mehr Speicher reserviert und verwendet, wird eine große Anzahl der Werte im Laufe der Zeit verworfen.
Aus diesem Grund weisen GHC-Programme in der Regel so hohe Gesamtzuweisungszahlen (von Gigabyte bis Terabyte) auf: Sie weisen ständig Speicher zu, und nur dank des effizienten GC können sie ihn vor dem Auslaufen zurückfordern.
quelle
Wenn Sie mit einer Sprache (einer beliebigen Sprache) Objekte dynamisch zuweisen können, gibt es drei praktische Möglichkeiten, um mit der Speicherverwaltung umzugehen:
Mit der Sprache können Sie nur Speicher auf dem Stapel oder beim Start zuweisen. Diese Einschränkungen schränken jedoch die Arten von Berechnungen, die ein Programm ausführen kann, stark ein. (In der Praxis. Theoretisch können Sie dynamische Datenstrukturen in (sagen wir) Fortran emulieren, indem Sie sie in einem großen Array darstellen. Es ist SCHRECKLICH ... und für diese Diskussion nicht relevant.)
Die Sprache kann einen expliziten
free
oderdispose
Mechanismus bereitstellen . Dies hängt jedoch vom Programmierer ab, um es richtig zu machen. Jeder Fehler in der Speicherverwaltung kann zu einem Speicherverlust führen ... oder schlimmer.Die Sprache (oder genauer gesagt die Sprachimplementierung) kann einen automatischen Speichermanager für den dynamisch zugewiesenen Speicher bereitstellen. dh irgendeine Form von Müllsammler.
Die einzige andere Möglichkeit besteht darin, niemals dynamisch zugewiesenen Speicher zurückzugewinnen. Dies ist keine praktische Lösung, außer für kleine Programme, die kleine Berechnungen durchführen.
Wenn Sie dies auf Haskell anwenden, hat die Sprache nicht die Einschränkung von 1. Es gibt keine manuelle Freigabeoperation gemäß 2. Um für nicht triviale Dinge verwendet werden zu können, muss eine Haskell-Implementierung einen Garbage Collector enthalten .
Vermutlich meinst du eine reine funktionale Sprache.
Die Antwort ist, dass ein GC unter der Haube erforderlich ist, um die Heap-Objekte zurückzugewinnen, die die Sprache erstellen muss. Beispielsweise.
Eine reine Funktion muss Heap-Objekte erstellen, da sie in einigen Fällen zurückgegeben werden muss. Das bedeutet, dass sie nicht auf dem Stapel zugeordnet werden können.
Die Tatsache, dass es Zyklen geben kann (die sich beispielsweise aus einem ergeben
let rec
), bedeutet, dass ein Referenzzählungsansatz für Heap-Objekte nicht funktioniert.Dann gibt es Funktionsabschlüsse ... die auch nicht auf dem Stapel zugeordnet werden können, da sie eine Lebensdauer haben, die (normalerweise) unabhängig von dem Stapelrahmen ist, in dem sie erstellt wurden.
Nahezu jedes Beispiel, bei dem es um Schließungen oder grafische Datenstrukturen ging, würde unter diesen Bedingungen auslaufen.
quelle
Ein Garbage Collector ist niemals erforderlich, sofern Sie über ausreichend Speicher verfügen. In Wirklichkeit haben wir jedoch kein unendliches Gedächtnis, und daher benötigen wir eine Methode, um nicht mehr benötigtes Gedächtnis zurückzugewinnen. In unreinen Sprachen wie C können Sie explizit angeben, dass Sie mit etwas Speicher fertig sind, um ihn freizugeben. Dies ist jedoch eine mutierende Operation (der gerade freigegebene Speicher ist nicht mehr sicher zu lesen), sodass Sie diesen Ansatz nicht verwenden können eine reine Sprache. Es ist also entweder statisch zu analysieren, wo Sie den Speicher freigeben können (im allgemeinen Fall wahrscheinlich unmöglich), Speicher wie ein Sieb zu verlieren (funktioniert hervorragend, bis Sie leer sind) oder einen GC zu verwenden.
quelle
GC ist in reinen FP-Sprachen ein "Muss". Warum? Operations Allokation und Free sind unrein! Und der zweite Grund ist, dass unveränderliche rekursive Datenstrukturen GC für ihre Existenz benötigen, da durch die Verknüpfung abstruse und nicht wartbare Strukturen für den menschlichen Geist entstehen. Backlinking ist natürlich ein Segen, denn das Kopieren von Strukturen, die es verwenden, ist sehr billig.
Wie auch immer, wenn Sie mir nicht glauben, versuchen Sie einfach, die FP-Sprache zu implementieren, und Sie werden sehen, dass ich Recht habe.
EDIT: Ich habe es vergessen. Faulheit ist HÖLLE ohne GC. Glaubst du mir nicht? Versuchen Sie es einfach ohne GC, zum Beispiel in C ++. Sie werden ... Dinge sehen
quelle
Haskell ist eine nicht strenge Programmiersprache, aber die meisten Implementierungen verwenden Call-by-Need (Faulheit), um Nicht-Strenge zu implementieren. In Call-by-Need bewerten Sie Inhalte nur dann, wenn sie zur Laufzeit mithilfe der Maschinerie von "Thunks" erreicht werden (Ausdrücke, die darauf warten, ausgewertet zu werden, und sich dann selbst überschreiben und sichtbar bleiben, damit ihr Wert bei Bedarf wiederverwendet werden kann).
Wenn Sie Ihre Sprache also träge mit Thunks implementieren, haben Sie alle Überlegungen zur Objektlebensdauer bis zum letzten Moment, der Laufzeit, verschoben. Da Sie jetzt nichts über Lebenszeiten wissen, können Sie vernünftigerweise nur Müll sammeln ...
quelle