it-swarm.com.de

Wie funktioniert die Speicherbereinigung in Sprachen, die nativ kompiliert werden?

Nach dem Durchsuchen mehrerer Antworten und eines Stapelüberlaufs ist klar, dass einige nativ kompilierte Sprachen über eine Garbage Collection verfügen. Mir ist aber unklar, wie genau das funktionieren würde.

Ich verstehe, wie die Speicherbereinigung mit einer interpretierten Sprache funktionieren kann. Der Garbage Collector würde einfach neben dem Interpreter ausgeführt und nicht verwendete und nicht erreichbare Objekte aus dem Programmspeicher löschen. Sie rennen beide zusammen.

Wie würde dies mit kompilierten Sprachen funktionieren? Ich verstehe, dass, sobald der Compiler den Quellcode zum Zielcode kompiliert hat - speziell nativer Maschinencode - dies erledigt ist. Seine Arbeit ist beendet. Wie könnte das kompilierte Programm auch Müll gesammelt werden?

Arbeitet der Compiler in irgendeiner Weise mit der CPU, während das Programm ausgeführt wird, um "Müll" -Objekte zu löschen? Oder enthält der Compiler einen minimalen Garbage Collector in der ausführbaren Datei des kompilierten Programms?.

Ich glaube, meine letztere Aussage hätte aufgrund dieses Auszugs aus dieser Antwort auf Stack Overflow mehr Gültigkeit als die erstere:

Eine solche Programmiersprache ist Eiffel. Die meisten Eiffel-Compiler generieren aus Gründen der Portabilität C-Code. Dieser C-Code wird verwendet, um Maschinencode von einem Standard-C-Compiler zu erzeugen. Eiffel-Implementierungen bieten GC (und manchmal sogar genaue GC) für diesen kompilierten Code, und VM ist nicht erforderlich. Insbesondere hat der VisualEiffel-Compiler nativen x86-Maschinencode direkt mit vollständiger GC-Unterstützung generiert .

Die letzte Anweisung scheint zu implizieren, dass der Compiler ein Programm in die endgültige ausführbare Datei einfügt, das während der Ausführung des Programms als Garbage Collector fungiert.

Die Seite auf der Website der Sprache D über Garbage Collection , die nativ kompiliert ist und über einen optionalen Garbage Collector verfügt, scheint auch darauf hinzudeuten, dass einige Hintergrundprogramme neben dem zu implementierenden ursprünglichen ausführbaren Programm ausgeführt werden Müllabfuhr.

D ist eine Systemprogrammiersprache mit Unterstützung für die Speicherbereinigung. Normalerweise ist es nicht erforderlich, Speicher explizit freizugeben. Ordnen Sie es einfach nach Bedarf zu, und der Garbage Collector gibt regelmäßig den gesamten nicht verwendeten Speicher an den Pool des verfügbaren Speichers zurück.

Wenn genau die oben erwähnte Methode ist verwendet würde, wie genau würde es funktionieren? Speichert der Compiler eine Kopie eines Garbage Collection-Programms und fügt sie in jede von ihm generierte ausführbare Datei ein?

Oder bin ich in meinem Denken fehlerhaft? Wenn ja, welche Methoden werden zur Implementierung der Garbage Collection für kompilierte Sprachen verwendet und wie genau würden sie funktionieren?

80
Christian Dean

Die Speicherbereinigung in einer kompilierten Sprache funktioniert genauso wie in einer interpretierten Sprache. Sprachen wie Go verwenden Tracing-Garbage-Collectors, obwohl ihr Code normalerweise vorab zu Maschinencode kompiliert wird.

Die Speicherbereinigung (Ablaufverfolgung) beginnt normalerweise damit, die Aufrufstapel aller aktuell ausgeführten Threads zu durchsuchen. Objekte auf diesen Stapeln sind immer live. Danach durchläuft der Garbage Collector alle Objekte, auf die Live-Objekte zeigen, bis das gesamte Live-Objektdiagramm erkannt wird.

Es ist klar, dass hierfür zusätzliche Informationen erforderlich sind, die Sprachen wie C nicht bereitstellen. Insbesondere ist eine Zuordnung des Stapelrahmens jeder Funktion erforderlich, die die Offsets aller Zeiger (und wahrscheinlich ihrer Datentypen) sowie Zuordnungen aller Objektlayouts enthält, die dieselben Informationen enthalten.

Es ist jedoch leicht zu erkennen, dass Sprachen mit starken Typgarantien (z. B. wenn Zeigerumwandlungen auf verschiedene Datentypen nicht zulässig sind) diese Karten tatsächlich zur Kompilierungszeit berechnen können. Sie speichern einfach eine Zuordnung zwischen Befehlsadressen und Stapelrahmenzuordnungen und eine Zuordnung zwischen Datentypen und Objektlayoutzuordnungen in der Binärdatei. Diese Informationen ermöglichen es ihnen dann, den Objektgraphen zu durchlaufen.

Der Garbage Collector selbst ist nichts anderes als eine Bibliothek, die mit dem Programm verknüpft ist, ähnlich wie die C-Standardbibliothek. Diese Bibliothek könnte beispielsweise eine ähnliche Funktion wie malloc() bereitstellen, mit der der Erfassungsalgorithmus ausgeführt wird, wenn der Speicherdruck hoch ist.

54
avdgrinten

Speichert der Compiler eine Kopie eines Garbage Collection-Programms und fügt sie in jede von ihm generierte ausführbare Datei ein?

Es klingt unelegant und komisch, aber ja. Der Compiler verfügt über eine gesamte Dienstprogrammbibliothek, die viel mehr als nur Garbage Collection-Code enthält. Aufrufe dieser Bibliothek werden in jede von ihm erstellte ausführbare Datei eingefügt. Dies wird als Laufzeitbibliothek bezeichnet, und Sie wären überrascht, wie viele verschiedene Aufgaben normalerweise ausgeführt werden.

122
Kilian Foth

Oder enthält der Compiler einen minimalen Garbage Collector im Code des kompilierten Programms?.

Das ist eine seltsame Art zu sagen: "Der Compiler verknüpft das Programm mit einer Bibliothek, die die Speicherbereinigung durchführt." Aber ja, genau das passiert.

Dies ist nichts Besonderes: Compiler verknüpfen normalerweise Tonnen Bibliotheken mit den Programmen, die sie kompilieren. Andernfalls könnten kompilierte Programme nicht viel bewirken, ohne viele Dinge von Grund auf neu zu implementieren: Selbst das Schreiben von Text auf den Bildschirm/eine Datei/... erfordert eine Bibliothek.

Aber vielleicht unterscheidet sich GC von diesen anderen Bibliotheken, die explizite APIs bereitstellen, die der Benutzer aufruft?

Nein: In den meisten Sprachen arbeiten die Laufzeitbibliotheken über GC hinaus viel hinter den Kulissen ohne öffentlich zugängliche API. Betrachten Sie diese drei Beispiele:

  1. Ausnahmeverbreitung und Stapelabwicklungs-/Destruktoraufruf.
  2. Dynamische Speicherzuweisung (die normalerweise nicht nur eine Funktion aufruft, wie in C, auch wenn keine Speicherbereinigung vorhanden ist).
  3. Verfolgung dynamischer Typinformationen (für Casts usw.).

Eine Garbage Collection-Bibliothek ist also überhaupt nichts Besonderes, und a priori hat nichts damit zu tun, ob ein Programm vorab kompiliert wurde.

58
Konrad Rudolph

Wie würde dies mit kompilierten Sprachen funktionieren?

Ihr Wortlaut ist falsch. A Programmiersprache ist eine Spezifikation, die in einem technischen Bericht geschrieben wurde (ein gutes Beispiel finden Sie unter R5RS ). Eigentlich beziehen Sie sich auf eine spezifische Sprache Implementierung (die eine Software ist).

(Einige Programmiersprachen haben schlechte oder sogar fehlende Spezifikationen oder entsprechen genau einer Beispielimplementierung. Dennoch definiert eine Programmiersprache ein Verhalten - z. es hat ein Syntax und Semantik -, es ist nicht ein Softwareprodukt, könnte aber implementiert sein = von einem Softwareprodukt; viele Programmiersprachen haben mehrere Implementierungen; Insbesondere ist "kompiliert" ein Adjektiv, das für Implementierungen gilt - auch wenn einige Programmiersprachen von Interpreten einfacher implementiert werden als von Compilern.)

Ich verstehe, dass der Compiler, sobald er den Quellcode zum Zielcode kompiliert hat - insbesondere zum nativen Maschinencode - fertig ist. Seine Arbeit ist beendet.

Beachten Sie, dass Interpreter und Compiler eine lose Bedeutung haben und einige Sprachimplementierungen als beides betrachtet werden können. Mit anderen Worten, dazwischen liegt ein Kontinuum. Lesen Sie das neueste Dragon Book und denken Sie an Bytecode , JIT-Kompilierung , dynamisch C-Code ausgeben, der kompiliert wird in ein "Plugin" dann dlopen (3) - durch den gleichen Prozess (und auf aktuellen Maschinen ist dies schnell genug, um mit einem interaktiven --- kompatibel zu sein REPL , siehe this )


Ich empfehle dringend, das GC-Handbuch zu lesen. Zur Beantwortung wird ein ganzes Buch benötigt . Lesen Sie vorher die Wikipage Garbage Collection (die Sie vermutlich vor dem Lesen unten gelesen haben).

Das Laufzeitsystem der kompilierten Sprachimplementierung enthält den Garbage Collector, und der Compiler generiert Code, der fit für dieses bestimmte Laufzeitsystem ist. Insbesondere rufen Zuordnungsprimitive (die zu Maschinencode kompiliert wurden, der dies tut) das Laufzeitsystem auf (oder können dies auch tun).

Wie könnte das kompilierte Programm auch Müll gesammelt werden?

Nur durch Ausgabe von Maschinencode, der das Laufzeitsystem verwendet (und "freundlich" und "kompatibel mit" ist).

Beachten Sie, dass Sie mehrere Garbage Collection-Bibliotheken finden können, insbesondere Boehm GC , Ravenbrooks MPS oder sogar meine (nicht gepflegte) Qish . Und das Codieren eines einfach GC ist nicht sehr schwierig (das Debuggen ist jedoch schwieriger, und das Codieren eines wettbewerbsfähig GC ist schwierig =).

In einigen Fällen würde der Compiler einen konservativ GC verwenden (wie Boehm GC ). Dann gibt es nicht viel zu codieren. Der konservative GC würde (wenn der Compiler seine Zuordnungsroutine oder die gesamte GC-Routine aufruft) manchmal scan den gesamten Aufrufstapel und annehmen, dass jede Speicherzone (indirekt) ) vom Call Stack aus erreichbar ist live. Dies wird als konservativ GC bezeichnet, da Tippinformationen verloren gehen: Wenn eine Ganzzahl auf dem Aufrufstapel wie eine Adresse aussieht, wird sie befolgt usw.

In anderen (schwierigeren) Fällen bietet die Laufzeit eine Generationskopie-Garbage Collection (ein typisches Beispiel ist der Ocaml-Compiler, der Ocaml-Code zu Maschinencode kompiliert, indem er eine solche verwendet a GC). Dann besteht das Problem darin, genau auf dem Aufruf alle Zeiger zu stapeln, und einige von ihnen werden vom GC verschoben. Anschließend generiert der Compiler Metadaten, die Aufrufstapel-Frames beschreiben, die von der Laufzeit verwendet werden. Also werden die Aufrufkonventionen und ABI spezifisch für diese Implementierung (dh Compiler) & Laufzeitsystem.

In einigen Fällen ist vom Compiler generierter Maschinencode (tatsächlich sogar Schließungen darauf zeigend) selbst wird Müll gesammelt. Dies gilt insbesondere für SBCL (eine gute Common LISP-Implementierung), die Maschinencode für jede REPL Interaktion. Dies erfordert auch einige Metadaten, die den Code und die darin verwendeten Aufrufrahmen beschreiben.

Speichert der Compiler eine Kopie eines Garbage Collection-Programms und fügt sie in jede von ihm generierte ausführbare Datei ein?

Art von. Das Laufzeitsystem kann jedoch eine gemeinsam genutzte Bibliothek usw. sein. Manchmal (unter Linux und mehreren anderen POSIX-Systemen) kann es sogar ein Skriptinterpreter sein, z. übergeben an execve (2) mit einem Shebang . Oder ein ELF Dolmetscher, siehe Elf (5) und PT_INTERP, usw.

Übrigens sind die meisten Compiler für Sprache mit Garbage Collection (und deren Laufzeitsystem) heute freie Software . Laden Sie also den Quellcode herunter und studieren Sie ihn.

23

Es gibt bereits einige gute Antworten, aber ich möchte einige Missverständnisse hinter dieser Frage beseitigen.

Es gibt keine "nativ kompilierte Sprache" an sich. Zum Beispiel wurde der gleiche Java Code) auf meinem alten Telefon (Java Dalvik) interpretiert (und dann teilweise zur Laufzeit kompiliert) und ist (vorzeitig) kompiliert mein neues Telefon (ART).

Der Unterschied zwischen nativem und interpretiertem Code ist viel weniger streng als es scheint. Beide benötigen einige Laufzeitbibliotheken und ein Betriebssystem, um zu funktionieren (*). Der interpretierte Code benötigt einen Interpreter, aber der Interpreter ist nur ein Teil der Laufzeit. Aber auch dies ist nicht streng, da Sie den Interpreter durch einen (Just-in-Time-) Compiler ersetzen könnten. Für maximale Leistung möchten Sie möglicherweise beides (desktop Java Laufzeit enthält einen Interpreter und zwei Compiler).

Unabhängig davon, wie der Code ausgeführt wird, sollte er sich gleich verhalten. Das Zuweisen und Freigeben von Speicher ist eine Aufgabe für die Laufzeit (genau wie das Öffnen von Dateien, das Starten von Threads usw.). In Ihrer Sprache schreiben Sie einfach new X() oder ähnlich. Die Sprachspezifikation sagt, was passieren soll und die Laufzeit macht es.

Es wird etwas freier Speicher zugewiesen, der Konstruktor wird aufgerufen usw. Wenn nicht genügend Speicher vorhanden ist, wird der Garbage Collector aufgerufen. Da Sie sich bereits in der Laufzeit befinden, bei der es sich um einen nativen Code handelt, spielt die Existenz eines Interpreters keine Rolle.

Es gibt wirklich keine direkte Verbindung zwischen Code-Interpretation und Garbage Collection. Es ist nur so, dass Low-Level-Sprachen wie C auf Geschwindigkeit und fein abgestimmte Kontrolle über alles ausgelegt sind, was nicht gut zu der Idee von nicht nativem Code oder einem Garbage Collector passt. Es gibt also nur eine Korrelation.

Dies war in den alten Zeiten sehr wahr, wo z. Der Java Interpreter war sehr langsam und der Garbage Collector ziemlich ineffizient. Heutzutage sind die Dinge ganz anders und das Sprechen über eine interpretierte Sprache hat jeglichen Sinn verloren.


(*) Zumindest wenn es um Allzweckcode geht, lassen Sie Bootloader und ähnliches beiseite.

6
maaartinus

Die Details variieren zwischen den Implementierungen, aber es ist im Allgemeinen eine Kombination der folgenden:

  • Eine Laufzeitbibliothek mit einem GC. Dies übernimmt die Speicherzuweisung und verfügt über einige andere Einstiegspunkte, einschließlich einer "GC_now" -Funktion.
  • Der Compiler erstellt Tabellen für den GC, damit er weiß, welche Felder in welchen Datentypen Referenzen sind. Dies wird auch für die Stapelrahmen für jede Funktion durchgeführt, damit der GC vom Stapel aus verfolgen kann.
  • Wenn der GC inkrementell ist (die GC-Aktivität ist mit dem Programm verschachtelt) oder gleichzeitig (wird in einem separaten Thread ausgeführt), enthält der Compiler auch speziellen Objektcode, um die GC-Datenstrukturen zu aktualisieren, wenn Referenzen aktualisiert werden. Die beiden haben ähnliche Probleme hinsichtlich der Datenkonsistenz.

Bei der inkrementellen und gleichzeitigen GC müssen der kompilierte Code und die GC zusammenarbeiten, um einige Invarianten beizubehalten. In einem Kopiersammler kopiert der GC beispielsweise Live-Daten von Raum A nach Raum B und hinterlässt den Müll. Für den nächsten Zyklus werden A und B umgedreht und wiederholt. Eine Regel kann also sein, sicherzustellen, dass jedes Mal, wenn das Benutzerprogramm versucht, auf ein Objekt in Raum A zu verweisen, dies erkannt wird und das Objekt sofort in Raum B kopiert wird, wo das Programm weiterhin darauf zugreifen kann. Eine Weiterleitungsadresse wird in Feld A belassen, um dem GC anzuzeigen, dass dies geschehen ist, sodass alle anderen Verweise auf das Objekt aktualisiert werden, sobald sie verfolgt werden. Dies ist als "Lesesperre" bekannt.

GC-Algorithmen wurden seit den 60er Jahren untersucht, und es gibt umfangreiche Literatur zu diesem Thema. Google, wenn Sie weitere Informationen wünschen.

3
Paul Johnson