Einführung in die Systemprogrammierung Prof. Dr. Christoph Reichenbach Fachbereich 12 / Institut für Informatik 9. Juli 2015 Übersetzer, Interpreter, Dynamischer Übersetzer I Verschiedene Modi der Programmausführung: I I I I Übersetzer Interpreter Dynamischer Übersetzer Kernunterschied: I I Wird Programm in Maschinencode übersetzt? Falls ja, wann? Interpreter Vor Ausführung Frontend Zwischenform (z.B. CPython, Ruby (ohne JRuby), Perl, PHP) Interpreter Vor Ausführung Während Ausführung Frontend Interpretierung Zwischenform (z.B. CPython, Ruby (ohne JRuby), Perl, PHP) Interpreter Interpreter Vor Ausführung Während Ausführung Frontend Interpretierung Zwischenform Interpreter CPU (z.B. CPython, Ruby (ohne JRuby), Perl, PHP) Keine Übersetzung in Maschinencode Übersetzer Vor Ausführung Frontend+Middle-End Zwischenform Backend (z.B. C, C++, Eiffel, Modula-3, FORTRAN, SML, Haskell) Übersetzer Vor Ausführung Während Ausführung Frontend+Middle-End Zwischenform Backend Maschinencode (z.B. C, C++, Eiffel, Modula-3, FORTRAN, SML, Haskell) Übersetzer Vor Ausführung Während Ausführung Frontend+Middle-End Zwischenform Backend Maschinencode CPU (z.B. C, C++, Eiffel, Modula-3, FORTRAN, SML, Haskell) Übersetzung in Maschinencode vor Ausführung Vergleich: Übersetzung vs Interpretierung Eigenschaft Konstruktion Laufzeit-Performanz Programmausführung Sprachmächtigkeit ?) Übersetzeroptimierungen Interpreter einfach langsam direkt hoch Flexibilität Übersetzer schwer schnell übersetzen & binden beschränkt? Dynamische Übersetzung I I I Auch: JIT (Just-In-Time)-Übersetzung Idee: Code-Übersetzung während Programmausführung Theorie: Verbindet Vorteile von Interpreter und Übersetzer Dynamische Übersetzung I I I I Auch: JIT (Just-In-Time)-Übersetzung Idee: Code-Übersetzung während Programmausführung Theorie: Verbindet Vorteile von Interpreter und Übersetzer Praxis: I I I Sehr schwer zu bauen Speicherbedarf steigt Kann bessere Performanz als traditioneller Übersetzung erreichen Dynamischer Übersetzer Vor Ausführung Zwischenform Frontend+Middle-End (z.B. Java, Scala, AttoL, .NET, JavaScript) Dynamischer Übersetzer Vor Ausführung Während Ausführung Zwischenform Frontend+Middle-End (optional) Middle-End & Backend Interpretierung Maschinencode Interpreter (z.B. Java, Scala, AttoL, .NET, JavaScript) Dynamischer Übersetzer Vor Ausführung Während Ausführung Zwischenform Frontend+Middle-End (optional) Middle-End & Backend Interpretierung Maschinencode Interpreter CPU (z.B. Java, Scala, AttoL, .NET, JavaScript) Übersetzung in Maschinencode währen Ausführung Dynamischer Übersetzer Vor Ausführung Während Ausführung Bytecode Zwischenform Frontend+Middle-End (optional) Middle-End & Backend Interpretierung Maschinencode Interpreter CPU (z.B. Java, Scala, AttoL, .NET, JavaScript) Übersetzung in Maschinencode währen Ausführung Zusammenfassung I Sprachimplementierung per: I I I Separatem Übersetzer Interpreter Dynamischem Übersetzer I I Abwägung zwischen: I I I Kann Interpreter beinhalten Mächtigkeit der Sprache Ausführungszeit / Speicherbedarf Sprachen können verschiedentlich implementiert werden I Beispiel: CPython vs. Jython Bytecode als Zwischensprache Historischer Hintegrund: I I I Entwicklung von Pascal-Übersetzern Portierung von Übersetzern von Architektur A nach B zeitaufwändig, fehleranfällig Idee:1 I I I I 1 Wir schreiben eine Übersetzerimplementierung, in Pascal Zielarchitektur P, eine Stapelmaschine (nicht in Hardware) P-Code: möglichst einfach gehalten Interpreter für P-Code: Einfach zu schreiben Nori, Ammmann, Jensen, Nägeli: “The Pascal P Compiler: Implementation notes”, ETH Zürich, Dezember 1974 Bytecode als Zwischensprache Historischer Hintegrund: I I I Entwicklung von Pascal-Übersetzern Portierung von Übersetzern von Architektur A nach B zeitaufwändig, fehleranfällig Idee:1 I I I I I 1 Wir schreiben eine Übersetzerimplementierung, in Pascal Zielarchitektur P, eine Stapelmaschine (nicht in Hardware) P-Code: möglichst einfach gehalten Interpreter für P-Code: Einfach zu schreiben Übersetzen den Übersetzer nach P-Code: ⇒ Ausführen des Übersetzers benötigt nur P-Code-Interpreter Nori, Ammmann, Jensen, Nägeli: “The Pascal P Compiler: Implementation notes”, ETH Zürich, Dezember 1974 Bytecode heute I I P-Code ist erstes Beispiel für Bytecode In unseren Beispielsprachen verwendet von: I I Python Java Bytecode heute I I P-Code ist erstes Beispiel für Bytecode In unseren Beispielsprachen verwendet von: I I I I Python Java Interpretierung ineffizient (Faktor 15-20 langsamer als übersetzer Code) Stattdessen: Übersetzung P-Code nach Maschinencode Bytecode: Vorteile und Nachteile Vorteile I I Obfuszierung trotz Portabilität Übersetzung Bytecode vs. Quellcode: I I I I Nachteile I I I I Sparen uns Parsen, Namensanalyse Einfache Interpretierung Software-Werkzeuge können einfache Programmrepräsentierung zur Analyse nutzen Oberflächensyntax kann geändert werden Aufwand Bytecode-Schnittstelle: I I I Spezifizierung Backend Frontend Speicherrepräsentierung Bytecode-Eingabe, Ausgabe implementieren Versionskompatibilität Andere Formen von Bytecode I Dalvik-Bytecode I I I Common Intermediate Language (CIL) I I I Verwendet auf Android Registertransfercode: Ähnelt realistischem Maschinencode Verwendet von Microsoft .NET Ähnlich Java-Bytecode LLVM-Bitcode I I ... Verwendet vom LLVM-Übersetzer Komplexer Code mit Eigenschaften einer Hochsprache Zusammenfassung: Bytecode I I I I I Bytecode repräsentiert Zwischenzustand des Übersetzers Meist weniger komplex als Hochsprache, Maschinensprache Plattformunabhangig Erleichtert Portierung von Programmen Höhere Implementierungskomplexität Starke Typisierung Früh-Entdeckung der folgenden Typprobleme: I I I Falscher Speicherbereich Falscher Typ Array-Grenzen überschritten Vermeidung: Typfehler „Falscher Speicherbereich“ I I I Stark typisierte Sprachen haben keinen Zeigertyp Stattdessen: Referenzen Typvertrag für Referenzen: Referenz zeigt immer auf: I I NULL-Adresse, oder Anfang eines Objektes In stark typisierten Sprachen verboten: void qsort (int *daten , int len) { ... qsort (daten , links ); qsort ( daten + links + 1, // Zeigerarithmetik len - links - 1); } Vermeidung: Typfehler „Falscher Speicherbereich“ I I I Stark typisierte Sprachen haben keinen Zeigertyp Stattdessen: Referenzen Typvertrag für Referenzen: Referenz zeigt immer auf: I I NULL-Adresse, oder Anfang eines Objektes In stark typisierten Sprachen verboten: void qsort (int *daten , int len) { ... qsort (daten , links ); qsort ( daten + links + 1, // Zeigerarithmetik len - links - 1); } Ohne Zeigertypen keine Zeigerarithmetik Vermeidung: Typfehler „Falscher Typ“ (1/2) I Alle Operationen sind an bestimmte Typen gebunden I I I Explizite Typhierarchie (Java, C++) Keine Typhierarchie: Typ definiert sich ‘implizit’ durch Eigenschaften (Python, AttoL) Bei jeder Operation: Typprüfung I I Im Übersetzer (statisch) Zur Laufzeit (dynamisch) AttoL obj a = [1, 2, 3]; // Array a := a + 1; // Fehler: Arithmetik auf Array Vermeidung: Typfehler „Falscher Typ“ (1/2) I Alle Operationen sind an bestimmte Typen gebunden I I I Explizite Typhierarchie (Java, C++) Keine Typhierarchie: Typ definiert sich ‘implizit’ durch Eigenschaften (Python, AttoL) Bei jeder Operation: Typprüfung I I Im Übersetzer (statisch) Zur Laufzeit (dynamisch) AttoL obj a = [1, 2, 3]; // Array a := a + 1; // Fehler: Arithmetik auf Array Nutzt Laufzeit-Typinformationen Vermeidung: Typfehler „Falscher Typ“ (2/2) AttoL $a0 obj a = ... ... a := a + 1; ⇒ Typdeskriptor Wert ld beqz ld li beq $a0, $a0, $t0, $v0, $t0, li li li jalr $a0, 0x1c76cb0 $a1, 0x4276e8 $v0, 0x41a77e $v0 ld li add $a0, 8($a0) $a1, 0x1 $a0, $a1 0($gp) fail 0($a0) 0x62f3c0 $v0, succeed ; lade ‘a’ ; Typ ‘int’ fail: succeed: ; fail_at_node Vermeidung: Typfehler „Falscher Typ“ (2/2) AttoL $a0 obj a = ... ... a := a + 1; ⇒ Typdeskriptor Wert ld beqz ld li beq $a0, $a0, $t0, $v0, $t0, li li li jalr $a0, 0x1c76cb0 $a1, 0x4276e8 $v0, 0x41a77e $v0 ld li add $a0, 8($a0) $a1, 0x1 $a0, $a1 0($gp) fail 0($a0) 0x62f3c0 $v0, succeed ; lade ‘a’ ; Typ ‘int’ fail: succeed: ; fail_at_node Vermeidung: Typfehler „Falscher Typ“ (2/2) AttoL $a0 obj a = ... ... a := a + 1; ⇒ ⇒ ⇒ fail: Typdeskriptor Wert ld beqz ld li beq $a0, $a0, $t0, $v0, $t0, li li li jalr $a0, 0x1c76cb0 $a1, 0x4276e8 $v0, 0x41a77e $v0 ld li add $a0, 8($a0) $a1, 0x1 $a0, $a1 0($gp) fail 0($a0) 0x62f3c0 $v0, succeed succeed: ; lade ‘a’ ; Typ ‘int’ ; fail_at_node Vermeidung: Typfehler „Falscher Typ“ (2/2) AttoL $a0 obj a = ... ... a := a + 1; fail: ⇒ ⇒ ⇒ ⇒ succeed: Typdeskriptor Wert ld beqz ld li beq $a0, $a0, $t0, $v0, $t0, li li li jalr $a0, 0x1c76cb0 $a1, 0x4276e8 $v0, 0x41a77e $v0 ld li add $a0, 8($a0) $a1, 0x1 $a0, $a1 0($gp) fail 0($a0) 0x62f3c0 $v0, succeed ; lade ‘a’ ; Typ ‘int’ ; fail_at_node Vermeidung: Typfehler „Falscher Typ“ (2/2) AttoL $a0 obj a = ... ... a := a + 1; Typdeskriptor $a0 + 8 ld beqz ld li beq $a0, $a0, $t0, $v0, $t0, li li li jalr $a0, 0x1c76cb0 $a1, 0x4276e8 $v0, 0x41a77e $v0 ld li add $a0, 8($a0) $a1, 0x1 $a0, $a1 0($gp) fail 0($a0) 0x62f3c0 $v0, succeed Wert ; lade ‘a’ ; Typ ‘int’ fail: succeed: ⇒ ⇒ ⇒ ; fail_at_node Vermeidung: Typfehler „Arraygrenzen überschritten“ (1/2) Arrayschrankenprüfung: I Jeder Arrayzugriff wird geprüft, bevor er erlaubt wird Java int[] array = ...; array[x] += 1; wird vom Laufzeitsystem transformiert: Java int[] array = ...; if (x < 0 || x >= array.length) throw new ArrayOutOfBoundsException(); else array[x] += 1; Vermeidung: Typfehler „Arraygrenzen überschritten“ (2/2) Array-Repräsentierung in AttoVM (ähnlich in Java): Typdeskriptor Größe Element #0 Element #1 ... Vermeidung: Typfehler „Arraygrenzen überschritten“ (2/2) Array-Repräsentierung in AttoVM (ähnlich in Java): Objektreferenz Typdeskriptor Größe Element #0 Element #1 ... Zusammenfassung: Starke Typisierung I Starke Typisierung erzwingt Typverträge I I Typprüfungen können stattfinden: I I I Garantierte Erkennung von Typfehlern statisch: Übersetzungszeit dynamisch: Laufzeit Vermeidung von Fehlern: I Falscher Speicherbereich: I I Falscher Typ: I I durch Verbot von Zeigerarithmetik/Zeigertypen durch Typprüfung (statisch oder dynamisch) Arraygrenzen überschritten: I durch Arrayschrankenprüfung (meist dynamisch) Manuelle Speicherverwaltung I I Speicher explizit freizugeben (free) ist fehleranfällig Keine bekannte einfache Methode, Fehler im Allgemeinfall zu finden Können wir free automatisieren? Automatische Speicherverwaltung I I Programm darf Speicher allozieren Laufzeitsystem dealloziert Speicher automatisch: I I Speicher ist erreichbar gdw das Programm in der Zukunft darauf zugreifen könnte Sobald Speicher nicht mehr erreichbar ist, wird er dealloziert (Speicherbereinigung) Automatische Speicherverwaltung I I Programm darf Speicher allozieren Laufzeitsystem dealloziert Speicher automatisch: I I Speicher ist erreichbar gdw das Programm in der Zukunft darauf zugreifen könnte Sobald Speicher nicht mehr erreichbar ist, wird er dealloziert (Speicherbereinigung) Wie erkennt das Laufzeitsystem, welcher Speicher erreichbar ist? Erkennung von erreichbarem Speicher I Immer erreichbar: I I I I Registerinhalte Lokale Variablen und Parameter auf dem Stapel Globale Variablen (.static) Der Rest ist nicht mehr erreichbar Erkennung von erreichbarem Speicher I Immer erreichbar: I I I I I Registerinhalte Lokale Variablen und Parameter auf dem Stapel Globale Variablen (.static) ...und transitiver Abschluß aller erreichbaren Objekte! Der Rest ist nicht mehr erreichbar Automatische Speicherbereinigung I Vorteile: I I I Nachteile: I I I I (Fast) keine Speicherfehler mehr Code wird kompakter Komplexeres Laufzeitsystem nötig Automatische Deallozierung kann ineffizienter sein Keine Zeigerarithmetik mehr erlaubt Ansätze: 1. 2. Referenzzählen (reference counting) Verfolgung (tracing) Referenzzählen (1/3) (reference counting ) (z.B. Python) I Jedes Objekt weiß, wie oft es referenziert wird: In Referenzzähler gespeichert Referenzzählen (1/3) (reference counting ) (z.B. Python) I I Jedes Objekt weiß, wie oft es referenziert wird: In Referenzzähler gespeichert Neuer Zeiger auf Objekt: 1. Referenzzähler++ Referenzzählen (1/3) (reference counting ) (z.B. Python) I I Jedes Objekt weiß, wie oft es referenziert wird: In Referenzzähler gespeichert Neuer Zeiger auf Objekt: 1. I Referenzzähler++ Zeiger zeigt nicht mehr auf Objekt: 1. 2. Referenzzähler-Wenn Referenzzähler == 0: Deallozieren Referenzzählen (2/3) Python ⇒ a b a b = = = = Obj() a None None Referenzzählen (2/3) Python a ⇒ b a b = = = = Obj() a None None a Typdeskriptor Ref: 1 ... Referenzzählen (2/3) Python a b ⇒ a b = = = = Obj() a None None a Typdeskriptor b Ref: 2 ... Referenzzählen (2/3) Python a b a ⇒ b = = = = Obj() a None None a Typdeskriptor b Ref: 1 ... Referenzzählen (2/3) Python a b a b ⇒ = = = = Obj() a None None a Typdeskriptor b Ref: 0 ... Referenzzählen (2/3) Python a b a b = = = = Obj() a None None a Typdeskriptor b Ref: 0 ... Problem #1: Zusätzliche Speicherzugriffe bei Kopie von Referenzen Referenzzählen (3/3) Python ⇒ a = b = a.x b.x a = b = Obj() Obj() = b = a None None Referenzzählen (3/3) a Python a = ⇒ b = a.x b.x a = b = Obj() Obj() = b = a None None Typdeskriptor Ref: 1 x Referenzzählen (3/3) a Python a = b = ⇒ a.x b.x a = b = Obj() Obj() = b = a None None Typdeskriptor Ref: 1 x b Typdeskriptor Ref: 1 x Referenzzählen (3/3) a Python a = b = a.x ⇒ b.x a = b = Obj() Obj() = b = a None None Typdeskriptor Ref: 1 x b Typdeskriptor Ref: 2 x Referenzzählen (3/3) a Python a = b = a.x b.x ⇒ a = b = Obj() Obj() = b = a None None Typdeskriptor Ref: 2 x b Typdeskriptor Ref: 2 x Referenzzählen (3/3) a Python a = b = a.x b.x a = ⇒ b = Obj() Obj() = b = a None None Typdeskriptor Ref: 1 x b Typdeskriptor Ref: 2 x Referenzzählen (3/3) a Python a = b = a.x b.x a = b = ⇒ Obj() Obj() = b = a None None Typdeskriptor Ref: 1 x b Typdeskriptor Ref: 1 x Problem #2: Referenzzyklen verhindern Speicherfreigabe Verfolgende Speicherbereinigung (tracing garbage collection) Wird „gelegentlich“ aktiviert: 1. Markiere alle erreichbaren Objekte 2. Finde alle nicht markierten (= nicht erreichbaren, ‘toten’) Objekte und dealloziere sie Verfolgende Speicherbereinigung (tracing garbage collection) Wird „gelegentlich“ aktiviert: 1. Markiere alle erreichbaren Objekte I Immer erreichbar: Wurzelmenge I I I Registerinhalte Lokale Variablen und Parameter auf dem Stapel Globale Variablen (Datensegment) 2. Finde alle nicht markierten (= nicht erreichbaren, ‘toten’) Objekte und dealloziere sie Verfolgende Speicherbereinigung (tracing garbage collection) Wird „gelegentlich“ aktiviert: 1. Markiere alle erreichbaren Objekte I Immer erreichbar: Wurzelmenge I I I I Registerinhalte Lokale Variablen und Parameter auf dem Stapel Globale Variablen (Datensegment) Beginnend mit Wurzelmenge, berechne transitiven Abschluß: I I I Untersuche Objektstruktur durch Laufzeittypinformationen Finde Zeiger auf Kindobjekte, markiere Iteriere/Rekurriere solange möglich 2. Finde alle nicht markierten (= nicht erreichbaren, ‘toten’) Objekte und dealloziere sie Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 I Wir markieren transitiv . . . Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 I Wir markieren transitiv . . . Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 I Wir markieren transitiv . . . Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 I Wir markieren transitiv . . . Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 I Wir markieren transitiv . . . Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 I Wir markieren transitiv . . . Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 I Wir markieren transitiv . . . Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 I Wir markieren transitiv . . . Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 I I Wir markieren transitiv . . . Nicht markierten Objekte sind nicht erreichbar Beispiel: Mark & Sweep Ein mögliches Verfahren zur automatischen Speicherbereinigung Wurzelmenge 0 5 1 2 9 3 6 4 7 8 I I I Wir markieren transitiv . . . Nicht markierten Objekte sind nicht erreichbar 7,8,9 können dealloziert werden Vergleich der Speicherbereinigungsformen I Referenzzählen ⊕ I Zeitbedarf gut kontrollierbar Zusätzliche Speicherzugriffe bei jeder Kopie einer Referenz Zyklen verhindern Speicherfreigabe Verfolgende Speicherbereinigung ⊕ Vollständiges Freigeben von Speicher Benötigt eines der Folgenden: I I I Pausen während der Ausführung (Mark & Sweep) Zusätzlichen Speicher (andere verfolgende Verfahren) Zusätzliche Speicherzugriffe (nebenläufige Verfahren) Zusammenfassung: Automatische Speicherverwaltung I Speicherfehler nehmen folgende Formen an: I I I Speicherfehler können durch Automatische Speicherverwaltung vermieden werden: I I I I Speicherlecks Speicherkorruption Sprache einschränken Explizites free verbieten/ignorieren Stattdessen implizit/automatisch deallozieren (Automatische Speicherbereinigung) Automatische Speicherbereinigung: I I I Referenzzählen Konstante Ausführungszeit, aber nicht ganz vollständig Mark & Sweep Gelegentliche Unterbrechungen, aber vollständig Andere Formen der Verfolgenden Automatischen Speicherverwaltung Implementierung der Verfolgenden Speicherbereinigung I Auslöser: I I Wurzelmenge: I I I I I Üblicherweise: Fehlgeschlagene Allozierung (‘kein Platz mehr frei’) Statische Variablen Register Stapelinhalte Verfolgen von Referenzen: Felder von ‘lebenden’ Objekten Freigabe des Speichers von ‘toten’ Objekten Wurzelmenge finden I I Schlüsselfrage: Welche Speicherstellen zeigen auf Objekte? Statische Variablen: I I I Typ verrät uns, was wir zu tun haben Objekttyp? → Teil der Wurzelmenge Anonsten (int etc.): ignorieren Wurzelmenge finden I I Schlüsselfrage: Welche Speicherstellen zeigen auf Objekte? Statische Variablen: I I I I Register: I I Typ verrät uns, was wir zu tun haben Objekttyp? → Teil der Wurzelmenge Anonsten (int etc.): ignorieren Variieren je nach Programmstelle Stapelinhalte: I Variieren je nach Programmstelle Stapel- und Registerinhalte li sw li jal move sw li jal $t0, 3 $t0, -8($fp) $a0, 8 allocate $t0, $v0 $v0, -8($fp) $a0, 16 allocate ; int ; Bytes ; Objekt ; Bytes Stapel- und Registerinhalte li sw li jal move sw li jal I In Wurzelmenge: I I ... Nicht in Wurzelmenge: I I I $t0 mem[$fp-8] ... $t0, 3 $t0, -8($fp) $a0, 8 allocate $t0, $v0 $v0, -8($fp) $a0, 16 allocate ; int ; Bytes ; Objekt ; Bytes Stapel- und Registerinhalte li sw li jal move sw li jal I In Wurzelmenge: I I $t0, 3 $t0, -8($fp) $a0, 8 allocate $t0, $v0 $v0, -8($fp) $a0, 16 allocate ; int ; Bytes ; Objekt ; Bytes I I ... I I Nicht in Wurzelmenge: I I I $t0 mem[$fp-8] ... In Wurzelmenge: I I $t0 $v0 mem[$fp-8] ... Nicht in Wurzelmenge: I ... Stapel- und Registerinhalte li sw li jal move sw li jal I In Wurzelmenge: I I $t0, 3 $t0, -8($fp) $a0, 8 allocate $t0, $v0 $v0, -8($fp) $a0, 16 allocate ; int ; Bytes ; Objekt ; Bytes I I ... I I Nicht in Wurzelmenge: I I I $t0 mem[$fp-8] ... In Wurzelmenge: I I $t0 $v0 mem[$fp-8] ... Nicht in Wurzelmenge: I ... Wurzelmenge variiert innerhalb von Subroutinen Stapelkarten I Wenn allocate die Speicherbereinigung anstößt: Parameter 1 Parameter 0 I altes $ra I alter $fp Lokale Var. 0 Lokale Var. 1 Lokale Var. 2 Lokale Var. 3 Parameter 1 Parameter 0 $ra Stapel: Was ist meine Rücksprungadresse aus $ra ? Rücksprungadresse beschreibt darüberliegenden Aktivierungseintrag: I I Anzahl Elemente auf dem Stapel Typen der Elemente Stapelkarten I Parameter 1 Parameter 0 ref ref altes $ra Wenn allocate die Speicherbereinigung anstößt: I I alter $fp Lokale Var. 0 Lokale Var. 1 Lokale Var. 2 Lokale Var. 3 Parameter 1 Parameter 0 ref w ref w $ra Stapelkarte Stapel: Was ist meine Rücksprungadresse aus $ra ? Rücksprungadresse beschreibt darüberliegenden Aktivierungseintrag: I I I Anzahl Elemente auf dem Stapel Typen der Elemente Komprimiert in Stapelkarte Stapelkarten I Parameter 1 Parameter 0 ref ref Wenn allocate die Speicherbereinigung anstößt: I altes $ra I alter $fp Lokale Var. 0 Lokale Var. 1 Lokale Var. 2 Lokale Var. 3 Parameter 1 Parameter 0 ref w ref w $ra Stapelkarte Stapel: Was ist meine Rücksprungadresse aus $ra ? Rücksprungadresse beschreibt darüberliegenden Aktivierungseintrag: I I I I Anzahl Elemente auf dem Stapel Typen der Elemente Komprimiert in Stapelkarte Übersetzer baut Abbildung von Rücksprungadressen auf Stapelkarten Stapelkarten I Parameter 1 Parameter 0 ref ref Wenn allocate die Speicherbereinigung anstößt: I altes $ra I alter $fp Lokale Var. 0 Lokale Var. 1 Lokale Var. 2 Lokale Var. 3 Parameter 1 Parameter 0 ref w ref w $ra Stapelkarte Stapel: Was ist meine Rücksprungadresse aus $ra ? Rücksprungadresse beschreibt darüberliegenden Aktivierungseintrag: I I I I I Anzahl Elemente auf dem Stapel Typen der Elemente Komprimiert in Stapelkarte Übersetzer baut Abbildung von Rücksprungadressen auf Stapelkarten Rekursion: Betrachte altes $ra vom Stapel genauso, bis wir am Ende des Stapels ankommen Registerkarten I Register können analog Registerkarten verwenden Registerkarten I I Register können analog Registerkarten verwenden Alternative: Register-Partitionierung I I Register $r0, $r2, $r4 . . . speichern nur Referenzen Register $r1, $r3, $r5 . . . speichern nur nicht-Referenzen Registerkarten I I Register können analog Registerkarten verwenden Alternative: Register-Partitionierung I I I Register $r0, $r2, $r4 . . . speichern nur Referenzen Register $r1, $r3, $r5 . . . speichern nur nicht-Referenzen Weitere Alternativen: I I I Universelles Verpacken: Alle Register sind 0 oder zeigen auf Referenz Tagging: Wir reservieren ein Bit im Register, um zwischen Referenzen/Nicht-Referenzen zu unterscheiden Kombinationen der obigen Konzepte Referenzen Verfolgen C# public class Node { Node next; // verfolgen int value; // nicht verfolgen } I I Benötigen Informationen darüber, welche Felder Referenzen sind Geeigneter Ablageort: I I Typdeskriptor Ein Bit pro Feld Objekte Verfolgen I Mark & Sweep: I I I I Merken uns ‘Lebendigkeit’ (liveness) als Bit im Objekt Drehen das Bit während Verfolgung um Nach der Verfolgung: Verwerfen alle Objekte mit ‘falschem’ Wert im Bit Einige fortgeschrittene Algorithmen verwenden alternative Ansätze Wurzelmenge: Register + Stapel + Statische Variablen 0 5 1 2 3 4 9 6 7 8 Speicher-Recycling I Nun haben wir alle ‘lebenden’ Objekte markiert. . . Speicher-Recycling I I Nun haben wir alle ‘lebenden’ Objekte markiert. . . . . . aber wie finden wir den Speicher der ‘toten’ Objekte? I I Verschiedene Ansätze möglich Benötigen meist Datenstruktur mit allen Objekten I I Verkettete Liste zwischen allen Objekten ‘Katalog’: Bits, die sich merken, welche (64-Bit oder größeren) Speicherbereiche belegt sind Speicher-Recycling I I Nun haben wir alle ‘lebenden’ Objekte markiert. . . . . . aber wie finden wir den Speicher der ‘toten’ Objekte? I I Verschiedene Ansätze möglich Benötigen meist Datenstruktur mit allen Objekten I I I Verkettete Liste zwischen allen Objekten ‘Katalog’: Bits, die sich merken, welche (64-Bit oder größeren) Speicherbereiche belegt sind Spätere Objektallozierung muß den freigegebenen Speicher nutzen können Kopierende Speicherbereinigung (z.B. AttoVM 0.4) I Verwendet zwei Ablagespeicher: I I Speicher A wird aktiv genutzt Speicher B gilt als ‘leer’ Speicher A Speicher B Kopierende Speicherbereinigung (z.B. AttoVM 0.4) I Verwendet zwei Ablagespeicher: I I I Speicher A wird aktiv genutzt Speicher B gilt als ‘leer’ Wenn A voll ist: I I Kopiere alle lebendigen Objekte nach B Korrigiere Zeiger Speicher A Speicher B Kopierende Speicherbereinigung (z.B. AttoVM 0.4) I Verwendet zwei Ablagespeicher: I I I Speicher A wird aktiv genutzt Speicher B gilt als ‘leer’ Wenn A voll ist: I I I Kopiere alle lebendigen Objekte nach B Korrigiere Zeiger Tausche die Namen der Speicher aus Speicher B Speicher A Kopierende Speicherbereinigung (z.B. AttoVM 0.4) I Verwendet zwei Ablagespeicher: I I I Speicher A wird aktiv genutzt Speicher B gilt als ‘leer’ Wenn A voll ist: I I I I Kopiere alle lebendigen Objekte nach B Korrigiere Zeiger Tausche die Namen der Speicher aus Neuer ‘B’-Speicher gilt wieder als ‘leer’ Speicher B Speicher A Zusammenfassung I Wurzelmenge finden: I I I I Statische Variablen: Statische Variablenkarte Variablen auf dem Stapel: Stapelkarten Register: Registerkarten, Registerpartitionierung, . . . Verfolgung: I I I Beginnen mit Wurzelmenge Annahme: Alle Objekte sind ‘tot’ Jedes referenzierte Objekt fragt Typdeskriptor, welche I I Verfolge alle Kinder, markiere als ‘lebendig’ Sobald Verfolgung fertig ist: I Finden alle ‘toten’ Objekte zur Wiederverwertung