Fakultät Elektronik und Informatik Studiengang Informatik Automatische dynamische Speicherverwaltung Vorlesung im Sommersemester 2015 Prof. Dr. habil. Christian Heinlein christian.heinleins.net C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) Vorlesungsüberblick ❐ Systemprogrammierung in C und C++ ❐ Dynamische Speicherverwaltung ❐ Automatische Speicherbereinigung (Garbage collection) ❍ Referenzzähler ❍ Mark&Sweep-Verfahren ❍ Mark&Compact-Verfahren ❍ Kopierende Verfahren ❍ Konservative Verfahren ❍ Inkrementelle Verfahren 1 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.1 Vorbemerkungen 1 Systemprogrammierung in C und C++ (Teil 1) 1 Systemprogrammierung in C und C++ (Teil 1) 1.1 Vorbemerkungen ❐ Wir verwenden C++ nicht als objektorientier te Programmiersprache, sondern als besseres C . ❐ Zwischen ❍ K&R-C (ursprüngliches C von Kernighan & Ritchie), ❍ ANSI-C (Standard-C), ❍ frühem C++ und ❍ heutigem Standard-C++ bestehen zum Teil erhebliche Unterschiede im Detail. ❐ Die folgenden Ausführungen beziehen sich auf den C++-Standard von 2003 (u. a., weil die Erweiterungen von C++11 für diese Vorlesung nicht relevant sind). 2 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.1 Elementare Typen 3 1.2 Datentypen 1.2.1 Elementare Typen Übersicht Typ bool char wchar_t short int long float double long double T* T (*)(args) übliche Größe 1 oder 4 1 2 oder 4 2 4 4 oder 8 4 8 12 oder 16 4 oder 8 4 oder 8 übliche Ausrichtung 1 oder 4 true Beispiele false 1 ’a’ ’\007’ ’\t’ ’\x20’ ’\” ’\0’ 2 oder 4 2 4 4 oder 8 4 4 oder 8 4 oder 8 4 oder 8 4 oder 8 L’ä’ 1 0377 0xff 1.0 0.5 2.8e5 0 0 L’°’ L’¶’ 5 10 (oktal) (hexadezimal) 1. 1e6 .5 1e−6 9.1e−17 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.1 Elementare Typen 4 Anmerkungen ❐ int entspricht meistens einem Maschinenwor t . ❐ Neben short, int und long gibt es auch unsigned short, unsigned int (oder kurz unsigned) und unsigned long. ❐ Durch Anfügen von u/U und/oder l/L kann man ganzzahlige Konstanten, die normalerweise Typ int besitzen, explizit als unsigned und/oder long kennzeichnen. ❐ Durch Anfügen von f/F oder l/L kann man Gleitkomma-Konstanten, die normalerweise Typ double besitzen, explizit als float oder long double kennzeichnen. ❐ Aufgrund der üblichen arithmetischen Umwandlungen ist dies normalerweise jedoch nicht erforderlich. ❐ Die ganzzahlige Konstante 0 dient gleichzeitig als Nullzeiger. ❐ char-Objekte sind nichts anderes als 1 Byte große ganze Zahlen (mit oder ohne Vorzeichen). Neben char gibt es auch signed char und unsigned char. ❐ Ein char-Wer t wie z. B. ’A’ stellt die Position des jeweiligen Zeichens im verwendeten Zeichensatz dar. (Daher wandelt z. B. c − ’A’ + ’a’ jeden Großbuchstaben c in den zugehörigen Kleinbuchstaben um, sofern alle Groß- und Klein- C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.1 Elementare Typen 5 buchstaben im Zeichensatz äquidistant liegen, was normalerweise der Fall ist. Die konkreten Zahlenwer te von ’A’ und ’a’ sind hierfür unwichtig.) ❐ wchar_t (wide character type) dient zur Repräsentation von Zeichensätzen mit mehr als 256 Zeichen (z. B. Unicode). Obwohl es sich um einen eigenen Typ handelt, besitzt er dieselben Eigenschaften wie einer der anderen ganzzahligen Typen. Ausrichtung (Alignment) ❐ Objekte eines Typs T können meist nur Vielfache einer Zahl aT als Adressen besitzen. ❐ Diese Zahl aT heißt Ausrichtung (engl. alignment ) des Typs T. ❐ Formal: aT = ggT AT , wenn AT die Menge aller zulässigen Adressen für Objekte des Typs T bezeichnet. ❐ Grundsätzlich gilt: sizeof(T) = k aT für ein k ∈ IN. Häufig gilt k = 1, d. h. aT = sizeof(T). ❐ Zum Teil gilt für double und long double aber auch: aT = sizeof(int) = Wor tgröße. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.2 Arrays 1.2.2 Arrays ❐ Beispiele: char s [256]; int x [10]; double m [4] [8]; long* v [5]; // Zweidimensionales Array. // Array von Zeigern auf long, // nicht Zeiger auf Array von long. ❐ Die Größe eines Arrays ergibt sich als Produkt aus der Größe des Elementtyps und der Anzahl der Elemente. ❐ Die Ausrichtung eines Arrays entspricht der Ausrichtung des Elementtyps. 6 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.3 Strukturen (Records) 1.2.3 Strukturen (Records) Beispiele // Komplexe Zahlen. struct Complex { double real; double imag; } x, y; // Knoten eines binären Baums. struct Node { char value; Node* left; Node* right; }; Node n; Node* p; 7 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.3 Strukturen (Records) Anmerkungen ❐ Ein Strukturobjekt enthält die deklarier ten Komponentenobjekte in der Reihenfolge ihrer Deklaration. ❐ Um die korrekte Ausrichtung aller Komponentenobjekte in einem Strukturobjekt zu gewährleisten, ergibt sich u. U. Verschnitt zwischen den Komponenten. ❐ Um die korrekte Ausrichtung aller Komponentenobjekte in einem Array von Strukturobjekten zu gewährleisten, ergibt sich u. U. zusätzlicher Verschnitt am Ende jedes Strukturobjekts. ❐ Die Größe einer Struktur ist daher u. U. größer als die Summe der Größen ihrer Komponenten. ❐ Ordnet man die Komponenten einer Struktur nach absteigender Ausrichtung an, so wird der Verschnitt minimal. ❐ Die Ausrichtung einer Struktur entspricht der maximalen Ausrichtung ihrer Komponenten. 8 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.3 Strukturen (Records) ❐ Die Ausrichtung aT eines Typs T kann wie folgt mit Hilfe einer Struktur ermittelt werden: struct Dummy { char x; T y; }; const int alignT = sizeof(Dummy) − sizeof(T); sizeof(Dummy) x y aT sizeof(T) = k aT 9 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.3 Strukturen (Records) Beispiele ❐ Für alle weiteren Beispiele sollen folgende Größen und Ausrichtungen gelten: Typ bool char short int long float double long double T* T (*)(args) Größe 4 1 2 4 4 4 8 16 4 4 Ausrichtung 4 1 2 4 4 4 8 8 4 4 10 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.3 Strukturen (Records) ❐ komplexe Zahlen struct Complex { // Offset double real; // 0 double imag; // 8 }; // Ausr. 8 8 8 real 0 Größe 8 8 16 Verschn. Gesamt 0 8 0 8 0 16 imag 8 16 11 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.3 Strukturen (Records) ❐ Knoten eines binären Baums struct Node { char value; Node* left; Node* right; }; // Offset // 0 // 4 // 8 // Ausr. 1 4 4 4 Größe 1 4 4 9 Verschn. Gesamt 3 4 0 4 0 4 3 12 // Offset // 0 // 4 // 8 // Ausr. 4 4 1 4 Größe 4 4 1 9 Verschn. Gesamt 0 4 0 4 3 4 3 12 value 0 left Oder struct Node { Node* left; Node* right; char value; }; 4 left 0 right 4 right 8 12 value 8 12 12 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.3 Strukturen (Records) ❐ zwei int, zwei short struct SISI { short s1; int i1; short s2; int i2; }; // Offset // 0 // 4 // 8 // 12 // Ausr. 2 4 2 4 4 Größe 2 4 2 4 12 Verschn. Gesamt 2 4 0 4 2 4 0 4 4 16 struct IISS { int i1; int i2; short s1; short s2; }; // Offset // 0 // 4 // 8 // 10 // Ausr. 4 4 2 2 4 Größe 4 4 2 2 12 Verschn. Gesamt 0 4 0 4 0 4 0 4 0 12 s1 i1 0 4 i1 0 s2 8 i2 4 i2 12 s1 8 s2 12 16 13 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.3 Strukturen (Records) 14 ❐ 50 % Verschnitt struct SDCIC { short s; double d; char c1; int i; char c2; }; // Offset // 0 // 8 // 16 // 20 // 24 // Ausr. 2 8 1 4 1 8 Größe 2 8 1 4 1 16 Verschn. Gesamt 6 8 0 8 3 4 0 4 7 8 16 32 struct DISCC { double d; int i; short s; char c1; char c2; }; // Offset // 0 // 8 // 12 // 14 // 15 // Ausr. 8 4 2 1 1 8 Größe 8 4 2 1 1 16 Verschn. Gesamt 0 8 0 4 0 2 0 1 0 1 0 16 s d 0 8 d 0 c1 16 i 8 s c1 c2 16 i c2 24 32 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.4 Unionen (Überlagerungen, variante Records) 1.2.4 Unionen (Überlagerungen, variante Records) Beispiel: Repräsentation arithmetischer Ausdrücke ❐ Ausdruckskategorien: enum Kind { Const, Var, Neg, Add, Sub, Mul, Div }; ❐ Definition als einfache Struktur : struct Expr { Kind kind; char* name; double value; Expr* body; Expr* left; Expr* right; }; // // // // // // Kategorie des Ausdrucks. Name einer Variablen. Wert einer Variablen oder Konstanten. Operand einer Negation. Linker und rechter Operand einer Addition, Subtraktion, Multiplikation oder Division. 15 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.4 Unionen (Überlagerungen, variante Records) 16 ❐ Für jede Ausdruckskategorie bleiben bestimmte Komponenten unbenutzt: Const name 0 8 Var name 0 value name 0 0 name 0 Mul name 0 Div name 8 right left 16 32 right 24 body 16 32 24 body value right left 16 8 32 24 body value right left 16 8 32 24 body value right left 16 8 Sub left 32 24 body value right 24 body value name left 16 8 Add body 16 8 Neg 0 value left 32 right 24 32 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.4 Unionen (Überlagerungen, variante Records) ❐ Platzsparende Definition mit Unionen: struct Expr { Kind kind; union { char* name; Expr* body; Expr* left; }; union { double value; Expr* right; }; }; // Falls kind gleich Var. // Falls kind gleich Neg. // Falls kind gleich Add, Sub, Mul, Div. // Falls kind gleich Var, Const. // Falls kind gleich Add, Sub, Mul, Div. name body left kind 0 4 value right 8 12 16 17 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.4 Unionen (Überlagerungen, variante Records) 18 Anmerkungen ❐ Eine Union enthält zu jedem Zeitpunkt genau eine ihrer Komponenten. ❐ Alle Komponenten beginnen am Anfang der Union. ❐ Die Ausrichtung einer Union entspricht der maximalen Ausrichtung ihrer Komponenten. ❐ Die Größe einer Union ergibt sich aus der maximalen Größe ihrer Komponenten, ggf. gerundet auf die Ausrichtung der Union. ❐ Die korrekte Verwendung der Komponenten liegt in der Verantwor tung des Programmierers. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.4 Unionen (Überlagerungen, variante Records) Weitere Beispiele union U { short s [3]; int i; }; // Offset // 0 // 0 // Ausr. 2 4 4 Größe 6 4 6 Verschn. Gesamt 2 8 4 8 2 8 s i 0 struct S { char c; U u; }; 4 // Offset // 0 // 4 // 8 Ausr. 1 4 4 Größe 1 6 7 Verschn. Gesamt 3 4 2 8 5 12 s c 0 i 4 8 12 19 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.4 Unionen (Überlagerungen, variante Records) union XY { // Offset char c; // 0 struct { char c; short s [3]; } x; // 0 struct { char c; int i; } y; // 0 }; // c x.c y.c 0 Ausr. 1 Größe 1 Verschn. Gesamt 7 8 2 7 1 8 4 4 5 7 3 1 8 8 x.s y.i 4 8 20 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.5 Zeigertypen 21 1.2.5 Zeigertypen ❐ Die nachfolgenden Ausführungen gelten für Typen T ungleich void. Inhalts- und Adress-Operator ❐ Für einen Zeiger p vom Typ T* liefer t der Ausdruck *p das Objekt vom Typ T, auf das p zeigt. ❐ Der Ausdruck *p ist ein L-Wer t , d. h. er darf wie eine Variable als Ziel von Zuweisungen verwendet werden. ❐ Für einen L-Wer t x vom Typ T liefer t der Ausdruck &x die Adresse vom Typ T*, die das Objekt x besitzt. ❐ Somit gilt: &*p == p *&x == x x p C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.5 Zeigertypen 22 Adress-Arithmetik ❐ Für einen Zeiger p vom Typ T* und eine ganze Zahl i sind p + i und p − i ebenfalls Zeiger vom Typ T*, deren Wer t i * sizeof(T) größer bzw. kleiner ist als der Wer t von p. ❐ Für zwei Zeiger p und q vom Typ T* ist q − p eine ganze Zahl, die angibt, wieviele Elemente vom Typ T zwischen den Adressen p (einschließlich) und q (ausschließlich) liegen. ❐ Rein formal gelten diese Regeln nur, wenn alle Zeigerwer te auf Elemente eines bestimmten Arrays vom Typ T[] (oder auf das erste Element „hinter“ diesem Array) verweisen. p−3 p p+2 p q−p q C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.5 Zeigertypen Array/Zeiger-Äquivalenz ❐ Ein Array a vom Typ T[] ist gleichzeitig ein (konstanter) Zeiger vom Typ T*, der auf das „nullte“ Element von a zeigt. ❐ Umgekehr t kann ein Zeiger p vom Typ T* auch als Array (unbekannter Größe) vom Typ T[] aufgefasst werden. ❐ Somit gilt für eine ganze Zahl i: a[i] == *(a+i) &a[i] == a+i p[i] == *(p+i) &p[i] == p+i und speziell: a[0] == *a &a[0] == a a[0] p[0] == *p &p[0] == p a[i] p[−2]p[−1] p[0] p[1] p[2] a a a+i p 23 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.5 Zeigertypen Umwandlungen (Casts) ❐ Ein Zeiger vom Typ T* kann implizit in einen Zeiger vom Typ void* umgewandelt werden: T* p = ...; void* q = p; ❐ Umgekehr t kann ein Zeiger vom Typ void* explizit in einen Zeiger vom Typ T* umgewandelt werden: T* r = (T*)q; // oder: T* r = static_cast<T*>(q); ❐ Ein Zeiger vom Typ T* kann explizit in einen Zeiger eines anderen Typs S* umgewandelt werden. T* p = ...; S* q = (S*)p; // oder: S* q = reinterpret_cast<S*>(p); 24 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.2 Datentypen 1 Systemprogrammierung in C und C++ (Teil 1) 1.2.5 Zeigertypen 25 ❐ Außerdem kann ein Zeiger explizit in eine ganze Zahl mit ausreichender Größe umgewandelt werden und umgekehr t (wobei der Zeigerwer t insgesamt unveränder t bleibt): typedef unsigned long ulong; T* p = ...; ulong u = (ulong)p; // oder: ulong u = reinterpret_cast<ulong>(p); T* r = (T*)u; // oder: T* r = reinterpret_cast<T*>(u); ❐ Normalerweise sind die Typen long und unsigned long hierfür ausreichend groß, aber rein formal ist dies nicht garantier t. ❐ Vorsicht: Wenn ein Zeigerwer t durch Umwandlungen erzeugt wurde, muss bei Anwendung des Inhaltsoperators auf korrekte Ausrichtung geachtet werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.3 Bit-Manipulationen 1 Systemprogrammierung in C und C++ (Teil 1) 1.3.1 Bitoperatoren 26 1.3 Bit-Manipulationen 1.3.1 Bitoperatoren Bitweises Verschieben ❐ Für eine vorzeichenlose ganze Zahl x vom Typ unsigned int oder unsigned long und eine ganze Zahl i zwischen 0 (einschließlich) und der Größe n von x in Bits (ausschließlich) liefer t der Ausdruck x << i bzw. x >> i eine ganze Zahl vom selben Typ wie x, die durch Verschieben des Bitmusters von x um i Positionen nach links bzw. rechts entsteht. (Wenn x den Typ unsigned short besitzt, wird es zunächst in unsigned int umgewandelt.) ❐ Hierbei gehen die am weitesten links bzw. rechts stehenden i Bits von x verloren, während auf der anderen Seite i Nullbits nachgeschoben werden. ❐ Somit entspricht x << i bzw. x >> i einer Multiplikation von x mit 2i (modulo 2n ) bzw. einer ganzzahligen Division von x durch 2i . ❐ Vorsicht: Für vorzeichenbehaftete ganze Zahlen x ist undefiniert, ob bei x >> i auf der linken Seite Nullbits oder der Wer t des Vorzeichenbits nachgeschoben werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.3 Bit-Manipulationen 1 Systemprogrammierung in C und C++ (Teil 1) 1.3.1 Bitoperatoren 27 Bitweise Verknüpfungen ❐ Für eine ganze Zahl x liefer t der Ausdruck ˜x eine ganze Zahl, deren Bitmuster durch bitweise Komplementbildung aus dem Bitmuster von x entsteht. ❐ Für ganze Zahlen x und y liefern die Ausdrücke x & y, x ^ y und x | y ganze Zahlen, deren Bitmuster durch eine bitweise logische Und- bzw. Exklusiv-oder- bzw. Inklusivoder-Verknüpfung der Bitmuster von x und y entsteht. ❐ Interpretiert man ein Bitmuster der Länge n als Menge M ⊆ { 0, . . ., n − 1 } (i ∈ M ⇔ Bit i gesetzt), so berechnen die Operatoren &, ^ und | den Durchschnitt, die symmetrische Differenz und die Vereinigung zweier Mengen, während der Operator ˜ das Komplement einer Menge berechnet. ❐ Beispiele: int x = 0x35; int y = 0xAC; // Bitmuster: 0...0|0011|0101 // Bitmuster: 0...0|1010|1100 int u = x & y; int v = x ^ y; int w = x | y; // Bitmuster: 0...0|0010|0100 // Bitmuster: 0...0|1001|1001 // Bitmuster: 0...0|1011|1101 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.3 Bit-Manipulationen 1 Systemprogrammierung in C und C++ (Teil 1) 1.3.1 Bitoperatoren 28 Anmerkungen ❐ Für eine Zweierpotenz n gilt: (x % n) == (x & (n−1)) ❐ Im Gegensatz zu den bitweisen Operatoren ˜, & und | arbeiten die Booleschen Operatoren !, && und || nicht auf den einzelnen Bits ihrer Operanden, sondern auf ihrem gesamten Wer t. (Beispielsweise liefer t 1 && 2 den Wer t true bzw. 1, während 1 & 2 den Wer t 0 besitzt.) ❐ Vorsicht: Die Operatoren &, ^ und | binden schwächer als die Vergleichsoperatoren ==, != etc. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.3 Bit-Manipulationen 1 Systemprogrammierung in C und C++ (Teil 1) 1.3.1 Bitoperatoren 29 Beispiele ❐ Definition von Flags: const unsigned READ = 1<<2, WRITE = 1<<1, EXEC = 1<<0; // Bitmuster: 0100, 0010, 0001; ❐ Kombinieren von Flags: unsigned flags = READ | WRITE; // 0100 | 0010 −> 0110 ❐ Setzen von Flags: flags |= EXEC; // 0110 | 0001 −> 0111 ❐ Löschen von Flags: flags &= ˜WRITE; // 0111 & ˜0010 −> 0111 & 1101 −> 0101 ❐ Umdrehen von Flags: flags ^= READ; // 0101 ^ 0100 −> 0001 ❐ Testen von Flags: if (flags & READ) ... // 0001 & 0100 −> 0000 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.3 Bit-Manipulationen 1 Systemprogrammierung in C und C++ (Teil 1) 1.3.2 Bitfelder 1.3.2 Bitfelder Beispiel: Flags ❐ Definition von Flags: struct Flags { unsigned READ : 1; unsigned WRITE : 1; unsigned EXEC : 1; } flags; ❐ Setzen von Flags: flags.EXEC = true; ❐ Löschen von Flags: flags.WRITE = false; ❐ Umdrehen von Flags: flags.READ ^= true; 30 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.3 Bit-Manipulationen 1 Systemprogrammierung in C und C++ (Teil 1) 1.3.2 Bitfelder ❐ Testen von Flags: if (flags.READ) ... Beispiel: kleine ganze Zahlen ❐ Definition von Vektoren mit drei 10-Bit-Koordinaten: struct Vector { unsigned x : 10; unsigned y : 10; unsigned z : 10; }; ❐ Verwendung z. B.: Vector add (Vector u, Vector v) { Vector w; w.x = u.x + v.x; w.y = u.y + v.y; w.z = u.z + v.z; return w; } 31 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.3 Bit-Manipulationen 1 Systemprogrammierung in C und C++ (Teil 1) 1.3.3 Ungenutzte Zeigerbits 32 Anmerkungen ❐ Bitfelder sind nur innerhalb von Strukturen erlaubt. ❐ In einer Deklaration T x : N muss die Zahl N eine Konstante sein, die nicht größer als die Größe des Typs T in Bits sein sollte. ❐ Es gibt keine Zeiger auf Bitfelder. ❐ Die Anordnung von Bitfeldern im Speicher ist implementierungsabhängig. 1.3.3 Ungenutzte Zeigerbits Idee ❐ Zeiger auf einen Typ T mit Ausrichtung aT > 1 müssen als Wer te Vielfache von aT besitzen. ❐ Daher sind in ihrem Bitmuster immer log2 aT Bits null. ❐ Diese Bits kann man zur Speicherung von Flags zweckentfremden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.3 Bit-Manipulationen 1 Systemprogrammierung in C und C++ (Teil 1) 1.3.3 Ungenutzte Zeigerbits 33 Beispiel: Rot-Schwarz-Bäume ❐ Saubere Lösung: // Knoten eines struct Node { int value; Node* left; Node* right; bool red; }; Rot−Schwarz−Baums. // // // // Wert des Knotens. Zeiger auf linken Nachfolger. Zeiger auf rechten Nachfolger. Für rote Knoten true, für schwarze false. // Baum mit Wurzelknoten n traversieren. void traverse (Node* n) { if (!n) return; cout << n−>value << " " << (n−>red ? "red" : "black") << endl; traverse(n−>left); traverse(n−>right); } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.3 Bit-Manipulationen 1 Systemprogrammierung in C und C++ (Teil 1) 1.3.3 Ungenutzte Zeigerbits ❐ Platzsparende Lösung durch Speicherung des Flags red in der Zeigerkomponente left: // Knoten eines struct Node { int value; Node* left; Node* right; }; Rot−Schwarz−Baums. // Wert des Knotens. // Zeiger auf linken Nachfolger und red−Bit. // Zeiger auf rechten Nachfolger. typedef unsigned long ulong; // Baum mit Wurzelknoten n traversieren. void traverse (Node* n) { if (!n) return; ulong x = (ulong)(n−>left); // red−Bit abfragen. cout << n−>value << " " << (x&1 ? "red" : "black") << endl; Node* left = (Node*)(x & ˜1); // red−Bit ausblenden. traverse(left); traverse(n−>right); } 34 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.4 L-Wer te, R-Wer te und Referenzen 1 Systemprogrammierung in C und C++ (Teil 1) 1.4.1 L- und R-Wer te 1.4 L-Werte, R-Werte und Referenzen 1.4.1 L- und R-Werte ❐ Ein L-Wer t ist ein Objekt, das eine Adresse besitzt und daher als Ziel einer Zuweisung verwendet werden darf, sofern es nicht const deklariert wurde. ❐ Alle anderen Objekte sind R-Wer te. ❐ In C sind die folgenden Objekte L-Wer te: ❍ Globale und lokale Variablen ❍ Funktionsparameter ❍ Strukturkomponenten (s.x, p−>x) ❍ Arrayelemente (a[i]) ❍ Dereferenzier te Zeiger (*p) Hierbei können s, p und a prinzipiell beliebige Ausdrücke (also auch R-Wer te) sein. ❐ In C++ liefern auch die folgenden Ausdrücke L-Wer te: ❍ Ausdrücke der Form (x, y), sofern y ein L-Wer t ist. ❍ Ausdrücke der Form (x ? y : z), sofern y und z L-Wer te desselben Typs sind. 35 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.4 L-Wer te, R-Wer te und Referenzen 1 Systemprogrammierung in C und C++ (Teil 1) 1.4.2 Referenzen 36 1.4.2 Referenzen ❐ Darüber hinaus gibt es in C++ Referenztypen, deren Objekte grundsätzlich L-Wer te sind. ❐ Ein Objekt r eines Referenztyps T&, das mit einem L-Wer t x des Typs T initialisier t wurde, entspricht konzeptuell einem implizit dereferenzier ten Zeigerwer t *p, dessen Zeiger p vom Typ T* mit der Adresse &x des Objekts x initialisier t wurde und nicht veränder t werden kann. ❐ Beispiel zum Vergleich von Referenzen und Zeigern: int x = 1; int& r = x; cout << r << endl; r = 2; cout << x << endl; int x = 1; int* p = &x; cout << *p << endl; *p = 2; cout << x << endl; // Ausgabe: 1 // Ausgabe: 2 ❐ Eine Variable eines Referenztyps T& muss bei ihrer Deklaration mit einem L-Wer t des Typs T initialisier t werden. ❐ Ein Funktionsparameter eines Referenztyps T& wird beim Aufruf der Funktion mit dem entsprechenden Funktionsargument (aktueller Parameter) initialisiert, bei dem es sich um einen L-Wer t des Typs T handeln muss. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.4 L-Wer te, R-Wer te und Referenzen 1 Systemprogrammierung in C und C++ (Teil 1) 1.4.3 Typische Verwendung von Referenzen ❐ Ein Funktionsresultat eines Referenztyps T& wird quasi durch Ausführung einer Anweisung return x im Funktionsrumpf mit einem L-Wer t x des Typs T initialisier t. 1.4.3 Typische Verwendung von Referenzen ❐ Parameterübergabe per Referenz (call by reference, VAR-Parameter in Pascal/Modula/Oberon): void swap (int& a, int& b) { int tmp = a; a = b; b = tmp; } ...... int x = 1, y = 2; swap(x, y); cout << x << " " << y << endl; // Ausgabe: 2 1 ❐ „Variablen“ als Funktionsresultate: char& elem (char* a, int i) { return a[i]; } ...... char x [10]; elem(x, 5) = ’y’; ❐ Vorsicht: Wird ein Objekt per Referenz zurückgeliefer t, so muss sichergestellt sein, dass es auch nach Beendigung der Funktion noch existier t. Insbesondere dürfen keine lokalen Variablen per Referenz zurückgeliefer t werden! 37 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.4 L-Wer te, R-Wer te und Referenzen 1 Systemprogrammierung in C und C++ (Teil 1) 1.4.4 Referenzen auf Konstanten 38 1.4.4 Referenzen auf Konstanten ❐ Objekte des Typs const T& dürfen mit beliebigen Werten des Typs T initialisier t werden. ❐ Wird zur Initialisierung ein R-Wer t x verwendet, so erzeugt der Compiler ein temporäres Objekt y des Typs T, das mit x initialisier t wird, und initialisiert dann das Referenzobjekt mit dem L-Wer t y. ❐ Ein Funktionsparameter des Typs const T& ist für den Aufrufer der Funktion äquivalent zu einem Parameter des Typs T: ❍ Er kann beliebige Objekte des Typs T als Argumente übergeben. ❍ Übergibt er einen L-Wer t, so wird dieser zwar per Referenz übergeben (was u. U. wesentlich effizienter als Übergabe per Wer t ist), kann innerhalb der Funktion aber (normalerweise) nicht veränder t werden. ❐ Ebenso ist ein Funktionsresultat des Typs const T& i. w. äquivalent zu einem Funktionswert des Typs T, kann aber oft effizienter zurückgeliefer t werden. (Da Funktionsresultate aber häufig in lokalen Variablen konstruiert werden, können sie meist nicht per Referenz zurückgegeben werden!) ❐ Der Effizienzgewinn kann insbesondere für große Struktur typen T und Typen mit explizitem Kopierkonstruktor (vgl. § 3.6) von Bedeutung sein. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 1.5 Literaturhinweise 1 Systemprogrammierung in C und C++ (Teil 1) 39 1.5 Literaturhinweise ❐ B. W. Kernighan, D. M. Ritchie: Programmieren in C (Zweite Ausgabe: ANSI-C). Carl Hanser Verlag, München, 1990. ❐ B. Stroustrup: The C++ Programming Language (Special Edition). Addison-Wesley, Reading, MA, 2000. ❐ A. T. Schreiner: Professor Schreiners UNIX-Sprechstunde. Carl Hanser Verlag, München, 1987. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.1 Zuteilung von Speicher 2 Dynamische Speicherverwaltung 2.1.1 Anwendungsbeispiel 2 Dynamische Speicherverwaltung 2.1 Zuteilung von Speicher 2.1.1 Anwendungsbeispiel // Binärer Suchbaum. struct Node { char* word; // Wort (Schlüssel). Node* left; // Linker und Node* right; // rechter Teilbaum. }; // Dynamische Speicherzuteilung. Node* newnode (); // Knoten. char* newstr (int len); // String der Länge len. 40 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.1 Zuteilung von Speicher 2 Dynamische Speicherverwaltung 2.1.1 Anwendungsbeispiel 41 // Wort w in Baum n einfügen, falls noch nicht vorhanden. void add (Node*& n, char* w) { // Wenn n ein Nullzeiger ist, ist man auf Blattebene angelangt. if (!n) { n = newnode(); n−>word = newstr(strlen(w) + 1); strcpy(n−>word, w); n−>left = n−>right = 0; return; } // Ggf. int c = if (c < else if } links oder rechts absteigen. strcmp(w, n−>word); 0) add(n−>left, w); (c > 0) add(n−>right, w); C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.1 Zuteilung von Speicher 2 Dynamische Speicherverwaltung 2.1.2 Zuteilung von Knoten (fester Größe) 2.1.2 Zuteilung von Knoten (fester Größe) const int N = 100; Node pool [N]; Node* high = pool; Node* newnode () { if (high < pool + N) return high++; else return 0; } pool high 42 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.1 Zuteilung von Speicher 2 Dynamische Speicherverwaltung 2.1.3 Zuteilung von Strings (variabler Größe) 2.1.3 Zuteilung von Strings (variabler Größe) const int N = 10000; char pool [N]; char* high = pool; char* newstr (int len) { if (high + len <= pool + N) { char* t = high; high += len; return t; } else { return 0; } } pool high 43 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.1 Anwendungsbeispiel 2.2 Rückgabe von Speicher 2.2.1 Anwendungsbeispiel // Dynamische Speicherrückgabe. void delnode (Node* n); // Knoten. void delstr (char* s); // String. // Teilbaum mit Wurzel n rekursiv löschen. void del (Node* n) { if (!n) return; del(n−>left); del(n−>right); delstr(n−>word); delnode(n); } 44 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.2 Rückgabe von Knoten (fester Größe) 45 2.2.2 Rückgabe von Knoten (fester Größe) Idee ❐ Zurückgegebene Knoten werden in einer linearen Liste verkettet (Freispeicherliste). ❐ Zur Verkettung wird die Zeigerkomponente left zweckentfremdet. ❐ Bei der Zuteilung von Knoten wird zuerst die Freispeicherliste abgearbeitet, bevor unbenutzte Knoten aus dem Pool vergeben werden. Realisierung const int N = 100; Node pool [N]; Node* high = pool; Node* head = 0; // Unbenutzte Knoten. // Zurückgegebene Knoten. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.2 Rückgabe von Knoten (fester Größe) // Knoten zuteilen. Node* newnode () { if (head) { // Ersten Knoten der Freispeicherliste // entnehmen und zurückliefern. Node* n = head; head = head−>left; return n; } else if (high < pool + N) { // Ersten unbenutzten Knoten // entnehmen und zurückliefern. return high++; } else { // Kein Knoten mehr verfügbar. return 0; } } 46 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.2 Rückgabe von Knoten (fester Größe) // Knoten zurückgeben. void delnode (Node* n) { // Knoten in Freispeicherliste einhängen. n−>left = head; head = n; } pool head high 47 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.3 Rückgabe von Strings fester Größe 48 2.2.3 Rückgabe von Strings fester Größe Idee ❐ Ersetze das Bytearray pool durch ein Array von Blöcken. ❐ Ein benutzter Block wird als String mit der Maximallänge L inter pretiert, während ein zurückgegebener Block als Knoten der Freispeicherliste inter pretiert wird. Realisierung const int L = 32; const int N = 100; union Block { char str [L]; Block* next; }; Block pool [N]; Block* high = pool; Block* head = 0; // Unbenutzte Blöcke. // Zurückgegebene Blöcke. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.3 Rückgabe von Strings fester Größe // String der Länge len zuteilen. char* newstr (int len) { if (len > L) { // Anforderung kann nicht erfüllt werden. return 0; } if (head) { // Ersten Block der Freispeicherliste verwenden. Block* b = head; head = head−>next; return b−>str; } else if (high < pool + N) { // Ersten unbenutzten Block verwenden. return high++−>str; } else { // Kein Block mehr verfügbar. return 0; } } 49 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.3 Rückgabe von Strings fester Größe // String s zurückgeben. void delstr (char* s) { // Block in Freispeicherliste einhängen. Block* b = (Block*)s; b−>next = head; head = b; } 50 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.4 Rückgabe von Strings variabler Größe 51 2.2.4 Rückgabe von Strings variabler Größe Schritt 1 ❐ Damit man bei der Rückgabe eines Speicherbereichs dessen Größe ermitteln kann, muss man sie bereits bei der Zuteilung notieren. ❐ Hierfür wird am Anfang jedes zugeteilten Speicherbereichs zusätzlicher Platz für eine ganze Zahl reservier t (Größenfeld ), die die Größe des Speicherbereichs (einschließlich des Größenfelds selbst) enthält. ❐ Damit dies für die Anwendung unsichtbar bleibt, wird nicht die Anfangsadresse des Speicherbereichs, sondern die Adresse des ersten „Nutzbytes“ zurückgeliefer t. const int I = sizeof(int); const int N = 10000; char pool [N]; char* high = pool; // Größe des Bereichs p abfragen bzw. auf s setzen. inline int& size (char* p) { return ((int*)(p))[−1]; } inline void size (char* p, int s) { size(p) = s; } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.4 Rückgabe von Strings variabler Größe // String der Länge len zuteilen. char* newstr (int len) { // Zusätzlicher Platz für Größenfeld. len += I; if (high + len <= pool + N) { char* p = high + I; // Nutzdaten. size(p, len); // Größenfeld. high += len; return p; } else { return 0; } } pool p p p p high 52 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.4 Rückgabe von Strings variabler Größe 53 Schritt 2 ❐ Zurückgegebene Speicherbereiche werden wiederum in einer Freispeicherliste verkettet. ❐ Zur Verkettung werden die ersten Nutzbytes des zurückgegebenen Speicherbereichs als Zeigerwer t zweckentfremdet. ❐ Somit befinden sich am Anfang jedes zurückgegebenen Speicherbereichs die folgenden Informationen: ❍ die Größe des Speicherbereichs [schwarz]; ❍ ein Zeiger auf den nächsten zurückgegebenen Speicherbereich (oder ein Nullzeiger) [dunkelgrau]. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.4 Rückgabe von Strings variabler Größe // Anfang der Freispeicherliste. char* head = 0; // Verkettungszeiger des Bereichs p abfragen bzw. auf q setzen. inline char*& next (char* p) { return ((char**)(p))[0]; } inline void next (char* p, char* q) { next(p) = q; } // Speicherbereich p zurückgeben. void delstr (char* p) { // p am Anfang der Freispeicherliste einhängen. next(p, head); head = p; } pool p p head high 54 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.4 Rückgabe von Strings variabler Größe 55 Schritt 3 ❐ Bei der Zuteilung eines Speicherbereichs wird zuerst die Freispeicherliste nach einem ausreichend großen Bereich durchsucht, bevor unbenutzte Bereiche des Pools vergeben werden. ❐ Außerdem muss sichergestellt werden, dass jeder Speicherbereich mindestens so groß wie ein Verkettungszeiger ist. const int P = sizeof(char*); // String der Länge len zuteilen. char* newstr (int len) { // Mindestens Platz für Verkettungszeiger // und zusätzlicher Platz für Größenfeld. if (len < P) len = P; len += I; // Freispeicherliste durchsuchen. char* p = head; char* q = 0; while (p && size(p) < len) { q = p; p = next(p); } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.4 Rückgabe von Strings variabler Größe if (p) { // Bereich p aushängen und verwenden. if (q) next(q, next(p)); else head = next(p); return p; } else if (high + len <= pool + N) { // Unbenutzten Bereich verwenden. p = high + I; // Nutzdaten. size(p, len); // Größenfeld. high += len; return p; } else { // Kein Platz mehr. return 0; } } 56 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.4 Rückgabe von Strings variabler Größe Schritt 4 ❐ Um bei der Wiederverwendung zurückgegebener Bereiche keinen Platz zu verschwenden, verbleibt der nicht benötigte Teil des wiederverwendeten Bereichs, wenn möglich, in der Freispeicherliste. ❐ Das Array pool kann dann einfach wie ein zurückgegebener Bereich der Größe N behandelt werden. pool head const int I = sizeof(int); const int P = sizeof(char*); const int N = 10000; char pool [N]; char* head = pool + I; 57 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.4 Rückgabe von Strings variabler Größe // Größe des Bereichs p abfragen bzw. auf s setzen. inline int& size (char* p) { return ((int*)(p))[−1]; } inline void size (char* p, int s) { size(p) = s; } // Verkettungszeiger des Bereichs p abfragen bzw. auf q setzen. inline char*& next (char* p) { return ((char**)(p))[0]; } inline void next (char* p, char* q) { next(p) = q; } // String der Länge len zuteilen. char* newstr (int len) { // Mindestens Platz für Verkettungszeiger // und zusätzlicher Platz für Größenfeld. if (len < P) len = P; len += I; // Freispeicherliste durchsuchen. char* p = head; char* q = 0; while (p && size(p) < len) { q = p; p = next(p); } if (!p) return 0; // Kein Platz mehr. 58 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.4 Rückgabe von Strings variabler Größe // Wenn die verbleibende Größe s kleiner als I + P ist, // muss der Bereich p ganz verwendet werden, andernfalls // kann der Restbereich r in der Freispeicherliste bleiben. int s = size(p) − len; char* r; if (s < I + P) { r = next(p); } else { size(p, len); r = p + len; size(r, s); next(r, next(p)); } // Bereich p aus der Freispeicherliste aushängen // oder durch den Restbereich r ersetzen (vgl. Abbildung). if (q) next(q, r); else head = r; return p; } 59 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.2 Rückgabe von Speicher 2 Dynamische Speicherverwaltung 2.2.4 Rückgabe von Strings variabler Größe next(q) oder head p next(p) next(q) oder head p r next(r) // Speicherbereich p zurückgeben. void delstr (char* p) { // p am Anfang der Freispeicherliste einhängen. next(p, head); head = p; } 60 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.1 Minimallösung 61 2.3 Allgemein verwendbare Speicherverwaltungsfunktionen 2.3.1 Minimallösung Motivation ❐ Der bis jetzt entwickelte Algorithmus enthält noch folgende Fehler : ❍ Die Felder size(head) und next(head) des ersten „zurückgegebenen“ Bereichs head werden nicht initialisiert. ❍ Bei der Interpretation von Speicherbereichen als int bzw. char* wird eine eventuell erforderliche Ausrichtung dieser Typen ignorier t. ❐ Außerdem kann der Algorithmus wie folgt verallgemeiner t werden: ❍ Wenn man darauf achtet, dass die von newstr zurückgeliefer ten Adressen p immer maximal ausgerichtet sind, können die zugeteilten Speicherbereiche nicht nur für Strings, sondern zur Speicherung beliebiger Daten verwendet werden. ❍ Um Speicherplatz wirklich dynamisch zu verwalten, muss das statische Array pool durch Speicherbereiche ersetzt werden, die dynamisch vom Betriebssystem angeforder t werden. ❐ Schließlich kann der Algorithmus in Hilfsfunktionen zerlegt werden, um spätere Weiterentwicklungen zu erleichtern. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.1 Minimallösung 62 Ideen ❐ Die maximale Ausrichtung aller denkbaren Typen erhält man als Ausrichtung einer Struktur oder Union, die alle Arten von elementaren Typen enthält. ❐ In der Praxis kann man sich bei den numerischen Typen jeweils auf den größten beschränken: union Maxalign { long l; long double d; char* p; void (*f) (); }; // // // // bool, char, wchar_t, short, int, long. float, double, long double. Datenzeiger. Funktionszeiger. ❐ Um Speicher vom Betriebssystem anzufordern, wird eine zunächst nicht näher definier te Funktion void* newmem (int size); verwendet. ❐ Die von newmem geliefer ten Zeigerwer te seien maximal ausgerichtet. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.1 Minimallösung Schnittstelle ❐ Anstelle der Funktionen newstr und delstr werden die Standardfunktionen extern "C" void* malloc (size_t len); extern "C" void free (void* p); verwendet. ❐ Damit diese Funktionen tatsächlich die gleichnamigen Funktionen der C-Standardbibliothek ersetzen können, müssen sie mittels extern "C" deklariert werden, um das „name mangling“ von C++ auszuschalten. ❐ size_t ist ein implementierungsabhängig definiertes Synonym eines geeigneten ganzzahligen Typs. ❐ Damit malloc vollkommen standardkonform ist, müsste im Fehlerfall nicht nur ein Nullzeiger geliefer t werden, sondern auch die globale Variable errno der C-Standardbibliothek auf den Wer t ENOMEM gesetzt werden. ❐ Ein Aufruf von free mit einem Nullzeiger p soll wirkungslos sein. 63 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.1 Minimallösung 64 Realisierung // Adresse eines beliebigen Speicherbereichs. typedef char* ptr; // Typ mit maximaler Ausrichtung. union Maxalign { long l; // bool, char, wchar_t, short, int, long. long double d; // float, double, long double. char* p; // Datenzeiger. void (*f) (); // Funktionszeiger. }; // Hilfsstruktur zur Bestimmung der Ausrichtung. struct Dummy { char x; Maxalign y; }; // x auf ein Vielfaches der Zweierpotenz a aufrunden. int align (int x, int a) { return (x + (a−1)) & ˜(a−1); } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.1 Minimallösung 65 // Konstanten. const int A = sizeof(Dummy) − sizeof(Maxalign); // Max. Ausrichtung. const int P = sizeof(ptr); // Größe eines Zeigers. const int I = sizeof(int); // Größe eines int−Werts. const int IA = align(I, A); // I aufgerundet auf A. const int S = getpagesize(); // Größe einer Speicherseite. const int B = 16 * S; // Standard−Blockgröße. // Mindestgröße eines Bereichs: // Größenfeld plus Verkettungszeiger. const int M = I + P; // Verlust (waste) pro Block: // Verschnitt am Anfang wegen Ausrichtung. const int W = IA − I; // Speicherbereich der Größe size mit maximal ausgerichteter // Anfangsadresse vom Betriebssystem beschaffen. void* newmem (int size); // Größe des Bereichs p abfragen bzw. auf s setzen. int& size (ptr p) { return ((int*)(p))[−1]; } void size (ptr p, int s) { size(p) = s; } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.1 Minimallösung 66 // Verkettungszeiger des Bereichs p abfragen bzw. auf q setzen. ptr& next (ptr p) { return ((ptr*)(p))[0]; } void next (ptr p, ptr q) { next(p) = q; } // Anfang der Freispeicherliste. ptr head = 0; // Neuen Block vom Betriebssystem beschaffen, der mindestens Platz // für einen Bereich der Länge len bietet, und initialisieren. ptr newblk (int len) { // Block mit ausreichender Größe beschaffen. int s = len + W <= B ? B : align(len + W, S); ptr p = (ptr)newmem(s); if (!p) return 0; // Größe des verbleibenden Bereichs eintragen. p += IA; size(p, s − W); return p; } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.1 Minimallösung // Bereich p am Anfang der Freispeicherliste einhängen. void link (ptr p) { next(p, head); head = p; } // Bereich p aus der Freispeicherliste aushängen. // q zeigt ggf. auf den Vorgänger in der Liste. void unlink (ptr p, ptr q) { if (q) next(q, next(p)); else head = next(p); } // Angeforderte Größe len anpassen. int adjust (int len) { len += I; // Platz für Größenfeld. if (len < M) len = M; // Mindestgröße beachten. return align(len, A); // Ausrichtung beachten. } 67 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.1 Minimallösung // Freien Bereich der Größe len suchen und ggf. aushängen. ptr search (int len) { ptr p = head, q = 0; while (p && size(p) < len) { q = p; p = next(p); } if (!p) return 0; unlink(p, q); return p; } // Bereich p ggf. aufteilen und Restbereich einhängen. void split (ptr p, int len) { // Die verbleibende Größe r muss mindestens M sein. int r = size(p) − len; if (r < M) return; // Größe von p auf len reduzieren. size(p, len); // Restbereich der Größe r anlegen und einhängen. p += len; size(p, r); link(p); } 68 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.1 Minimallösung // Speicherbereich der Größe len zuteilen. extern "C" void* malloc (size_t len) { // Größe anpassen. len = adjust(len); // Ausreichend großen Bereich suchen // oder vom Betriebssystem beschaffen. ptr p = search(len); if (!p) p = newblk(len); if (!p) return 0; // Bereich ggf. aufteilen. split(p, len); return p; } // Speicherbereich p zurückgeben. extern "C" void free (void* p_) { ptr p = (ptr)p_; // Wenn p kein Nullzeiger ist, // Bereich p in die Freispeicherliste hängen. if (p) link(p); } 69 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.1 Minimallösung p p p 70 p p p p head Die Abbildung zeigt zwei mittels newblk beschaffte Speicherblöcke. Am Anfang jedes Bereichs befindet sich ein Größenfeld (schwarz) der Größe I. Bei einem freien Bereich (weiß) folgt anschließend der Verkettungszeiger (dunkelgrau) der Größe P. Die von malloc geliefer ten Zeigerwer te p auf benutzte Bereiche (hellgrau) besitzen die maximale Ausrichtung A, sodass auch die Größenfelder und Verkettungszeiger korrekt ausgerichtet sind. Am Anfang jedes Blocks entsteht dadurch ein Verschnitt der Größe IA − I (die auch null sein kann). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.2 Zusammenfassen benachbarter freier Bereiche 71 2.3.2 Zusammenfassen benachbarter freier Bereiche Motivation ❐ Um die Fragmentierung des Speichers zu reduzieren, sollten benachbarte freie Speicherbereiche zusammengefasst werden. ❐ Um effizient festzustellen, ob ein Bereich frei ist oder nicht, wird ein entsprechendes Indikatorbit in seinem Größenfeld gespeichert. (Andernfalls müsste man die Freispeicherliste nach dem Bereich durchsuchen.) ❐ Dann kann bei der Freigabe eines Bereichs relativ leicht überprüft werden, ob sein rechter Nachbar ebenfalls frei ist. Wenn ja, werden die Bereiche zu einem einzigen Bereich zusammengefasst und die Überprüfung wiederholt. ❐ Damit dies korrekt funktioniert, muss es zu jedem benutzten Bereich einen rechten Nachbarn geben. Daher wird am Ende jedes mit newblk beschafften Blocks ein Dummy-Bereich angelegt, der nur aus einem Größenfeld besteht und als benutzt gekennzeichnet ist. ❐ Um den rechten Nachbarn ggf. effizient aus der Freispeicherliste aushängen zu können, wird diese als doppelt verkettete Ringliste organisier t. (Eine einfach verkettete Liste müsste man ebenfalls durchlaufen, um den Vorgänger des auszuhängenden Bereichs in der Liste zu finden.) Hierfür muss ein Bereich mindestens so groß wie zwei Zeiger sein. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.2 Zusammenfassen benachbarter freier Bereiche 72 ❐ Um die Wahrscheinlichkeit zu erhöhen, dass ein zurückgegebener Bereich noch mit einem anderen Bereich zusammengefasst werden kann, wird er nicht am Anfang, sondern am Ende der Freispeicherliste eingehängt, damit er möglichst lang in der Liste verbleibt. ❐ Um benachbarte freie Bereiche garantier t zu vermeiden, muss ein zurückgegebener Bereich ggf. auch mit seinem linken Nachbarn zusammengefasst werden. Um effizient herauszufinden, ob dieser frei ist − und wenn ja, wo er beginnt − , ist jedoch weitere Verwaltungsinformation notwendig (→ Übungsaufgabe). Realisierung (Ausschnitte) // Damit im Größenfeld Platz für das Indikatorbit ist, // müssen Bereichsgrößen garantiert gerade Zahlen sein. const int A_ = sizeof(Dummy) − sizeof(Maxalign); const int A = A_ >= 2 ? A_ : 2; // Mindestgröße eines Bereichs: // Größenfeld plus zwei Verkettungszeiger. const int M = I + 2*P; // Verlust (waste) pro Block: // Verschnitt am Anfang wegen Ausrichtung, Dummybereich am Ende. const int W = (IA − I) + I; C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.2 Zusammenfassen benachbarter freier Bereiche // Zugriff auf Größenfeld und Indikatorbit des Bereichs p. const int Used = 1<<0; int& size_used (ptr p) { return ((int*)(p))[−1]; } // Größe des Bereichs p abfragen bzw. auf s setzen, // ohne sein Indikatorbit zu verändern. int size (ptr p) { return size_used(p) & ˜Used; } void size (ptr p, int s) { (size_used(p) &= Used) |= s; } // Indikatorbit für den Bereich p abfragen bzw. setzen // bzw. zurücksetzen, ohne seine Größe zu verändern. bool used (ptr p) { return size_used(p) & Used; } void use (ptr p) { size_used(p) |= Used; } void unuse (ptr p) { size_used(p) &= ˜Used; } // Vorwärtszeiger des Bereichs p abfragen bzw. auf q setzen. ptr& next (ptr p) { return ((ptr*)(p))[0]; } void next (ptr p, ptr q) { next(p) = q; } // Rückwärtszeiger des Bereichs p abfragen bzw. auf q setzen. ptr& prev (ptr p) { return ((ptr*)(p))[1]; } void prev (ptr p, ptr q) { prev(p) = q; } 73 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.2 Zusammenfassen benachbarter freier Bereiche 74 // Anfang und Ende der Freispeicherliste. ptr sentinel [] = { (ptr)sentinel, (ptr)sentinel }; const ptr head = (ptr)sentinel; // Neuen Block vom Betriebssystem beschaffen, der mindestens Platz // für einen Bereich der Länge len bietet, und initialisieren. ptr newblk (int len) { // Block mit ausreichender Größe beschaffen. int s = len + W <= B ? B : align(len + W, S); ptr p = (ptr)newmem(s); if (!p) return 0; // Benutzten Dummybereich am Ende eintragen. use(p + s); // Größe des verbleibenden Bereichs eintragen. p += IA; size(p, s − W); return p; } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.2 Zusammenfassen benachbarter freier Bereiche // Bereich p am Ende der Freispeicherliste einhängen. void link (ptr p) { ptr q = prev(head); prev(head, p); next(p, head); prev(p, q); next(q, p); } // Bereich p aus der Freispeicherliste aushängen. void unlink (ptr p) { ptr q = next(p), r = prev(p); next(r, q); prev(q, r); } // Freien Bereich der Größe len suchen und ggf. aushängen. ptr search (int len) { ptr p = next(head); while (p != head && size(p) < len) p = next(p); if (p == head) return 0; unlink(p); return p; } 75 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.2 Zusammenfassen benachbarter freier Bereiche // Bereich p ggf. aufteilen und Restbereich einhängen. void split (ptr p, int len) { // Die verbleibende Größe r muss mindestens M sein. int r = size(p) − len; if (r < M) return; // Größe von p auf len reduzieren. size(p, len); // Unbenutzten Restbereich der Größe r anlegen und einhängen. p += len; size(p, r); unuse(p); link(p); } 76 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.2 Zusammenfassen benachbarter freier Bereiche 77 // Bereich p ggf. mehrmals mit seinem rechten Nachbarn zusammen− // fassen, der hierfür aus der Freispeicherliste ausgehängt wird. void merge_right (ptr p) { while (true) { ptr q = p + size(p); if (used(q)) return; unlink(q); size(p, size(p) + size(q)); } } // Speicherbereich der Größe len zuteilen. extern "C" void* malloc (size_t len) { // Größe anpassen. len = adjust(len); // Ausreichend großen Bereich suchen // oder vom Betriebssystem beschaffen. ptr p = search(len); if (!p) p = newblk(len); if (!p) return 0; C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.2 Zusammenfassen benachbarter freier Bereiche // Bereich ggf. aufteilen und als benutzt kennzeichnen. split(p, len); use(p); return p; } // Speicherbereich p zurückgeben. extern "C" void free (void* p_) { ptr p = (ptr)p_; // Spezialfall behandeln. if (!p) return; // Bereich ggf. mit seinem rechten Nachbarn zusammenfassen. merge_right(p); // Bereich in die Freispeicherliste hängen. link(p); // Bereich als unbenutzt kennzeichnen. unuse(p); } 78 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.2 Zusammenfassen benachbarter freier Bereiche p p p p p p p head Die Abbildung zeigt wiederum zwei mittels newblk beschaffte Speicherblöcke. Das Größenfeld (schwarz) jedes Bereichs enthält jetzt zusätzlich das Indikatorbit (kleines Quadrat; grau entspricht true, d. h. benutzt, weiß entspricht false, d. h. unbenutzt). Bei einem freien Bereich (weiß) folgen anschließend Vorwär ts- und Rückwär tszeiger (dunkelgrau), jeweils mit Größe P. Am Ende jedes Blocks befindet sich ein als benutzt gekennzeichneter Dummybereich, der nur aus einem Größenfeld besteht. 79 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.3 Allgemein verwendbare Speicherverwaltungs. . . 2 Dynamische Speicherverwaltung 2.3.3 Weitere Verbesserungsmöglichkeiten 2.3.3 Weitere Verbesserungsmöglichkeiten ❐ Um die Suche nach einem ausreichend großen freien Bereich zu beschleunigen, kann man mehrere Freispeicherlisten verwalten, die jeweils nur Bereiche einer bestimmten Größenordnung enthalten. ❐ Um schnell einen möglichst genau passenden Bereich zu finden − und so unnötige Bereichsteilungen zu vermeiden − , kann man die Bereiche einer Freispeicherliste nach ihrer Größe sortier t verwalten. ❐ Um häufig auftretende kleine Bereiche (z. B. 8, 16, 32 Byte) ohne zusätzliches Größenfeld zu verwalten, kann man sie in speziellen Arrays speichern. Allerdings muss durch zusätzliche Verwaltungsinformation zweifelsfrei und effizient feststellbar sein, ob ein freizugebender Bereich Teil eines solchen Arrays ist oder nicht. ❐ Um unnötigen Ressourcenverbrauch zu vermeiden, sollten größere zusammenhängende Freispeicherbereiche wieder an das Betriebssystem zurückgegeben werden. ❐ Um dynamisch wachsende (oder schrumpfende) Arrays o. ä. zu unterstützen, sollte eine zusätzliche Funktion void* realloc(void* ptr, size_t len) angeboten werden (→ Übungsaufgabe). ❐ Usw. 80 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.1 Aufbau eines Prozesses (unter Unix) 2.4 Schnittstelle zum Betriebssystem 2.4.1 Aufbau eines Prozesses (unter Unix) ❐ Der virtuelle Adressraum eines Prozesses besteht aus folgenden Segmenten: ❍ text: Programmcode (schreibgeschützt, ggf. von mehreren Prozessen gemeinsam benutzt) ❍ data: explizit initialisierte statische Daten ❍ bss (block storage segment): nicht explizit (und daher implizit mit Null) initialisierte statische Daten ❍ stack: lokale Daten ❐ Die Größe des Stacks wird automatisch angepasst, d. h. er wächst und schrumpft dynamisch. ❐ Je nach Architektur, wächst der Stack „vorwär ts“ oder „rückwär ts“, d. h. in Richtung größerer bzw. kleinerer Adressen. ❐ Besteht ein Prozess aus mehreren Threads, so besitzt jeder Thread einen eigenen Stack. 81 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.1 Aufbau eines Prozesses (unter Unix) text 0 stack etext edata end text 0 data bss 82 etext data bss stack stack edata end Die Abbildung zeigt zwei mögliche Aufteilungen des virtuellen Adressraums eines Prozesses: Oben gibt es einen „rückwär ts“ wachsenden Stack, unten zwei „vorwär ts“ wachsende Stacks. Das text-Segment beginnt bei der virtuellen Adresse 0. etext, edata und end sind konstante Zeigerwer te, die das Ende des text-, data- bzw. bss-Segments bzw. den Anfang des nachfolgenden Segments bezeichnen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.2 Der Systemaufruf sbrk 83 2.4.2 Der Systemaufruf sbrk ❐ Das BSS-Segment eines Prozesses kann mit Hilfe des Systemaufrufs void* sbrk (int n); dynamisch um n Byte vergrößer t werden. (Ist n negativ, so wird das Segment wieder verkleinert.) ❐ sbrk liefer t als Resultat die Endadresse des BSS-Segments vor dem Aufruf, d. h. bei positivem n die Anfangsadresse des hinzugekommenen Speicherbereichs. (Für n gleich 0 erhält man das aktuelle Ende des BSS-Segments, ohne es zu verändern.) ❐ Falls der Aufruf wegen mangelnder Ressourcen fehlschlägt, liefer t er (wie jeder UnixSystemaufruf) den Wer t −1 (!). ❐ Auf diese Weise kann sich ein Prozess dynamisch zusätzlichen Speicher beschaffen: void* newmem (int size) { void* p = sbrk(size); if (p == (void*)(−1)) return 0; return p; } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.2 Der Systemaufruf sbrk text 0 data bss etext edata end heap 84 stack sbrk(0) ❐ Da die Speicherverwaltung des Betriebssystems seitenorientier t arbeitet, ist es sinnvoll, immer Vielfache der Seitengröße anzufordern, die man mit Hilfe des Systemaufrufs getpagesize ermitteln kann. ❐ Allerdings liefer t sbrk beim ersten Aufruf (und nach einem Aufruf mit einer „krummen“ Größe) u. U. eine „krumme“ Adresse zurück. Daher muss newmem wie folgt erweiter t werden, damit es immer maximal ausgerichtete Adressen liefer t (vgl. § 2.3.1): // Speicherbereich der Größe size mit maximal ausgerichteter // Anfangsadresse vom Betriebssystem beschaffen. void* newmem (int size) { // Speicher der Größe size beschaffen. void* p = sbrk(size); if (p == (void*)(−1)) return 0; C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.2 Der Systemaufruf sbrk 85 // Wenn die Anfangsadresse p nicht maximal ausgerichtet ist: if (int d = (unsigned long)(p) & (A−1)) { // (A − d) Bytes zusätzlich beschaffen // und p entsprechend erhöhen. if (sbrk(A − d) == (void*)(−1)) { // Falls dies fehlschlagen sollte, alles rückgängig machen. sbrk(−size); return 0; } p = (ptr)(p) + (A − d); } return p; } Problem ❐ Um einen mit sbrk beschafften Speicherbereich an das Betriebssystem zurückgeben zu können, müssen zuvor alle später beschafften Speicherbereiche zurückgegeben werden (LIFO-Strategie). ❐ Daher können Speicherbereiche nicht unabhängig voneinander zurückgegeben werden, was insbesondere bei sehr großen Bereichen problematisch sein kann. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.3 Gemeinsam benutzte Speicherbereiche 86 2.4.3 Gemeinsam benutzte Speicherbereiche ❐ Mit Hilfe der Systemaufrufe shmget, shmat, shmdt und shmctl (shared memory get, attach, detach und control) können Speicherbereiche erzeugt, verwaltet und wieder gelöscht werden, die von mehreren Prozessen gemeinsam benutzt werden (Sharedmemory-Segmente). ❐ Auf diese Weise könnte man prinzipiell einen Heap realisieren, den sich mehrere Prozesse teilen. text data bss text data bss shm shm stack stack C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.4 In den Speicher eingeblendete Dateien 2.4.4 In den Speicher eingeblendete Dateien Prinzip ❐ Mit Hilfe des Systemaufrufs mmap (memor y map) lassen sich Dateibereiche in den vir tuellen Speicher eines Prozesses einblenden. ❐ Lesende bzw. schreibende Zugriffe auf die entsprechenden Speicherbereiche sind dann äquivalent zu lesenden bzw. schreibenden Zugriffen auf die zugehörigen Dateien. text data bss mmap stack 87 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.4 In den Speicher eingeblendete Dateien 88 Details ❐ mmap benötigt folgende Argumente: 1. entweder die gewünschte Anfangsadresse des Speicherbereichs oder Null, um die Wahl der Adresse dem Betriebssystem zu überlassen; 2. die Größe des Speicherbereichs in Byte; 3. die gewünschten Zugriffsrechte auf den Speicherbereich (bitweise Oder-Verknüpfung von PROT_READ und/oder PROT_WRITE); 4. eine der Optionen MAP_SHARED oder MAP_PRIVATE (siehe unten); 5. einen Dateideskriptor; 6. die Anfangsposition (offset) des einzublendenden Dateibereichs (muss ein Vielfaches der Seitengröße sein). ❐ Als Resultatwer t liefer t mmap die tatsächliche Anfangsadresse des Speicherbereichs (üblicherweise ein Vielfaches der Seitengröße) bzw. im Fehlerfall den Wer t MAP_FAILED. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.4 In den Speicher eingeblendete Dateien Beispiel #include <fcntl.h> #include <sys/stat.h> #include <sys/mman.h> // open, O_RDWR // fstat, struct stat // mmap, PROT_*, MAP_* // Ersetze in der Datei argv[1] ’\n’ durch ’\r’. int main (int argc, char* argv []) { // Datei zum Lesen und Schreiben (O_RDWR) öffnen. int fd = open(argv[1], O_RDWR); if (fd < 0) ...... // Größe der Datei bestimmen. struct stat s; if (fstat(fd, &s) < 0) ...... int n = s.st_size; // Datei einblenden. char* p = (char*) mmap(0, n, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if (p == MAP_FAILED) ...... 89 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.4 In den Speicher eingeblendete Dateien 90 // Datei verändern. for (; n−−; p++) if (*p == ’\n’) *p = ’\r’; } Einblenden privater Kopien ❐ Verwendet man anstelle von MAP_SHARED die Option MAP_PRIVATE, so wird eine private Kopie der Datei eingeblendet, d. h. Schreibzugriffe auf den Speicherbereich haben keine Auswirkung auf die Datei. ❐ Um Speicherplatz zu sparen und unnötiges Kopieren zu vermeiden, werden Speicherseiten vom Betriebssystem erst dann wirklich kopier t, wenn sie vom Prozess veränder t werden (copy on write, dunkelgraue Bereiche in der nachfolgenden Abbildung). text data bss mmap stack C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.4 In den Speicher eingeblendete Dateien 91 Einblenden von Pseudodateien ❐ Neben gewöhnlichen Dateien kann man mit mmap auch Pseudodateien wie z. B. /dev/zero einblenden. ❐ Verwendet man die Option MAP_PRIVATE, so beschafft man sich damit faktisch einen neuen Speicherbereich vom Betriebssystem, der mit Null initialisiert ist: // Speicherbereich der Größe size mit maximal ausgerichteter // Anfangsadresse vom Betriebssystem beschaffen. void* newmem (int size) { static int fd = open("/dev/zero", O_RDWR); if (fd < 0) return 0; void* p = mmap(0, size, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0); if (p == MAP_FAILED) return 0; return p; } ❐ Auf manchen Systemen kann man mit Hilfe der Option MAP_ANONYMOUS auch eine „anonyme“ (d. h. faktisch eine nicht vorhandene) Datei „einblenden“: C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.4 Schnittstelle zum Betriebssystem 2 Dynamische Speicherverwaltung 2.4.4 In den Speicher eingeblendete Dateien 92 #ifdef MAP_ANONYMOUS void* newmem (int size) { void* p = mmap(0, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, −1, 0); if (p == MAP_FAILED) return 0; return p; } #else ...... // Implementierung von newmem wie oben. #endif Vorteil ❐ Ein auf diese Weise beschaffter Speicherbereich kann mit Hilfe des Systemaufrufs munmap unabhängig von anderen Speicherbereichen an das Betriebssystem zurückgegeben werden: // Speicherbereich p der Größe size // ans Betriebssystem zurückgeben. void delmem (void* p, int size) { munmap(p, size); } ❐ Daher ist es vor teilhaft, wenn malloc sehr große Speicheranforderungen direkt durch einzelne Aufrufe von mmap erfüllt und free derar tige Bereiche mit munmap wieder zurückgibt (→ Übungsaufgabe). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.5 Buddy-Verfahren 2 Dynamische Speicherverwaltung 2.5.1 Prinzip 93 2.5 Buddy-Verfahren 2.5.1 Prinzip Ausgangssituation ❐ Gegeben sei ein initialer Speicherbereich Sn , der aus 2n elementaren Zellen besteht. ❐ Gegeben seien außerdem leere Freispeicherlisten F0 , . . ., Fn−1 sowie eine Freispeicherliste Fn , die den initialen Bereich Sn enthält. ❐ Jede Freispeicherliste Fi kann freie Bereiche Si der Größe 2i enthalten. ❐ Beispiel für n = 4: F0 F1 F2 F3 F4 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.5 Buddy-Verfahren 2 Dynamische Speicherverwaltung 2.5.1 Prinzip 94 Zuteilung von Speicher ❐ Um eine Speicheranforderung der Größe m zu erfüllen, wird mit Hilfe der Freispeicherlisten zunächst ein möglichst kleiner freier Bereich Si mit m ≤ 2i ermittelt und aus seiner Freispeicherliste Fi entfernt. ❐ Anschließend wird der Bereich Si ggf. so lange halbiert, bis man einen minimalen Bereich Sj mit 2j −1 < m ≤ 2j gefunden hat. ❐ Bei jeder Halbierung eines Bereichs Sk entstehen jeweils zwei gleichgroße Teilbereiche (buddies) Sk −1 , von denen einer ggf. weiter halbiert wird, während der andere in die Freispeicherliste Fk −1 eingetragen wird. ❐ Beispiel 1: Zustand nach Zuteilung eines Bereichs S1 der Größe 21 : F0 F1 F2 F3 F4 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.5 Buddy-Verfahren 2 Dynamische Speicherverwaltung 2.5.1 Prinzip ❐ Beispiel 2: Zustand nach weiterer Zuteilung zweier Bereiche S2 der Größe 22 : F0 F1 F2 F3 F4 Rückgabe von Speicher ❐ Bei der Rückgabe eines Speicherbereichs Si wird überprüft, ob sein „Bruder“ S′i ebenfalls frei ist. ❐ Wenn ja, wird S′i aus der Freispeicherliste Fi entfernt und mit Si zu einem freien Bereich Si +1 zusammengefasst. ❐ Dies wird so lange wiederholt, bis der entsprechende Bruder nicht mehr frei ist. ❐ Der resultierende Bereich Sj wird abschließend in die Freispeicherliste Fj eingetragen. 95 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.5 Buddy-Verfahren 2 Dynamische Speicherverwaltung 2.5.1 Prinzip ❐ Beispiel 3: Zustand nach Rückgabe des linken Bereichs der Größe 22 : F0 F1 F2 F3 F4 ❐ Beispiel 4: Zustand nach Rückgabe des Bereichs der Größe 21 : F0 F1 F2 F3 F4 96 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.5 Buddy-Verfahren 2 Dynamische Speicherverwaltung 2.5.3 Anmerkungen 97 2.5.2 Details ❐ Analog zu § 2.3.2, befindet sich am Anfang jedes zugeteilten Bereichs ein „unsichtbares“ Größenfeld inklusive Indikatorbit. ❐ Die Freispeicherlisten werden, ebenfalls analog zu § 2.3.2, als doppelt verkettete Ringlisten verwaltet. ❐ Wenn a die relative Adresse eines Speicherbereichs Si darstellt, so findet man mit a ^ (1<<i) sehr effizient die relative Adresse seines Bruders S′i . 2.5.3 Anmerkungen ❐ Da im ursprünglichen Algorithmus von Knuth für benutzte Bereiche kein Größenfeld verwaltet wird, muss bei der Rückgabe eines Bereichs nicht nur seine Anfangsadresse, sondern auch seine Größe angegeben werden. ❐ Trotz des fehlenden Größenfelds wird davon ausgegangen, dass am Anfang jedes zugeteilten Bereichs Platz für ein Indikatorbit ist. ❐ Trifft diese Annahme nicht zu, so muss man die Indikatorbits in einem separaten Bitvektor speichern. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.5 Buddy-Verfahren 2 Dynamische Speicherverwaltung 2.5.4 Bewertung 2.5.4 Bewertung ❐ Das Verfahren ist relativ elegant und effizient. ❐ Allerdings geht durch die Rundung auf Zweier potenzen zum Teil viel Platz verloren. 98 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 2.6 Literaturhinweise 2 Dynamische Speicherverwaltung 99 2.6 Literaturhinweise ❐ D. E. Knuth: The Art of Computer Programming. Volume I: Fundamental Algorithms (Second Edition). Addison-Wesley, 1973. ❐ A. V. Aho, R. Sethi, J. D. Ullman: Compilers. Principles, Techniques, and Tools. Addison-Wesley, Reading, MA, 1986. ❐ P. R. Wilson, M. S. Johnstone, M. Neely, D. Boles: “Dynamic Storage Allocation: A Survey and Critical Review.” In: H. Baker (ed.): Memor y Management (Int. Workshop IWMM 95; Kinross, Scotland, September 1995; Proceedings). Springer-Verlag, Lecture Notes in Computer Science 986, 1995, 1− −116. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.1 Vorbemerkungen 3 Systemprogrammierung in C und C++ (Teil 2) 100 3 Systemprogrammierung in C und C++ (Teil 2) 3.1 Vorbemerkungen ❐ Zur Erinnerung: Wir verwenden C++ nicht als objektorientier te Programmiersprache, sondern als besseres C . ❐ Zwischen Strukturen (struct) und Klassen (class) besteht kein wesentlicher Unterschied. ❐ Formal ist eine Struktur eine Klasse, deren Elemente (und Basisklassen) standardmäßig public sind. ❐ Entsprechend ist eine Union (union) eine Klasse, deren Elemente standardmäßig public sind und deren Objekte zu jedem Zeitpunkt genau eines der Datenelemente enthalten. ❐ Aufgrund der hohen Komplexität von C++ sind die nachfolgenden Ausführungen zum Teil bewusst vereinfacht und unvollständig. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.2 Konstruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.2.1 Beispiel: Rationale Zahlen 101 3.2 Konstruktoren 3.2.1 Beispiel: Rationale Zahlen // Definition: struct Rational { // Datenelemente (numerator, denominator). int num, den; // Konstruktoren. Rational () { num = 0; den = 1; } Rational (int n) { num = n; den = 1; } Rational (int n, int d) { num = n; den = d; } }; // Verwendung: Rational r1 = Rational(); Rational r2 = Rational(2); Rational r3 = Rational(1, 2); // Oder: Rational r2 = 2; // Oder: Rational r1; // Ohne Klammern! Rational r2(2); Rational r3(1, 2); C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.2 Konstruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.2.2 Erläuterungen 102 3.2.2 Erläuterungen ❐ Ein Konstruktor besitzt denselben Namen wie seine Klasse bzw. Struktur. ❐ Ebenso wie Funktionen, können Konstruktoren überladen werden. ❐ Ein Konstruktor „konstruiert“ ein Objekt seines Typs, indem er seine Datenelemente geeignet initialisier t . Er kümmer t sich nicht um die eigentliche Erzeugung des Objekts, d. h. um die Bereitstellung seines Speicherplatzes. ❐ Unter Umständen erzeugt ein Konstruktor im Rahmen der Initialisierung seiner Datenelemente weitere Objekte, für deren Initialisierung ihrerseits Konstruktoren aufgerufen werden. ❐ Konstruktoren können explizit oder implizit aufgerufen werden: ❍ Besitzt ein Typ T einen Konstruktor, der ohne Argumente aufgerufen werden kann (entweder, weil er keine Parameter besitzt, oder weil alle Parameter DefaultArgumente besitzen), so wird er automatisch bei jeder Deklaration einer Variablen mit Typ T aufgerufen, sofern die Variable nicht explizit initialisiert wird. ❍ Besitzt T einen Konstruktor, der mit einem Argument eines beliebigen Typs U aufgerufen werden kann, so wird er bei Bedarf automatisch aufgerufen, um ein Objekt mit Typ U in ein Objekt mit Typ T umzuwandeln (implizite Typumwandlung, vgl. § 3.4). Will man dies verhindern, muss man den Konstruktor mit dem Schlüsselwor t explicit vereinbaren. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.2 Konstruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.2.3 Verwendung von Element-Initialisierern 3.2.3 Verwendung von Element-Initialisierern Beispiel struct Rational { // Datenelemente. const int num, den; // Konstruktoren. Rational () : num(0), den(1) {} Rational (int n) : num(n), den(1) {} Rational (int n, int d) : num(n), den(d) {} }; 103 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.2 Konstruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.2.3 Verwendung von Element-Initialisierern 104 Erläuterungen ❐ Element-Initialisierer stellen Konstruktoraufrufe für die Datenelemente der Klasse/Struktur dar, die vor der Ausführung des Konstruktorrumpfs (in der Reihenfolge, in der die Datenelemente deklarier t wurden!) ausgeführt werden. ❐ Wenn es für ein Datenelement keinen Element-Initialisierer gibt, wird für dieses Element je nach Typ entweder sein parameterloser Konstruktor ausgeführ t (den es in diesem Fall geben muss!), oder das Element bleibt uninitialisiert. ❐ Durch die explizite Verwendung von Element-Initialisierern kann diese eventuelle Standard-Initialisierung von Datenelementen vermieden werden. ❐ Falls ein Datenelement keinen parameterlosen Konstruktor besitzt, muss man es mit einem Element-Initialisierer initialisieren. ❐ Ebenso muss man Referenzelemente und konstante Datenelemente mit ElementInitialisierern initialisieren, weil man ihnen nur auf diese Weise Wer te zuordnen kann. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.2 Konstruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.2.4 Separate Deklaration und Definition von . . . 105 Beispiel: Paare rationaler Zahlen struct RationalPair { Rational x, y; RationalPair (Rational x, Rational y) : x(x), y(y) {} RationalPair (int a, int b, int c, int d) : x(a, b), y(c, d) {} }; 3.2.4 Separate Deklaration und Definition von Konstruktoren ❐ Einfache Konstruktoren kann man direkt in ihrer Typdefinition definieren (d. h. implementieren). Sie sind in diesem Fall automatisch inline deklariert. ❐ Komplizier tere Konstruktoren werden in der Typdefinition meist nur deklariert und später separat definier t . ❐ Dies ist insbesondere dann sinnvoll, wenn die Typdefinition in einer separaten Definitionsdatei (header file) steht. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.2 Konstruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.2.4 Separate Deklaration und Definition von . . . Beispiel // Typdefinition. struct Rational { // Datenelemente. const int num, den; // Deklaration der Konstruktoren. Rational (); Rational (int n); Rational (int n, int d); }; ...... // Definition der Konstruktoren. Rational::Rational () : num(0), den(1) {} Rational::Rational (int n) : num(n), den(1) {} Rational::Rational (int n, int d) : num(n), den(d) {} 106 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.3 Überladene Operatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.3.1 Definition durch gewöhnliche Funktionen 107 3.3 Überladene Operatoren 3.3.1 Definition durch gewöhnliche Funktionen // Summe Rational return } Rational return } bzw. Produkt von x und y (binäre Operatoren). operator+ (Rational x, Rational y) { Rational(x.num * y.den + y.num * x.den, x.den * y.den); operator* (Rational x, Rational y) { Rational(x.num * y.num, x.den * y.den); // Negation bzw. Kehrwert von x (unäre Operatoren). Rational operator− (Rational x) { return Rational(−x.num, x.den); } Rational operator˜ (Rational x) { return Rational(x.den, x.num); } // Differenz bzw. Quotient von x und y (binäre Operatoren). Rational operator− (Rational x, Rational y) { return x + −y; } Rational operator/ (Rational x, Rational y) { return x * ˜y; } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.3 Überladene Operatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.3.3 Definition durch Elementfunktionen 3.3.2 Verwendung Rational p(1), q(2); Rational r = (p + ˜q) * (p − p/q); 3.3.3 Definition durch Elementfunktionen („Methoden“) struct Rational { // Datenelemente. const int num, den; // Konstruktoren. Rational () : num(0), den(1) {} Rational (int n) : num(n), den(1) {} Rational (int n, int d) : num(n), den(d) {} 108 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.3 Überladene Operatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.3.3 Definition durch Elementfunktionen 109 // Operatoren: // Summe Rational return } Rational return } bzw. Produkt von *this und that (binäre Operatoren). operator+ (Rational that) const { Rational(num*that.den + that.num*den, den*that.den); operator* (Rational that) const { Rational(num * that.num, den * that.den); // Negation bzw. Kehrwert von *this (unäre Operatoren). Rational operator− () const { return Rational(−num, den); } Rational operator˜ () const { return Rational(den, num); } // Differenz bzw. Quotient von *this und that (binäre Operatoren). Rational operator− (Rational that) const { return *this + −that; } Rational operator/ (Rational that) const { return *this * ˜that; } }; C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.3 Überladene Operatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.3.4 Erläuterungen 110 3.3.4 Erläuterungen ❐ Das Schlüsselwor t operator, gefolgt von einem Operatorsymbol, entspricht syntaktisch einem Funktionsnamen. ❐ Das Schlüsselwor t const nach der Parameterliste einer Elementfunktion zeigt an, dass die Funktion das aktuelle Objekt *this nicht verändern darf, d. h. dass sie auch für ein konstantes Objekt aufgerufen werden darf. Da dies die Anwendungsmöglichkeiten der Funktion potentiell erhöht, wird const üblicherweise so oft wie möglich angegeben. Andererseits darf eine const-Funktion ihrerseits auch nur const-Funktionen auf dem aktuellen Objekt *this aufrufen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.3 Überladene Operatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.3.4 Erläuterungen ❐ Ein Ausdruck wie z. B. (p + ˜q) * (p − p/q) mit Variablen p und q vom Typ Rational wird vom Compiler entweder in gewöhnliche Funktionsaufrufe oder in Aufrufe von Elementfunktionen (oder eine passende Mischform) transformiert: // Falls Operatoren durch gewöhnliche // Funktionen definiert wurden: operator*( operator+(p, operator˜(q)), operator−(p, operator/(p, q)) ) // Falls Operatoren durch // Elementfunktionen definiert wurden: p.operator+(q.operator˜()).operator*( p.operator−(p.operator/(q)) ) 111 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.3 Überladene Operatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.3.4 Erläuterungen 112 ❐ Normalerweise ist die Definition durch gewöhnliche Funktionen und die Definition durch Elementfunktionen äquivalent. ❐ Die Operatoren = (Zuweisung), [] (Indexoperation), () (Funktionsaufruf) und −> (Elementauswahl über Zeiger) können jedoch nur als Elementfunktionen implementier t werden. ❐ Der Operator −> wird hierbei als unärer Postfix-Operator interpretier t, auf dessen Resultat der Operator erneut angewandt wird (vgl. § 3.8.2). ❐ Die Operatoren :: (Qualifizierung von Namen), . (Elementauswahl), .* (Elementauswahl durch sog. Elementzeiger) und ?: (Fallunterscheidung) können grundsätzlich nicht überladen werden. ❐ Man kann weder neue Operatorsymbole einführen noch die Regeln für Vorrang und Assoziativität der vorhandenen Operatoren verändern. ❐ Mindestens ein Operand eines überladenen Operators muss ein benutzerdefinier ter Typ (d. h. eine Klasse/Struktur oder ein Aufzählungstyp) sein, d. h. die Bedeutung der eingebauten Operatoren kann nicht veränder t werden. ❐ Ebenso wie bei gewöhnlichen Funktionsaufrufen, ist es undefiniert, ob vor dem Aufruf eines binären Operators zuerst sein linker oder sein rechter Operand ausgewertet wird. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.4 Benutzerdefinier te Typumwandlungen 3 Systemprogrammierung in C und C++ (Teil 2) 113 3.4 Benutzerdefinierte Typumwandlungen ❐ Ein (nicht als explicit deklarierter) Konstruktor eines Typs T, der mit einem Argument eines beliebigen Typs U aufgerufen werden kann, definiert gleichzeitig eine implizit anwendbare Typumwandlung von U nach T. ❐ Umgekehr t kann man in der Typdefinition von T mit Hilfe eines Konver tierungsoperators eine implizite Umwandlung von T in einen beliebigen Zieltyp V deklarieren. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.4 Benutzerdefinier te Typumwandlungen 3 Systemprogrammierung in C und C++ (Teil 2) 114 Beispiel // Definition. struct Rational { // Datenelemente. const int num, den; // Konstruktor, der mit 0, 1 oder 2 Argumenten aufrufbar ist. // Definiert eine implizite Umwandlung von int nach Rational. Rational (int n = 0, int d = 1) : num(n), den(d) {} // Implizite Umwandlung von Rational nach double. operator double () const { return (double)num/den; } // Implizite Umwandlung nach bool zur Verwendung in Bedingungen. operator bool () const { return num != 0; } }; // Verwendung. Rational r = 4; if (r) { cout << sqrt(r) << endl; } // Umwandlung int −> Rational. // Umwandlung Rational −> bool. // Umwandlung Rational −> double. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.4 Benutzerdefinier te Typumwandlungen 3 Systemprogrammierung in C und C++ (Teil 2) 115 Anmerkungen ❐ Die letzten Parameter einer Funktion oder eines Konstruktors können DefaultArgumente besitzen, die verwendet werden, wenn beim (expliziten oder impliziten) Aufruf keine zugehörigen Argumente angegeben sind. ❐ Umwandlungen durch Konstruktoren und Umwandlungen durch Konvertierungsoperatoren sind grundsätzlich äquivalent. ❐ Benutzerdefinier te Typumwandlungen sind nicht transitiv, d. h. auf jeden Teilausdruck eines Ausdrucks wird maximal eine angewandt. ❐ Die übermäßige Verwendung benutzerdefinier ter Typumwandlungen kann leicht zu unerwünschten Mehrdeutigkeiten führen, z. B.: Rational r = ...; double d = r + 3.5; Hier kann r entweder (wie erwünscht) nach double konver tier t werden oder aber nach bool, was aufgrund der „üblichen arithmetischen Umwandlungen“ wie int behandelt wird und daher ebenfalls mit dem double-Wer t 3.5 addier t werden kann! C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.4 Benutzerdefinier te Typumwandlungen 3 Systemprogrammierung in C und C++ (Teil 2) 116 ❐ Hat man zusätzlich operator+ für Rational-Wer te definier t (vgl. § 3.3), so könnte auch 3.5 nach int und dann durch einen impliziten Konstruktoraufruf nach Rational konver tier t werden, um anschließend diesen Operator aufzurufen. (Ob das Ergebnis vom Typ Rational anschließend eindeutig nach double konver tier t werden kann, ist für die Auswertung der Addition irrelevant.) C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.5 Destruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.5.1 Beispiel 117 3.5 Destruktoren 3.5.1 Beispiel // Folge von Zufallszahlen mit zufälliger Länge. struct RandSeq { int length; // Länge der Folge. int* elems; // Dynamisch erzeugtes Array mit den Elementen. // Konstruktor erzeugt und füllt das dynamische Array mit RandSeq (int n) { // Zufallszahlen aus der Menge [0, n). length = rand() % n; elems = new int [length]; for (int i = 0; i < length; i++) elems[i] = rand() % n; } // Destruktor vernichtet das dynamische Array wieder. ˜RandSeq () { delete [] elems; } // Länge bzw. i−tes Element abfragen. int len () const { return length; } int get (int i) const { return elems[i]; } }; C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.5 Destruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.5.1 Beispiel // Exemplarische Verwendung. int main (int argc, char* argv []) { // Das erste Kommandozeilenargument // muss eine natürliche Zahl n sein. int n = atoi(argv[1]); // n Zufallsfolgen erzeugen und ausgeben. for (int i = 0; i < n; i++) { // Zufallsfolge als lokales Objekt r "konstruieren". RandSeq r(n); // Elemente der Folge ausgeben. for (int j = 0; j < r.len(); j++) { cout << r.get(j) << " "; } cout << endl; // Am Ende des Blocks wird das lokale Objekt r // "zerstört", d. h. sein Destruktor aufgerufen. } } 118 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.5 Destruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.5.2 Erläuterungen 119 3.5.2 Erläuterungen ❐ Ein Destruktor ist eine parameterlose (Pseudo-) Elementfunktion, deren Name aus einer Tilde und dem Namen des Typs besteht. ❐ Ein Destruktor „zerstör t“ ein Objekt seines Typs, indem er ggf. erforderliche Aufräumarbeiten ausführ t. Er kümmert sich nicht um die eigentliche Vernichtung des Objekts, d. h. um die Freigabe seines Speicher platzes. ❐ Unter Umständen vernichtet ein Destruktor im Rahmen seiner Aufräumarbeiten jedoch andere Objekte, die typischerweise von einem Konstruktor seines Typs dynamisch erzeugt wurden. ❐ Destruktoren werden in aller Regel implizit aufgerufen, bevor ein Objekt vernichtet wird: ❍ Globale Variablen werden am Ende der Programmausführung vernichtet. ❍ Lokale Variablen werden am Ende des Blocks vernichtet, in dem sie deklarier t wurden. ❍ Mit new erzeugte dynamische Objekte werden mittels delete vernichtet. ❍ Elementvariablen einer Struktur werden vernichtet, wenn das umgebende Strukturobjekt vernichtet wird. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.5 Destruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.5.3 Problem 3.5.3 Problem // Elemente der Zufallsfolge s ausgeben. void print (RandSeq s) { for (int j = 0; j < s.len(); j++) { cout << s.get(j) << " "; } cout << endl; // Am Ende der Funktion wird das lokale Objekt s // "zerstört", d. h. sein Destruktor aufgerufen. } int main (int argc, char* argv []) { int n = atoi(argv[1]); for (int i = 0; i < n; i++) { RandSeq r(n); print(r); // Am Ende des Blocks wird das lokale Objekt r // "zerstört", d. h. sein Destruktor aufgerufen. } } 120 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.5 Destruktoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.5.3 Problem 121 ❐ Beim Aufruf der Funktion print wird − mit Hilfe eines implizit definierten Kopierkonstruktors (vgl. § 3.6.1) − ein neues Objekt s mit Typ RandSeq als Kopie des Objekts r erzeugt. Demnach verweisen r.elems und s.elems anschließend auf dasselbe dynamische Array. ❐ Am Ende der Funktion wird für das Objekt s der Destruktor ˜RandSeq aufgerufen, der dieses dynamische Array vernichtet. ❐ Am Ende des for-Blocks in main wird wie zuvor der Destruktor für das Objekt r aufgerufen, der dieses dynamische Array erneut vernichten will, was zu undefiniertem Verhalten führt. ❐ Außerdem dürfte man nach dem Aufruf von print nicht mehr auf die Elemente von r zugreifen. ❐ Lösung: Explizite Definition des Kopierkonstruktors (vgl. § 3.6.1). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.6 Kopieroperatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.6.1 Kopierkonstruktoren 122 3.6 Kopieroperatoren 3.6.1 Kopierkonstruktoren struct RandSeq { int length; // Länge der Folge. int* elems; // Dynamisch erzeugtes Array mit den Elementen. // Konstruktor erzeugt und füllt das dynamische Array mit RandSeq (int n) { // Zufallszahlen aus der Menge [0, n). length = rand() % n; elems = new int [length]; for (int i = 0; i < length; i++) elems[i] = rand() % n; } // Kopierkonstruktor erzeugt eine "tiefe" Kopie des Arrays von RandSeq (const RandSeq& that) : length(that.length) { // that. elems = new int [length]; for (int i = 0; i < length; i++) elems[i] = that.elems[i]; } // Destruktor vernichtet das dynamische Array wieder. ˜RandSeq () { delete [] elems; } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.6 Kopieroperatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.6.1 Kopierkonstruktoren 123 // Länge bzw. i−tes Element abfragen. int len () const { return length; } int get (int i) const { return elems[i]; } }; Erläuterungen ❐ Ein Kopierkonstruktor eines Typs T ist ein Konstruktor mit einem Parameter des Typs const T& oder T&. ❐ Wenn für einen Typ kein expliziter Kopierkonstruktor definier t ist, wird vom Compiler ein impliziter Kopierkonstruktor definier t, der alle Elementvariablen des Typs kopier t. Hierfür werden ggf. die Kopierkonstruktoren der Elementvariablen aufgerufen. ❐ Der (explizit oder implizit definierte) Kopierkonstruktor eines Typs wird u. a. bei der Übergabe eines Objekts als Parameter oder als Funktionsresultat aufgerufen. (Deshalb darf der Kopierkonstruktor selbst keinen Parameter mit Typ T besitzen, weil er sonst bei der Übergabe dieses Parameters ebenfalls aufgerufen werden müsste.) ❐ Vermeidbare Aufrufe des Kopierkonstruktors dürfen jedoch vom Compiler eliminiert werden. ❐ Außerdem kann man selbst Aufrufe des Kopierkonstruktors vermeiden, indem man Parameter als Referenzen (normalerweise auf Konstanten) deklarier t (vgl. § 1.4.4). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.6 Kopieroperatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.6.1 Kopierkonstruktoren Beispiel Rational operator* (Rational x, Rational y) { return Rational(x.num * y.num, x.den * y.den); } Rational a(1, 2); Rational b = a * Rational(3, 4); 124 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.6 Kopieroperatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.6.1 Kopierkonstruktoren 125 ❐ Hier finden prinzipiell folgende Konstruktoraufrufe statt: 1. Konstruktion von a durch direkte Initialisierung: Rational (int, int) 2. Parameterübergabe von a an operator*, d. h. Konstruktion des Parameters x als Kopie von a: Rational (const Rational&) 3. Konstruktion eines anonymen temporären Objekts durch direkte Initialisierung: Rational (int, int) 4. Parameterübergabe dieses Objekts an operator*, d. h. Konstruktion des Parameters y als Kopie dieses Objekts: Rational (const Rational&) 5. Konstruktion des Resultatwer ts von operator* durch direkte Initialisierung: Rational (int, int) 6. Rückgabe dieses Resultatwer ts, d. h. Konstruktion eines temporären Objekts als Kopie dieses Resultatwer ts: Rational (const Rational&) 7. Konstruktion von b als Kopie dieses temporären Objekts: Rational (const Rational&) C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.6 Kopieroperatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.6.1 Kopierkonstruktoren 126 ❐ Durch einfache Optimierungen kann der Compiler jedoch die meisten Aufrufe des Kopierkonstruktors eliminieren: 1. Konstruktion von a durch direkte Initialisierung: Rational (int, int) 2. Parameterübergabe von a an operator*, d. h. Konstruktion des Parameters x als Kopie von a: Rational (const Rational&) 3. Konstruktion des Parameters y durch direkte Initialisierung: Rational (int, int) 4. Konstruktion des Resultatwer ts b von operator* durch direkte Initialisierung: Rational (int, int) ❐ Bei einer Deklaration von operator* mit Referenzparametern Rational operator* (const Rational& x, const Rational& y) { return Rational(x.num * y.num, x.den * y.den); } entfällt auch noch der Aufruf des Kopierkonstruktors zur Initialisierung des Parameters x. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.6 Kopieroperatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.6.2 Weiteres Problem 127 3.6.2 Weiteres Problem int main () { RandSeq r(...); RandSeq s(...); ...... r = s; ...... } ❐ Durch die Zuweisung r = s wird r.elems durch s.elems überschrieben. ❐ Am Ende der Funktion werden die Destruktoren für r und s aufgerufen, die jetzt beide dasselbe Array s.elems vernichten (wollen), während das ursprüngliche Array r.elems für immer als „Speicherleiche“ übrig bleibt. ❐ Lösung: Explizite Definition des kopierenden Zuweisungsoperators (vgl. § 3.6.3). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.6 Kopieroperatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.6.3 Kopierende Zuweisungsoperatoren 128 3.6.3 Kopierende Zuweisungsoperatoren struct RandSeq { int length; // Länge der Folge. int* elems; // Dynamisch erzeugtes Array mit den Elementen. // Konstruktor erzeugt und füllt das dynamische Array mit RandSeq (int n) { // Zufallszahlen aus der Menge [0, n). length = rand() % n; elems = new int [length]; for (int i = 0; i < length; i++) elems[i] = rand() % n; } // Kopierkonstruktor erzeugt eine "tiefe" Kopie des Arrays von RandSeq (const RandSeq& that) : length(that.length) { // that. elems = new int [length]; for (int i = 0; i < length; i++) elems[i] = that.elems[i]; } // Destruktor vernichtet das dynamische Array wieder. ˜RandSeq () { delete [] elems; } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.6 Kopieroperatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.6.3 Kopierende Zuweisungsoperatoren 129 // Kopierender Zuweisungsoperator vernichtet das eigene Array // und erstellt eine "tiefe" Kopie des Arrays von that. RandSeq& operator= (const RandSeq& that) { // Vorsicht bei Selbstzuweisungen! if (this != &that) { // Vorsicht: Erst das neue Array erzeugen (was prinzipiell // fehlschlagen kann), bevor das alte vernichtet wird! int* es = new int [that.length]; delete [] elems; elems = es; length = that.length; for (int i = 0; i < length; i++) elems[i] = that.elems[i]; } // Selbstreferenz zurückliefern. return *this; } // Länge bzw. i−tes Element abfragen. int len () const { return length; } int get (int i) const { return elems[i]; } }; C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.6 Kopieroperatoren 3 Systemprogrammierung in C und C++ (Teil 2) 3.6.3 Kopierende Zuweisungsoperatoren 130 Erläuterungen ❐ Ein kopierender Zuweisungsoperator eines Typs T ist ein überladener Operator = mit einem Parameter des Typs const T&, T& oder T, der anhand der üblichen Regeln für überladene Operatoren aufgerufen wird. ❐ Wenn für einen Typ kein expliziter kopierender Zuweisungsoperator definiert ist, wird vom Compiler ein impliziter Operator definiert, der für alle Elementvariablen des Typs eine kopierende Zuweisung ausführt. Hierfür werden ggf. die kopierenden Zuweisungsoperatoren der Elementvariablen aufgerufen. ❐ Im Gegensatz zu vielen anderen Programmiersprachen, besteht in C++ ein wesentlicher Unterschied zwischen der Initialisierung eines Objekts (durch einen Konstruktor) und der Zuweisung an ein Objekt (durch einen Zuweisungsoperator). ❐ Da das Zielobjekt im einen Fall uninitialisiert und im anderen Fall bereits initialisiert ist, müssen Konstruktoren und Zuweisungsoperatoren normalerweise unterschiedliche Anweisungen ausführen. ❐ Häufig enthält ein kopierender Zuweisungsoperator sowohl Teile eines Destruktors als auch Teile eines Konstruktors. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.7 Beispiel: Dynamische Zeichenketten 3 Systemprogrammierung in C und C++ (Teil 2) 3.7.1 Formulierung in Java 3.7 Beispiel: Dynamische Zeichenketten 3.7.1 Formulierung in Java class Test { // Zeichenkette s ausgeben. public static void print (String s) { System.out.println(s); } // Hauptprogramm. public static void main (String [] args) { String s1 = args[0], s2 = args[1], s; if (s1.equals(s2)) s = " == "; else s = " != "; // Verkettung von s1, s und s2 an Methode print übergeben. print(s1 + s + s2); } } 131 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.7 Beispiel: Dynamische Zeichenketten 3 Systemprogrammierung in C und C++ (Teil 2) 3.7.2 Nachbildung in C 132 3.7.2 Nachbildung in C // Typdefinition. typedef const char* String; // Zeichenkette s ausgeben. void print (String s) { printf("%s\n", s); } // Hauptprogramm. int main (int argc, char* argv []) { String s1 = argv[1], s2 = argv[2], s; if (strcmp(s1, s2) == 0) s = " == "; else s = " != "; // Verkettung von s1, s und s2 an Funktion print übergeben. { // Lokaler Block, um Hilfsvariable t deklarieren zu können. char* t = malloc(strlen(s1) + strlen(s) + strlen(s2) + 1); if (!t) exit(1); strcpy(t, s1); strcat(t, s); strcat(t, s2); print(t); free(t); } } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.7 Beispiel: Dynamische Zeichenketten 3 Systemprogrammierung in C und C++ (Teil 2) 3.7.3 Realisierung in C++ 3.7.3 Realisierung in C++ // Typdefinition. struct String { // Interne Repräsentation. const char* str; // str mit dynamisch erzeugter Verkettung // von s1 und ggf. s2 initialisieren. void init (const char* s1, const char* s2 = 0) { int len = strlen(s1); if (s2) len += strlen(s2); char* s = new char [len + 1]; strcpy(s, s1); if (s2) strcat(s, s2); str = s; } // Normaler Konstruktor. String (const char* s1 = "", const char* s2 = 0) { init(s1, s2); } 133 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.7 Beispiel: Dynamische Zeichenketten 3 Systemprogrammierung in C und C++ (Teil 2) 3.7.3 Realisierung in C++ // Kopierkonstruktor. String (const String& that) { init(that.str); } // Kopierender Zuweisungsoperator. String& operator= (const String& that) { if (this != &that) { const char* s = str; init(that.str); delete [] s; } return *this; } // Destruktor. ˜String () { delete [] str; } }; 134 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.7 Beispiel: Dynamische Zeichenketten 3 Systemprogrammierung in C und C++ (Teil 2) 3.7.3 Realisierung in C++ // Verkettungs− und Vergleichsoperatoren. String operator+ (const String& s1, const String& s2) { return String(s1.str, s2.str); } bool operator== (const String& s1, const String& s2) { return strcmp(s1.str, s2.str) == 0; } Anwendung // Zeichenkette s ausgeben. void print (const String& s) { cout << s.str << endl; } // Hauptprogramm. int main (int argc, char* argv []) { String s1 = argv[1], s2 = argv[2], s; if (s1 == s2) s = " == "; else s = " != "; // Verkettung von s1, s und s2 an Funktion print übergeben. print(s1 + s + s2); } 135 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.7 Beispiel: Dynamische Zeichenketten 3 Systemprogrammierung in C und C++ (Teil 2) 3.7.3 Realisierung in C++ Ablauf ❐ Konstruktion von s1 durch String (const char*) → dynamische Kopie s1.str von argv[1] ❐ Konstruktion von s2 durch String (const char*) → dynamische Kopie s2.str von argv[2] ❐ Konstruktion von s durch String () → dynamische Kopie s.str von "" ❐ Parameterübergabe von s1 und s2 an operator== per Referenz ❐ Implizite Konvertierung von " == " oder " != " in ein temporäres Objekt t1 mit Typ String durch String (const char*) → dynamische Kopie t1.str von " == " oder " != " ❐ Zuweisung von t1 an s durch operator= → Freigabe von s.str → dynamische Kopie s.str von t1.str ❐ Destruktion des temporären Objekts t1 durch ˜String → Freigabe von t1.str 136 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.7 Beispiel: Dynamische Zeichenketten 3 Systemprogrammierung in C und C++ (Teil 2) 3.7.3 Realisierung in C++ ❐ Parameterübergabe von s1 und s an operator+ per Referenz ❐ Konstruktion des Resultats t2 von s1 + s durch String (const char*, const char*) → dynamische Verkettung t2.str von s1.str und s.str ❐ Parameterübergabe von t2 und s2 an operator+ per Referenz ❐ Konstruktion des Resultats t3 von t2 + s2 durch String (const char*, const char*) → dynamische Verkettung t3.str von t2.str und s2.str ❐ Parameterübergabe von t3 an print per Referenz ❐ Ausführung von print ❐ Destruktion von t3 durch ˜String → Freigabe von t3.str ❐ Destruktion von t2 durch ˜String → Freigabe von t2.str ❐ Destruktion von s durch ˜String → Freigabe von s.str ❐ Destruktion von s2 durch ˜String → Freigabe von s2.str ❐ Destruktion von s1 durch ˜String → Freigabe von s1.str 137 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.8 Ver packte Zeiger (smart pointers) 3 Systemprogrammierung in C und C++ (Teil 2) 3.8.1 Definition mit „nackten“ Zeigern 3.8 Verpackte Zeiger (smart pointers) Beispiel: Lineare Listen 3.8.1 Definition mit „nackten“ Zeigern Datenstrukturen // Elementtyp. typedef int T; // Listenknoten. struct Node { T head; Node* tail; // Erstes Element. // Restliste. // Konstruktor. Node (T h, Node* t) : head(h), tail(t) {} }; // Liste. typedef Node* List; 138 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.8 Ver packte Zeiger (smart pointers) 3 Systemprogrammierung in C und C++ (Teil 2) 3.8.1 Definition mit „nackten“ Zeigern Funktionen // Liste elementweise konstruieren. List cons (T h, List t = 0) { return new Node(h, t); } // Erstes Element liefern. T head (List ls) { return ls−>head; } // Restliste liefern. List tail (List ls) { return ls−>tail; } // Liste komplett freigeben. void disp (List ls) { if (List t = tail(ls)) disp(t); delete ls; } 139 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.8 Ver packte Zeiger (smart pointers) 3 Systemprogrammierung in C und C++ (Teil 2) 3.8.1 Definition mit „nackten“ Zeigern Verwendung // ls1 = [1, 2, 3] List ls1 = cons(1, cons(2, cons(3))); // ls2 = [2, 3] List ls2 = tail(ls1); // i = 3 int i = head(tail(ls2)); // Liste freigeben. disp(ls1); 140 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.8 Ver packte Zeiger (smart pointers) 3 Systemprogrammierung in C und C++ (Teil 2) 3.8.2 Definition mit „ver packten“ Zeigern 141 3.8.2 Definition mit „verpackten“ Zeigern ❐ Der Zeigertyp List wird durch eine Struktur/Klasse ersetzt, die lediglich aus einem Node* besteht. ❐ Damit ein List-Objekt (nahezu) überall verwendet werden kann, wo ein Node*Objekt erwar tet wird und umgekehr t, besitzt List einen Konstruktor mit Parameter Node* zur impliziten Umwandlung von Node* nach List sowie einen Konvertierungsoperator zur impliziten Umwandlung von List nach Node* (vgl. § 3.4). ❐ Da der Zugriffsoperator −> eine Sonderrolle spielt, muss er zusätzlich definiert werden. Ein Ausdruck der Gestalt ls−>... mit einem List-Objekt ls wird dann vom Compiler als ls.operator−>()−>... inter pretiert. ❐ Neben den o. g. Elementfunktionen kann die Klasse List beliebige weitere Elementfunktionen besitzen, z. B. einen parameterlosen Konstruktor. ❐ Der Typ Node sowie die Funktionen cons, head, tail und disp können unveränder t bleiben. ❐ An den durch Kommentare gekennzeichneten Stellen werden jedoch implizit die in List definier ten Umwandlungsfunktionen 1, 2 bzw. 3 aufgerufen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.8 Ver packte Zeiger (smart pointers) 3 Systemprogrammierung in C und C++ (Teil 2) 3.8.2 Definition mit „ver packten“ Zeigern // Listenknoten. struct Node { T head; Node* tail; // Erstes Element. // Restliste. // Konstruktor. Node (T h, Node* t) : head(h), tail(t) {} }; // Liste. struct List { // Nackter Zeiger. Node* ptr; // Umwandlung von und nach Node*. List (Node* p) : ptr(p) {} operator Node* () const { return ptr; } Node* operator−> () const { return ptr; } // Weiterer Konstruktor. List () : ptr(0) {} }; // 1 // 2 // 3 142 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.8 Ver packte Zeiger (smart pointers) 3 Systemprogrammierung in C und C++ (Teil 2) 3.8.2 Definition mit „ver packten“ Zeigern // Liste elementweise konstruieren. List cons (T h, List t = /*1*/0) { return new Node(h, /*2*/t); } // Erstes Element liefern. T head (List ls) { return ls/*3*/−>head; } // Restliste liefern. List tail (List ls) { return ls/*3*/−>tail; } // Liste komplett freigeben. void disp (List ls) { if (List t = tail(ls)) disp(t); delete /*2*/ls; } 143 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.8 Ver packte Zeiger (smart pointers) 3 Systemprogrammierung in C und C++ (Teil 2) 3.8.2 Definition mit „ver packten“ Zeigern 144 ❐ Durch zusätzliche Definition von Kopierkonstruktor, kopierendem Zuweisungsoperator und Destruktor könnten alle relevanten Operationen (Erzeugung, Initialisierung, Zuweisung und Vernichtung von „Zeigern“) überwacht und ggf. redefinier t werden. ❐ Außerdem kann List jetzt als generische Typschablone (template) definiert werden (vgl. § 3.9), was für die ursprüngliche typedef-Deklaration nicht möglich ist. ❐ Anmerkung: Der Typ String aus § 3.7.3 ist eigentlich ein „ver packter“ const char*. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.9 Typ- und Funktionsschablonen (Templates) 3 Systemprogrammierung in C und C++ (Teil 2) 3.9.1 Beispiel: Lineare Listen (vgl. § 3.8.2) 145 3.9 Typ- und Funktionsschablonen (Templates) 3.9.1 Beispiel: Lineare Listen (vgl. § 3.8.2) Problem ❐ Der Elementtyp T kann zwar leicht geändert werden, aber anschließend muss das Programm neu übersetzt werden. ❐ Die Verwendung mehrerer Listen mit unterschiedlichen Elementtypen in einem Programm ist nicht möglich. Lösung ❐ Definition der Typen Node und List als Typschablonen (class templates) und der Funktionen cons, head, tail und disp als Funktionsschablonen (function templates). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.9 Typ- und Funktionsschablonen (Templates) 3 Systemprogrammierung in C und C++ (Teil 2) 3.9.1 Beispiel: Lineare Listen (vgl. § 3.8.2) Datenstrukturen // Listenknoten mit beliebigem Elementtyp T. template <typename T> struct Node { T head; // Erstes Element. Node* tail; // Restliste. Node (T h, Node* t) : head(h), tail(t) {} }; // Liste mit beliebigem Elementtyp T. template <typename T> struct List { Node<T>* ptr; // Nackter Zeiger. // Umwandlung von und nach Node<T>*. List (Node<T>* p) : ptr(p) {} operator Node<T>* () const { return ptr; } Node<T>* operator−> () const { return ptr; } // Weiterer Konstruktor. List () : ptr(0) {} }; 146 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.9 Typ- und Funktionsschablonen (Templates) 3 Systemprogrammierung in C und C++ (Teil 2) 3.9.1 Beispiel: Lineare Listen (vgl. § 3.8.2) Funktionen // Liste elementweise konstruieren. template <typename T> List<T> cons (T h, List<T> t = 0) { return new Node<T>(h, t); } // Erstes Element liefern. template <typename T> T head (List<T> ls) { return ls−>head; } // Restliste liefern. template <typename T> List<T> tail (List<T> ls) { return ls−>tail; } // Liste komplett freigeben. template <typename T> void disp (List<T> ls) { if (List<T> t = tail(ls)) disp(t); delete ls; } 147 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.9 Typ- und Funktionsschablonen (Templates) 3 Systemprogrammierung in C und C++ (Teil 2) 3.9.1 Beispiel: Lineare Listen (vgl. § 3.8.2) Verwendung // i1 = [1, 2, 3], c1 = [’x’, ’y’, ’z’] List<int> i1 = cons(1, cons(2, cons(3))); List<char> c1 = cons(’x’, cons(’y’, cons(’z’))); // i2 = [2, 3], c2 = [’y’, ’z’] List<int> i2 = tail(i1); List<char> c2 = tail(c1); // i = 3, c = ’z’ int i = head(tail(i2)); char c = head(tail(c2)); // Listen freigeben. disp(i1); disp(c1); 148 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.9 Typ- und Funktionsschablonen (Templates) 3 Systemprogrammierung in C und C++ (Teil 2) 3.9.1 Beispiel: Lineare Listen (vgl. § 3.8.2) 149 Erläuterungen ❐ Das Präfix template <typename T> leitet die Definition einer Typ- oder Funktionsschablone ein, aus der der Compiler bei Bedarf entsprechende konkrete Typen (z. B. List<int>) bzw. Funktionen (z. B. int head(List<int>)) erzeugen kann. ❐ Bei der Verwendung einer Typschablone muss der Schablonenparameter T explizit angegeben werden (z. B. List<int>). Ausnahme: Innerhalb einer Schablone selbst ist beispielsweise Node als Typname gleichbedeutend mit Node<T>. ❐ Bei der Verwendung einer Funktionsschablone (z. B. tail(i1)) kann der Compiler den Schablonenparameter T normalerweise aus den Typen der normalen Funktionsparameter ableiten (template argument deduction). ❐ Innerhalb einer Schablonendefinition kann der Name T wie ein gewöhnlicher Typname verwendet werden. ❐ Schablonendefinitionen können von mehreren Typparametern (und auch von konstanten Wer ten) abhängen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.9 Typ- und Funktionsschablonen (Templates) 3 Systemprogrammierung in C und C++ (Teil 2) 3.9.2 Beispiel: Paare 150 3.9.2 Beispiel: Paare // Definition. template <typename X, typename Y> struct Pair { // Interne Repräsentation. X x; Y y; // Konstruktor. Pair (X x, Y y) : x(x), y(y) {} }; // Bequemere "Konstruktorfunktion" (weil bei Funktionsschablonen // "template argument deduction" erfolgt). template <typename X, typename Y> Pair<X, Y> mkpair (X x, Y y) { return Pair<X, Y>(x, y); } // Verwendung. mkpair(mkpair(1, ’x’), mkpair(2.0, true)) // Konstruiert ein Objekt des Typs // Pair< Pair<int, char>, Pair<double, bool> >. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.9 Typ- und Funktionsschablonen (Templates) 3 Systemprogrammierung in C und C++ (Teil 2) 3.9.2 Beispiel: Paare Konstruktorschablonen etc. template <typename X, typename Y> struct Pair { // Interne Repräsentation. X x; Y y; // Konstruktorschablone: Pair<X, Y>−Objekt aus Werten // der (zuweisungskompatiblen) Typen U und V konstruieren. template <typename U, typename V> Pair<X, Y> (U u, V v) : x(u), y(v) {} // Operatorschablone: Pair<U, V>−Objekt that // an Pair<X, Y>−Objekt *this zuweisen. template <typename U, typename V> Pair<X, Y>& operator= (Pair<U, V> that) { x = that.x; y = that.y; return *this; } } Pair<double, double> p(1, 2); p = mkpair(3, 4); 151 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.9 Typ- und Funktionsschablonen (Templates) 3 Systemprogrammierung in C und C++ (Teil 2) 3.9.3 Beispiel: Matrizen 152 3.9.3 Beispiel: Matrizen // MxN−Matrix mit Elementtyp T. template <typename T, int M, int N> struct Matrix { T elems [M] [N]; // Zweidimensionales Array von Elementen. }; // Produkt der LxM−Matrix A und der MxN−Matrix B. template <typename T, int L, int M, int N> Matrix<T, L, N> operator* (const Matrix<T, L, M>& A, const Matrix<T, M, N>& B) { Matrix<T, L, N> C; // LxN−Ergebnismatrix. for (int i = 0; i < L; i++) { for (int k = 0; k < N; k++) { C.elems[i][k] = 0; // Ergebniselement C[i][k] berechnen. for (int j = 0; j < M; j++) { C.elems[i][k] += A.elems[i][j] * B.elems[j][k]; } } } return C; } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 3.9 Typ- und Funktionsschablonen (Templates) 3 Systemprogrammierung in C und C++ (Teil 2) 3.9.4 Weitere Beispiele Matrix<double, 3, 2> A; A.elems[0][0] = ...; A.elems[0][1] = ...; ...... Matrix<double, 2, 4> B; B.elems[0][0] = ...; B.elems[0][1] = ...; ...... Matrix<double, 3, 4> C = A * B; 3.9.4 Weitere Beispiele // Absoluter Betrag von x. template <typename T> T abs (T x) { return x >= 0 ? x : −x; } // Maximum von x und y. template <typename T> T max (T x, T y) { return x >= y ? x : y; } // Array v der Größe n sortieren. template <typename T> void sort (T* v, int n) { ...... } 153 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.2 Ziel 4 Garbage Collection 4.1 Übersetzungsversuche ❐ Automatische (dynamische) Speicherverwaltung ❐ Automatische Speicherfreigabe ❐ Automatische Freispeichersammlung ❐ Automatische Speicherbereinigung ❐ „Müllabfuhr“ 4.2 Ziel ❐ Automatische Freigabe dynamisch erzeugter Objekte, die vom Anwendungsprogramm nicht mehr gebraucht werden 154 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.3 Motivation 4.3.1 Schwachstellen manueller Speicherfreigabe 155 4.3 Motivation 4.3.1 Schwachstellen manueller Speicherfreigabe ❐ Manuelle Speicherfreigabe birgt zwei Hauptgefahren: ❍ Speicher wird zu früh oder mehrmals freigegeben → ungültige Zeiger (dangling pointers) ❍ Speicher wird zu spät oder gar nicht freigegeben → Speicherlecks (memor y leaks) ❐ Manuelle Speicherfreigabe erhöht den Entwicklungsaufwand: ❍ Programme werden länger und komplizier ter. ❍ Die o. g. Fehler sind meist sehr schwer zu finden, weil ein Fehler an einer Stelle des Programms häufig zu einer Fehlfunktion an einer völlig anderen Stelle führt. ❐ Manuelle Speicherfreigabe kann ineffizient sein: ❍ Um die o. g. Fehler garantier t zu vermeiden, müssen dynamische Datenstrukturen oft unnötig kopier t werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.3 Motivation 4.3.2 Vorzüge automatischer Speicherfreigabe 156 ❐ Manuelle Speicherfreigabe birgt Sicherheitsrisiken: ❍ Über ungültige Zeiger kann man u. U. unbefugt Daten lesen oder verändern. ❍ Handelt es sich bei diesen Daten um Funktionszeiger, so kann man u. U. unbefugt fremde Funktionen aufrufen oder aber fremden Code dazu bringen, eigene Funktionen auszuführen. 4.3.2 Vorzüge automatischer Speicherfreigabe ❐ Erhöhte Sicherheit: ❍ Es gibt weder Speicherlecks noch ungültige Zeiger (außer durch Adressarithmetik oder fehlerhafte Anwendung des Adressoperators). ❍ Man kann weder absichtlich noch versehentlich auf „fremde“ Daten zugreifen (außer durch Überschreitung von Arraygrenzen). ❐ Bequemlichkeit: ❍ „Lassen Sie andere Leute (bzw. die Maschine) die Arbeit tun!“ ❐ Einfachheit: ❍ Programme sind z. T. erheblich einfacher, kürzer und lesbarer. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.3 Motivation 4.3.3 Weitere Aspekte 157 4.3.3 Weitere Aspekte ❐ Bestimmte Programmier paradigmen verlangen automatische Speicherverwaltung, da sie sowohl die Zuteilung als auch die Rückgabe von Speicher vollkommen verbergen: ❍ funktionale Programmierung ❍ logische Programmierung ❍ z. T. objektorientierte Programmierung ❐ Da globale (bzw. statische) und lokale Variablen üblicherweise automatisch verwaltet werden, ist es konsequent, auch dynamische Datenstrukturen automatisch zu verwalten. ❐ Das Erkennen nicht mehr benötigter Objekte ist normalerweise ein globales Problem, während Prinzipien wie Abstraktion und Modularität lokalen Charakter besitzen. ❐ Die Verwendung eines guten Garbage Collectors kann u. U. sogar effizienter als manuelle Speicherverwaltung sein, weil bei einer Speicherbereinigung die Fragmentierung des Heaps beseitigt und damit die Speicherzuteilung beschleunigt werden kann (vgl. § 7.5). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.4 Begriffsdefinitionen 158 4.4 Begriffsdefinitionen ❐ Die Daten bzw. Objekte eines Programms werden in drei Kategorien unterteilt: ❍ globale Objekte -- globale bzw. statische Variablen -- werden vom Laufzeitsystem beim Start des Programms erzeugt und am Ende des Programms vernichtet -- befinden sich im globalen Datensegment des Programms ❍ lokale Objekte -- lokale Variablen, Parameter und ggf. temporäre Objekte von Funktionen -- werden vom Laufzeitsystem beim Aufruf einer Funktion (bzw. am Anfang eines lokalen Blocks) erzeugt und am Ende der Funktion (bzw. des Blocks) vernichtet -- befinden sich auf dem (bzw. einem) Laufzeitstack ❍ dynamische Objekte -- während der Programmausführung zusätzlich erzeugte Objekte, deren Lebensdauer nicht durch lexikalische Gültigkeitsbereiche bestimmt wird -- werden vom Anwendungsprogramm (mutator ) explizit erzeugt und von der automatischen Speicherverwaltung (collector ) ggf. vernichtet -- befinden sich im Heap C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.4 Begriffsdefinitionen ❐ Objekte können zwei Arten von Daten enthalten: ❍ atomare Daten -- z. B. Zahlen, Zeichen etc. -- für die automatische Speicherverwaltung irrelevant ❍ Zeiger auf andere Objekte -- zumeist Zeiger auf dynamische Objekte -- für die automatische Speicherverwaltung u. U. relevant ❐ Zeiger werden in zwei Kategorien unterteilt: ❍ Wurzelzeiger -- globale und lokale Zeiger -- bilden zusammen die Wurzelmenge ❍ Folgezeiger -- Zeiger in dynamischen Objekten -- spannen zusammen einen gerichteten Objektgraphen auf 159 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.4 Begriffsdefinitionen ❐ Dynamische Objekte werden in zwei Kategorien unterteilt: ❍ lebende Objekte -- entweder direkt oder indirekt über eine Kette von Folgezeigern von einem Wurzelzeiger aus erreichbar -- sind für das Anwendungsprogramm prinzipiell zugreifbar ❍ tote Objekte (Müll ) -- von keinem Wurzelzeiger aus mehr erreichbar -- sind für das Anwendungsprogramm nicht mehr zugreifbar und können daher von der automatischen Speicherverwaltung vernichtet werden 160 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.4 Begriffsdefinitionen 161 Heap Stack Stack Globales Datensegment lebende Objekte Wurzelzeiger tote Objekte Folgezeiger atomare Daten C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.5 Grobklassifikation von Verfahren 4.5 Grobklassifikation von Verfahren Strategie ❐ reference counting (Kap. 5) ❐ tracing ❍ mark and sweep (Kap. 6) ❍ copying (Kap. 7) } mark and compact (Kap. 8) Implementierung ❐ Compiler und Laufzeitsystem ❐ Bibliothek oder Anwendungsprogramm 162 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection Weitere Unterscheidungsmerkmale ❐ unterbrechend ↔ nebenläufig ❐ vollständig ↔ par tiell ❐ exakt ↔ konservativ ❐ zentral ↔ verteilt 4.5 Grobklassifikation von Verfahren 163 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.6 Bewertungskriterien 164 4.6 Bewertungskriterien ❐ Zusatzaufwand bei normalen Operationen des Anwendungsprogramms (z. B. Zeigerzuweisungen) ❐ Durchschnittlicher Zeitbedarf für eine Kollektor-Phase ❐ Effizienz des Kollektors: Anzahl wiedergewonnener Bytes pro Zeiteinheit ❐ Fragmentierung des Heaps → Effizienz der Speicherzuteilung ❐ Zusätzlicher Platzbedarf in dynamischen Objekten für Verwaltungsinformation ❐ Verhalten bei fast vollem Heap ❐ Charakteristika des Anwendungsprogramms: Anzahl, Größe, Typ, Topologie und Lebensdauer von dynamischen Objekten ❐ Echtzeitfähigkeit ❐ Interaktion mit virtueller Speicherverwaltung und Caching ❐ usw. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 4 Garbage Collection 4.7 Literaturhinweise 165 4.7 Literaturhinweise ❐ R. Jones, R. Lins: Garbage Collection. Algorithms for Automatic Dynamic Memory Management . John Wiley & Sons, Chichester, 2000. ❐ D. E. Knuth: The Art of Computer Programming. Volume I: Fundamental Algorithms (Second Edition). Addison-Wesley, 1973. ❐ www.cs.kent.ac.uk/people/staff/rej/gc.html C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.1 Prinzip 5 Referenzzähler 5 Referenzzähler 5.1 Prinzip Erzeugung dynamischer Objekte ❐ Jedes dynamische Objekt besitzt einen Referenzzähler , der angibt, wie oft das Objekt referenzier t wird, d. h. wie viele Zeiger momentan darauf verweisen. ❐ Bei der Erzeugung eines Objekts wird sein Zähler mit 1 initialisiert. 1 166 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.1 Prinzip 5 Referenzzähler 167 Verwaltung dynamischer Objekte ❐ Bei Zeigeroperationen werden die Zähler der betroffenen Objekte wie folgt manipulier t: ❐ Wird ein Zeiger p kopier t, so wird der Zähler des Objekts *p um 1 erhöht. Dies geschieht insbesondere bei der Parameterübergabe von Zeigerwer ten an Funktionen und bei der Initialisierung lokaler Zeigervariablen. n n+1 ❐ Bevor ein Zeiger p ungültig wird, wird der Zähler des Objekts *p um 1 erniedrigt. Dies geschieht insbesondere am Ende von Funktionen, die Zeiger als Parameter oder lokale Variablen besitzen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.1 Prinzip 5 Referenzzähler 168 n−1 n ❐ Wird ein Zeiger p an eine Variable q zugewiesen, so wird der Zähler des Objekts *p um 1 erhöht und der Zähler des Objekts *q um 1 erniedrigt. (Bei einer „Selbstzuweisung“ von q an q bleibt der Zähler des Objekts *q somit unveränder t.) p q m n p m+1 q n−1 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.1 Prinzip 5 Referenzzähler 169 ❐ Ist p bzw. q ein Nullzeiger, so entfällt die entsprechende Manipulation des Zählers für *p bzw. *q. ❐ Um zufällige Zeigerwer te zu vermeiden, werden nicht explizit initialisierte Zeigervariablen implizit als Nullzeiger initialisiert. Zerstörung dynamischer Objekte ❐ Erreicht der Zähler eines Objekts den Wer t 0, so wird das Objekt freigegeben. 1 0 ❐ Enthält ein freigegebenes Objekt selbst Zeigerwer te, so werden diese ungültig, d. h. die Zähler der von ihnen referenzier ten Objekte werden um 1 erniedrigt. Ggf. werden die entsprechenden Objekte selbst freigegeben usw. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.1 Prinzip 5 Referenzzähler 0 1 1 170 1 0 0 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.2 Beispiel: Lineare Listen 5 Referenzzähler 5.2 Beispiel: Lineare Listen ❐ Würde man die Listenelemente aus § 3.9.1 mit Referenzzählern ausstatten (→ Übungsaufgabe), so könnte sich folgendes Szenario ergeben. 171 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.2 Beispiel: Lineare Listen 5 Referenzzähler Schritt 1 List<int> ls1 = cons(1, cons(2, cons(3))); List<int> ls2 = cons(4, tail(tail(ls1))); List<int> ls3 = cons(5, cons(6, tail(ls1))); 1 ls3 5 1 ls1 1 1 ls2 4 1 6 2 2 2 3 172 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.2 Beispiel: Lineare Listen 5 Referenzzähler Schritt 2 ls2 = ls1; 1 ls3 5 2 ls1 1 0 ls2 4 1 6 2 2 1 3 173 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.2 Beispiel: Lineare Listen 5 Referenzzähler Schritt 3 void f (List<int> ls) { while (ls) { cout << head(ls) << endl; ls = tail(ls); } } f(ls1); 1 ls3 ls1 ls2 1 5 6 2 (3) 2 (3) 1 (2) 1 2 3 ls 174 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.2 Beispiel: Lineare Listen 5 Referenzzähler Schritt 4 ls3 = 0; 0 ls3 5 2 ls1 ls2 1 0 6 1 2 1 3 175 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.1 Interne Repräsentation mit Referenzzählern 5.3 Beispiel: Dynamische Zeichenketten in C++ (vgl. § 3.7.3) 5.3.1 Interne Repräsentation mit Referenzzählern struct StringRep { const char* str; int count; // Zeichenkette. // Referenzzähler. // str mit dynamisch erzeugter Verkettung // von s1 und ggf. s2 initialisieren. void init (const char* s1, const char* s2) { int len = strlen(s1); if (s2) len += strlen(s2); char* s = new char [len + 1]; strcpy(s, s1); if (s2) strcat(s, s2); str = s; } 176 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.1 Interne Repräsentation mit Referenzzählern // Konstruktor. StringRep (const char* s1, const char* s2) { init(s1, s2); count = 0; } // Destruktor. ˜StringRep () { delete [] str; } }; 177 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.2 Eigentliche Typdefinition 5.3.2 Eigentliche Typdefinition struct String { // Interne Repräsentation. StringRep* rep; // Referenzzähler erhöhen bzw. erniedrigen. // Ggf. interne Repräsentation freigeben. void inc () { ++ rep−>count; } void dec () { if (−− rep−>count == 0) delete rep; } // Normaler Konstruktor. String (const char* s1 = "", const char* s2 = 0) { rep = new StringRep(s1, s2); inc(); } 178 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.2 Eigentliche Typdefinition // Kopierkonstruktor. String (const String& s) { rep = s.rep; inc(); } // Kopierender Zuweisungsoperator. String& operator= (const String& s) { if (this != &s) { dec(); rep = s.rep; inc(); } return *this; } // Destruktor. ˜String () { dec(); } }; 179 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.4 Anmerkungen 180 5.3.3 Operatoren // Verkettungsoperator. String operator+ (const String& s1, const String& s2) { return String(s1.rep−>str, s2.rep−>str); } // Vergleichsoperator. bool operator== (const String& s1, const String& s2) { return strcmp(s1.rep−>str, s2.rep−>str) == 0; } 5.3.4 Anmerkungen ❐ Dynamische Kopien von elementaren Zeichenketten werden nur vom Konstruktor StringRep erzeugt und vom zugehörigen Destruktor ˜StringRep wieder freigegeben. ❐ Referenzzähler werden vom Konstruktor StringRep mit Null initialisiert und anschließend nur von den Hilfsfunktionen inc und dec des Typs String manipulier t. ❐ StringRep-Objekte werden nur innerhalb des Typs String erzeugt und wieder vernichtet. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.4 Anmerkungen 181 StringObjekte StringRepObjekte 1 2 elementare Zeichenketten ❐ Beim kopierenden Zuweisungsoperator muss wieder auf eine korrekte Behandlung von Selbstzuweisungen geachtet werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.5 Anwendung 5.3.5 Anwendung // Zeichenkette s ausgeben. void print (const String& s) { cout << s.rep−>str << endl; } // Hauptprogramm. int main (int argc, char** argv) { String s1 = argv[1], s2 = argv[2], s; if (s1 == s2) s = " == "; else s = " != "; // Verkettung von s1, s und s2 an Funktion print übergeben. print(s1 + s + s2); } 182 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.6 Ablauf 5.3.6 Ablauf ❐ Konstruktion von s1 durch String (const char*) → Erzeugung eines neuen StringRep-Objekts r1 mit Referenzzähler 1 → dynamische Kopie r1.str von argv[1] ❐ Konstruktion von s2 durch String (const char*) → Erzeugung eines neuen StringRep-Objekts r2 mit Referenzzähler 1 → dynamische Kopie r2.str von argv[2] ❐ Konstruktion von s durch String () → Erzeugung eines neuen StringRep-Objekts r3 mit Referenzzähler 1 → dynamische Kopie r3.str von "" 183 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.6 Ablauf s1 r1 s2 1 "abc" r2 184 s 1 "def" r3 1 "" ❐ Parameterübergabe von s1 und s2 an operator== per Referenz ❐ Implizite Konvertierung von " == " oder " != " in ein temporäres Objekt t1 mit Typ String durch String (const char*) → Erzeugung eines neuen StringRep-Objekts r4 mit Referenzzähler 1 → dynamische Kopie r4.str von " == " oder " != " C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.6 Ablauf ❐ Zuweisung von t1 an s durch operator= → Erniedrigung des Referenzzählers r3.count auf 0 und anschließende Vernichtung von r3 → Freigabe von r3.str → Erhöhung des Referenzzählers r4.count auf 2 ❐ Destruktion des temporären Objekts t1 durch ˜String → Erniedrigung des Referenzzählers r4.count auf 1 s1 r1 s2 1 "abc" r2 s 1 "def" r3 t1 0 "" r4 1 " != " 185 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.6 Ablauf ❐ Parameterübergabe von s1 und s an operator+ per Referenz ❐ Konstruktion des Resultats t2 von s1 + s durch String (const char*, const char*) → Erzeugung eines neuen StringRep-Objekts r5 mit Referenzzähler 1 → dynamische Verkettung r5.str von r1.str und r3.str ❐ Parameterübergabe von t2 und s2 an operator+ per Referenz ❐ Konstruktion des Resultats t3 von t2 + s2 durch String (const char*, const char*) → Erzeugung eines neuen StringRep-Objekts r6 mit Referenzzähler 1 → dynamische Verkettung r6.str von r5.str und r2.str 186 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.6 Ablauf t2 r5 t3 1 r6 1 "abc != " "abc != def" ❐ Parameterübergabe von t3 an print per Referenz ❐ Ausführung von print ❐ Destruktion von t3 durch ˜String → Erniedrigung des Referenzzählers r6.count auf 0 und anschließende Vernichtung von r6 → Freigabe von r6.str 187 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.3 Beispiel: Dynamische Zeichenketten in C++ 5 Referenzzähler 5.3.6 Ablauf ❐ Destruktion von t2 durch ˜String → Erniedrigung des Referenzzählers r5.count auf 0 und anschließende Vernichtung von r5 → Freigabe von r5.str ❐ Destruktion von s durch ˜String → Erniedrigung des Referenzzählers r3.count auf 0 und anschließende Vernichtung von r3 → Freigabe von r3.str ❐ Destruktion von s2 durch ˜String → Erniedrigung des Referenzzählers r2.count auf 0 und anschließende Vernichtung von r2 → Freigabe von r2.str ❐ Destruktion von s1 durch ˜String → Erniedrigung des Referenzzählers r1.count auf 0 und anschließende Vernichtung von r1 → Freigabe von r1.str 188 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.4 Bewertung 5 Referenzzähler 189 5.4 Bewertung Pro ❐ Konzeptuell einfaches Verfahren. ❐ Funktionier t im Prinzip „nebenläufig“: ❍ Der Zusatzaufwand für die Speicherbereinigung ver teilt sich gleichmäßig auf die gesamte Programmausführung. ❍ Es gibt (fast; siehe unten) keine unkontrollier ten Unterbrechungen der normalen Programmausführung. ❍ Daher ist das Verfahren prinzipiell echtzeitfähig. ❐ Tote Objekte werden (meist; siehe unten) sofor t freigegeben. Eventuell erforderliche Aufräumarbeiten (Destruktoren) werden somit sofor t ausgeführ t. ❐ Funktionier t problemlos auch bei sehr vollem Heap. ❐ Funktionier t problemlos auch für parallele und ver teilte Systeme, sofern der Zugriff auf die Referenzzähler geeignet synchronisiert wird. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.4 Bewertung 5 Referenzzähler 190 Contra ❐ Der Zusatzaufwand für die Speicherbereinigung ist relativ hoch (ca. ein Dutzend zusätzliche Instruktionen pro Zeigerzuweisung). ❐ Bei Zeigeroperationen müssen auch die referenzier ten Objekte manipuliert werden, was u. U. zusätzliche Seitenfehler verursacht und somit die Performance zusätzlich beeinträchtigt. ❐ Der Zusatzaufwand muss ständig betrieben werden, obwohl Inkrement- und Dekrement-Operationen von Zählern häufig unmittelbar aufeinander folgen (z. B. beim Durchlaufen einer linearen Liste oder beim Aufruf kurzer Funktionen). ❐ Der für den Referenzzähler benötigte Platz ist je nach Objektgröße relativ groß (bei einem einfachen Listenknoten z. B. 50 %). ❐ Wenn tote Objekte rekursiv freigegeben werden, besteht doch die Gefahr unkontrollier ter Unterbrechungen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.4 Bewertung 5 Referenzzähler 191 ❐ Zirkuläre Strukturen werden niemals freigegeben, z. B.: ❍ Ringlisten ❍ doppelt verkettete Listen ❍ Bäume mit Rückwär ts-Zeigern zum Vaterknoten ❍ allgemeine Graphen → Nach wie vor Gefahr von Speicherlecks. Resümee ❐ Für bestimmte Anwendungen gut geeignet (z. B. Strings, zyklenfreie Datenstrukturen, Dateisysteme). ❐ In C++ sogar ohne Compiler-Unterstützung elegant und für das Anwendungsprogramm transparent realisierbar. ❐ Für beliebige Datenstrukturen nicht oder nur in Kombination mit einem anderen Verfahren geeignet. ❐ Bei einer solchen Kombination muss das andere Verfahren normalerweise wesentlich seltener angestoßen werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.5 Verbesserungsmöglichkeiten 5 Referenzzähler 5.5.1 Verzöger te Freigabe von Objekten 192 5.5 Verbesserungsmöglichkeiten 5.5.1 Verzögerte Freigabe von Objekten Problem ❐ Die Freigabe eines Objekts kann u. U. eine Lawine weiterer Freigaben auslösen, die zu einer unerwünschten und nicht kalkulierbaren Unterbrechung des Anwendungsprogramms führt. ❐ Beispiele: ❍ Der Wurzelknoten eines großen Binärbaums wird freigegeben. ❍ Das erste Element einer langen linearen Liste wird freigegeben. Lösung ❐ Ein Objekt, dessen Referenzzähler null wird, wird zwar in die Freispeicherliste eingefügt, aber noch nicht wirklich freigegeben. ❐ Erst wenn der Speicherplatz des Objekts neu zugeteilt wird, wird das Objekt wirklich freigegeben, d. h. erst jetzt werden die Referenzzähler seiner Nachfolgerobjekte erniedrigt und diese ggf. in die Freispeicherliste eingefügt. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.5 Verbesserungsmöglichkeiten 5 Referenzzähler 5.5.1 Verzöger te Freigabe von Objekten 193 Bewertung ❐ Konzeptuell einfaches Verfahren. ❐ Das Problem der unkontrollier ten Unterbrechungen wird (weitgehend) gelöst. ❐ Der Vor teil einer sofor tigen Objektfreigabe wird aufgegeben. ❐ Wenn ein Objekt sehr viele Zeiger enthält (wie z. B. ein Array von Zeigern), so ist seine Wiederverwendung immer noch mit relativ hohem und eventuell nicht kalkulierbarem Aufwand verbunden. ❐ Das Verfahren erforder t einen Eingriff in die vorhandene Speicherverwaltung (new/delete bzw. malloc/free) bzw. die Implementierung einer eigenen Speicherverwaltung. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.5 Verbesserungsmöglichkeiten 5 Referenzzähler 5.5.2 Kleine Referenzzähler 194 5.5.2 Kleine Referenzzähler Problem ❐ Der für einen Referenzzähler benötigte Platz ist relativ groß. ❐ Um Überlauf garantier t zu vermeiden, muss er prinzipiell so groß wie ein Zeiger sein. ❐ Bei einem einfachen Listenknoten, der selbst nur zwei Zeiger head und tail enthält, ist dies ein Overhead von 50 %. Lösung ❐ Verwendung kleinerer Zähler, z. B. nur ein Byte oder wenige Bits. ❐ Erreicht ein Zähler seinen Maximalwer t, wird er nicht mehr veränder t − er bleibt bei diesem Wer t „hängen“. ❐ Von Zeit zu Zeit werden mit Hilfe eines traversierenden Verfahrens die Wer te aller Referenzzähler neu ermittelt. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.5 Verbesserungsmöglichkeiten 5 Referenzzähler 5.5.2 Kleine Referenzzähler 195 Bewertung ❐ Für viele Anwendungen sind kleine Zähler ausreichend. ❐ Um zyklische Strukturen freigeben zu können, braucht man ohnehin ein zusätzliches traversierendes Verfahren. ❐ Die Sonderbehandlung des maximalen Zählerwer ts erhöht den Aufwand für Zeigeroperationen nochmals. ❐ Aufgrund von Ausrichtungszwängen kann der eingesparte Platz u. U. gar nicht direkt genutzt werden. Extremfall ❐ Referenzzähler sind nur 1 Bit groß, d. h. faktisch keine Zähler mehr, sondern nur noch Indikatoren (Flags), die anzeigen, ob es genau einen oder potentiell mehr als einen Zeiger auf ein Objekt gibt. ❐ Auf diese Weise können Objekte, die nur lokal verwendet wurden, schnell wieder freigegeben werden, während alle anderen Objekte anderweitig eingesammelt werden müssen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.5 Verbesserungsmöglichkeiten 5 Referenzzähler 5.5.3 Eingeschränkte Zähleraktualisierung 196 ❐ Die Indikatorbits müssen nicht mehr unbedingt in den Objekten, sie können eventuell auch in den Zeigern gespeicher t werden. Dadurch entfällt bei Zeigeroperationen der Zugriff auf die referenzier ten Objekte, der u. U. zu zusätzlichen Seitenfehlern führ t. 5.5.3 Eingeschränkte Zähleraktualisierung Problem ❐ Referenzzähler müssen ständig aktualisiert werden, obwohl sie sich auf lange Sicht oft gar nicht ändern. ❐ Beispiele: ❍ Traversierung von Datenstrukturen: Der Zähler des „aktuellen Objekts“ wird kurzzeitig erhöht und anschließend sofor t wieder erniedrigt. ❍ Aufruf von Funktionen: Werden Zeiger als Parameter übergeben, so werden die Zähler der referenzier ten Objekte kurzzeitig erhöht und nach Beendigung der Funktion wieder erniedrigt. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.5 Verbesserungsmöglichkeiten 5 Referenzzähler 5.5.3 Eingeschränkte Zähleraktualisierung 197 Lösung 1 ❐ Ein optimierender Compiler entfernt unnötige Paare von Inkrement- und DekrementAnweisungen. Lösung 2 ❐ Bei Zuweisungen an globale und lokale Zeigervariablen sowie Parameter werden grundsätzlich keine Zähler aktualisiert. ❐ Der Referenzzähler eines Objekts zählt also nur noch die Verweise von anderen dynamischen Objekten. ❐ Wird der Referenzzähler eines Objekts null, so wird das Objekt nicht freigegeben, sondern in eine temporäre Tabelle (zero count table, ZCT) eingetragen. ❐ Wird der Referenzzähler eines solchen Objekts wieder erhöht (weil es von einem anderen Objekt referenzier t wird), wird es aus der Tabelle entfernt. ❐ Von Zeit zu Zeit wird die Tabelle bereinigt: Alle Objekte in der Tabelle, die nicht direkt über Variablen oder Parameter (d. h. über Wurzelzeiger ) erreichbar sind, können freigegeben werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.5 Verbesserungsmöglichkeiten 5 Referenzzähler 5.5.4 Behandlung zyklischer Strukturen 198 Bewertung ❐ Wesentliche Verbesserung der Gesamtperformance, da ein Großteil aller Zeigerzuweisungen ohne Zusatzaufwand ausgeführt werden kann. ❐ Die Verwaltung der ZCT ist relativ umständlich und braucht zusätzlichen Platz. 5.5.4 Behandlung zyklischer Strukturen ❐ Mithilfe des Programmierers ❍ Wenn aufgrund der Programmlogik klar ist, dass eine zyklische Struktur freigegeben werden kann, wird der Zyklus zuerst aufgebrochen und anschließend der letzte Zeiger darauf entfernt. ❐ Spezialfall ❍ Jeder Zyklus besitzt genau einen „Eingang“, d. h. genau ein Objekt, über das er von „außen“ referenzier t wird. ❍ Wenn der letzte externe Zeiger auf dieses Objekt entfernt wird, kann der gesamte Zyklus freigegeben werden. ❍ Dieser Spezialfall tritt häufig bei der Implementierung funktionaler Programmiersprachen auf. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.5 Verbesserungsmöglichkeiten 5 Referenzzähler 5.5.4 Behandlung zyklischer Strukturen 199 ❐ Gruppenzähler (→ Übungsaufgabe) ❍ Jedes Objekt gehört zu einer Gruppe. ❍ Jede Gruppe besitzt einen Zähler, der die externen Referenzen auf die gesamte Gruppe zählt. ❍ Zyklen dürfen nur innerhalb einer Gruppe auftreten. ❍ Wenn der Zähler einer Gruppe null wird, kann die gesamte Gruppe freigegeben werden. ❐ Unterscheidung von starken und schwachen Zeigern: ❍ Lebendige Objekte müssen über starke Zeiger erreichbar sein. ❍ Der durch die starken Zeiger gebildete Graph ist azyklisch. ❍ Nur schwache Zeiger können Zyklen schließen. ❐ Kombination mit traversierenden Verfahren C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.6 Literaturhinweise 5 Referenzzähler 200 5.6 Literaturhinweise ❐ Ursprüngliches Verfahren für Lisp ❍ G. E. Collins: “A Method for Overlapping and Erasure of Lists.” Communications of the ACM 3 (12) December 1960, 655− −657. ❐ Verzöger te Freigabe von Objekten ❍ J. Weizenbaum: “Symmetric List Processor.” Communications of the ACM 6 (9) September 1963, 524− −544. ❐ Eingeschränkte Zähleraktualisierung ❍ L. P. Deutsch, D. G. Bobrow: “An Efficient Incremental Automatic Garbage Collector.” Communications of the ACM 19 (7) July 1976, 522− −526. ❐ Ein-Bit-Zähler ❍ D. P. Friedman, D. S. Wise: “The One-Bit Reference Count.” BIT 17 (3) 1977, 351− −359. ❍ W. R. Stoye, T. J. W. Clarke, A. C. Norman: “Some Practical Methods for Rapid Combinator Reduction.” In: G. L. Steele (ed.): Conf. Record of the 1984 ACM Symp. on Lisp and Functional Programming (Austin, Texas, August 1984). ACM Press, 159− −166. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.6 Literaturhinweise 5 Referenzzähler 201 ❐ Behandlung von Zyklen ❍ J. H. McBeth: “On the Reference Counting Method.” Communications of the ACM 6 (9) September 1963, 575. ❍ J. Weizenbaum: “Recovery of Reentrant List Structures in SLIP.” Communications of the ACM 12 (7) July 1969, 370− −372. ❍ D. P. Friedman, D. S. Wise: “Reference Counting Can Manage the Circular Environments of Mutual Recursion.” Information Processing Letters 8 (1) Januar y 1979, 41− −45. ❍ D. G. Bobrow: “Managing Reentrant Structures Using Reference Counts.” ACM Transactions on Programming Languages and Systems 2 (3) July 1980, 269− −273. ❍ D. R. Brownbridge: “Cyclic Reference Counting for Combinator Machines.” In: J. Jouannaud (ed.): Record of the 1985 Conf. on Functional Programming and Computer Architecture (Nancy, France, September 1985). Springer-Verlag, Lecture Notes in Computer Science 201, 1985. ❍ T. H. Axford: “Reference Counting of Cyclic Graphs for Functional Programs.” The Computer Journal 33 (5) 1990, 466− −470. ❍ T. W. Christopher: “Reference Count Garbage Collection.” Software—Practice and Experience 14 (6) June 1984, 503− −507. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 5.6 Literaturhinweise 5 Referenzzähler 202 ❐ Garbage-Collection-Hardware ❍ K. D. Nilsen, W. J. Schmidt: “A High-Performance Hardware-Assisted Real Time Garbage Collection System.” Journal of Programming Languages 2 (1) 1994. ❍ D. S. Wise: “Design for a Multiprocessing Heap with On-Board Reference Counting.” In: J. Jouannaud (ed.): Record of the 1985 Conf. on Functional Programming and Computer Architecture (Nancy, France, September 1985). Springer-Verlag, Lecture Notes in Computer Science 201, 1985, 289− −304. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.1 Prinzip 6 Mark&Sweep-Verfahren 203 6 Mark&Sweep-Verfahren 6.1 Prinzip Ablauf einer Speicherbereinigung ❐ Markierungsphase (mark ): Markierung lebendiger Objekte ❍ Ausgehend von der aktuellen Menge der Wurzelzeiger, werden alle dynamischen Objekte markiert , die direkt oder indirekt über eine Kette von Folgezeigern erreichbar sind. ❐ Reinigungsphase (sweep): Freigabe toter Objekte ❍ Bei einem Durchlauf durch den gesamten Heap werden alle nicht markier ten Objekte freigegeben und alle Markierungen wieder entfernt. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.1 Prinzip 6 Mark&Sweep-Verfahren Benötigte Information ❐ Wurzelmenge ❍ globale Zeigervariablen ❍ lokale Zeigervariablen ❍ temporäre Zeiger in Registern oder auf dem Stack ❐ Folgezeiger ❍ Zeigerkomponenten von dynamischen Objekten 204 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.2 Mögliche Implementierung in C++ (1) 6 Mark&Sweep-Verfahren 6.2.1 Markierungsphase 6.2 Mögliche Implementierung in C++ (1) 6.2.1 Markierungsphase Prinzip ❐ Um alle lebendigen Objekte zu markieren, wird über alle Wurzelzeiger iterier t und jeder so erreichbare Teilgraph des Objektgraphen rekursiv mittels Tiefensuche markiert. ❐ Sobald ein bereits markier tes Objekt erreicht wird, kann die Rekursion an dieser Stelle abgebrochen werden. ❐ Auf diese Weise werden auch zyklische Objektstrukturen korrekt behandelt. Unterstützung durch die Speicherverwaltung // Zeiger auf ein Objekt. typedef char* ptr; // Markierungsbit von Objekt p abfragen/setzen/zurücksetzen. bool marked (ptr p); void mark (ptr p); void unmark (ptr p); 205 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.2 Mögliche Implementierung in C++ (1) 6 Mark&Sweep-Verfahren 6.2.1 Markierungsphase // Iterator über alle Wurzelzeiger. struct RootIter { // Iterator initialisieren. RootIter (); // Gibt es weitere Elemente? operator bool () const; // Aktuelles Element liefern. ptr& operator* () const; // Iterator weitersetzen. RootIter& operator++ (); }; // Iterator über alle Folgezeiger des Objekts p ab Index i. struct SuccIter { SuccIter (ptr p, int i = 0); operator bool () const; ptr& operator* () const; SuccIter& operator++ (); }; 206 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.2 Mögliche Implementierung in C++ (1) 6 Mark&Sweep-Verfahren 6.2.1 Markierungsphase Algorithmus // Teilgraph beginnend bei Objekt p // rekursiv mittels Tiefensuche markieren. void markgraph (ptr p) { if (p && !marked(p)) { mark(p); for (SuccIter s(p); s; ++s) { markgraph(*s); } } } // Alle erreichbaren Objekte markieren. void markall () { for (RootIter r; r; ++r) { markgraph(*r); } } 207 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.2 Mögliche Implementierung in C++ (1) 6 Mark&Sweep-Verfahren 6.2.2 Reinigungsphase 6.2.2 Reinigungsphase Prinzip ❐ In der Reinigungsphase werden alle Heapobjekte durchlaufen, um einerseits die Markierungen der lebendigen Objekte zu entfernen und andererseits tote Objekte freizugeben. Unterstützung durch die Speicherverwaltung // Iterator über alle Heapobjekte. struct HeapIter { HeapIter (); operator bool () const; ptr operator* () const; HeapIter& operator++ (); // Aktuelles Objekt freigeben. void operator˜ (); }; 208 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.2 Mögliche Implementierung in C++ (1) 6 Mark&Sweep-Verfahren 6.2.2 Reinigungsphase Algorithmus // Heap säubern. void sweep () { for (HeapIter h; h; ++h) { ptr p = *h; if (marked(p)) unmark(p); else ˜h; } } 209 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.1 Motivation 210 6.3 Alternative Markierungsalgorithmen 6.3.1 Motivation Problem ❐ Rekursive Markierungsalgorithmen führen leicht zu einem unkontrollier ten Stacküberlauf und damit zu einem Programmabsturz. ❐ Da zum Zeitpunkt einer Speicherbereinigung typischerweise (fast) kein Speicher mehr verfügbar ist, kann der Stack nicht beliebig vergrößer t werden. ❐ Außerdem verbrauchen rekursive Funktionsaufrufe sowohl „unnötigen“ Platz auf dem Aufrufstack (z. B. für Rücksprungadressen etc.) als auch „unnötige“ Ausführungszeit (für das Sichern und Wiederherstellen von Registerinhalten am Anfang und Ende einer Funktion etc.). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.1 Motivation 211 Lösung 1 ❐ Verwendung äquivalenter iterativer Markierungsalgorithmen mit einem explizit verwalteten Stack von Objektzeigern, um einen Stacküberlauf erkennen und abfangen zu können und den „unnötigen“ Ressourcenverbrauch zu vermeiden. ❐ Bei einem Stacküberlauf werden weitere Push-Operationen ignorier t, der Algorithmus aber einfach weiter ausgeführt. ❐ Nach Beendigung des Algorithmus wird der Heap nach markier ten Objekten durchsucht, die noch nicht markier te Nachfolger besitzen, und der Markierungsalgorithmus für jedes dieser Nachfolgerobjekte neu gestartet. ❐ Dies wird ggf. so oft wiederholt, bis kein Stacküberlauf mehr auftritt. Lösung 2 ❐ Der „Rekursions-Rückweg“ wird nicht mit Hilfe eines Stacks, sondern durch temporäres Umdrehen von Zeigern (pointer reversal ) direkt in den traversier ten dynamischen Objekten gespeichert. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.2 Iterative Markierungsalgorithmen 6.3.2 Iterative Markierungsalgorithmen Explizit verwalteter Stack von Objektzeigern const int N = 1024; ptr stack [N]; int top = 0; void push (ptr p) { stack[top++] = p; } ptr pop () { if (top > 0) return stack[−−top]; else return 0; } 212 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.2 Iterative Markierungsalgorithmen 213 Prinzip ❐ Um alle lebendigen Objekte zu markieren, wird wieder über alle Wurzelzeiger iterier t und jeder so erreichbare Teilgraph iterativ (ähnlich wie bei Breitensuche) markier t. ❐ Hierfür werden in jedem Iterationsschritt alle noch nicht markier ten Nachfolger eines markierten Objekts markier t und (sofern sie selbst Nachfolger besitzen) auf den Stack gelegt. ❐ Anschließend wird das oberste Objekt vom Stack entfernt und für den nächsten Iterationsschritt verwendet. ❐ Die Iteration endet, wenn der Stack leer ist. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.2 Iterative Markierungsalgorithmen Algorithmus // Teilgraph beginnend bei Objekt p // iterativ (ähnlich wie bei Breitensuche) markieren. void markgraph (ptr p) { do { for (SuccIter s(p); s; ++s) { ptr q = *s; if (q && !marked(q)) { mark(q); if (SuccIter(q)) push(q); } } } while (p = pop()); } 214 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.2 Iterative Markierungsalgorithmen // Alle erreichbaren Objekte markieren. void markall () { for (RootIter r; r; ++r) { ptr p = *r; if (p && !marked(p)) { mark(p); markgraph(p); } } } 215 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.2 Iterative Markierungsalgorithmen 216 Anmerkungen ❐ Um den Stack möglichst platzsparend zu verwenden, werden nur Nachfolger q darauf gelegt, die momentan noch nicht markier t sind und die selbst wiederum Nachfolger besitzen. ❐ Außerdem werden die Nachfolger markier t, bevor sie auf den Stack gelegt werden, damit sie nicht erneut daraufgelegt werden, wenn man sie später noch auf einem anderen Weg erreicht. ❐ Die Reihenfolge, in der die Nachfolgerobjekte eines markier ten Objekts abgearbeitet werden, ist prinzipiell beliebig. ❐ Daher könnte anstelle eines Stacks mit Operationen push und pop (d. h. eines LIFOContainers) auch eine Queue mit Operationen put und get (d. h. ein FIFO-Container wie bei normaler Breitensuche) oder ein Bag mit Operationen add und remove (d. h. ein „nichtdeterministischer“ Container) verwendet werden. ❐ Allerdings kann der benötigte Speicherplatz je nach Abarbeitungsreihenfolge − abhängig von der jeweiligen Anwendung − sehr unterschiedlich sein (→ Übungsaufgabe). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.3 For tsetzung der Markierung nach . . . 217 6.3.3 Fortsetzung der Markierung nach Stacküberlauf Stack mit Überlaufkontrolle ❐ Wenn der Stack voll ist, werden weitere Pushoperationen ignorier t und ein Überlaufindikator gesetzt; ansonsten läuft der Markierungsalgorithmus aber normal weiter. ❐ Je nach Rechnerarchitektur und Betriebssystem, kann man einen Stacküberlauf u. U. auch ohne explizite Überprüfungen erkennen, indem man den Stack durch eine schreibgeschützte Speicherseite begrenzt. ❐ Ob das Abfangen des entsprechenden Seitenfehlers effizienter ist als explizite Überprüfungen, hängt wesentlich davon ab, wie häufig ein Stacküberlauf auftritt. bool overflow = false; void push (ptr p) { if (top < N) stack[top++] = p; else overflow = true; } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.3 For tsetzung der Markierung nach . . . 218 Prinzip ❐ Wenn während der Markierung ein Stacküberlauf auftrat, wird nach Beendigung des Markierungsalgorithmus nach markier ten Objekten gesucht, die noch nicht markier te Nachfolger besitzen (was „normalerweise“ nicht vorkommt), und der Markierungsalgorithmus für jedes dieser Nachfolgerobjekte neu gestartet. ❐ Dies wird ggf. so oft wiederholt, bis kein Stacküberlauf mehr auftritt. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.3 For tsetzung der Markierung nach . . . Algorithmus // Funktionen markgraph und markall wie zuvor. // Fortsetzung nach Stacküberlauf. void recover () { while (overflow) { overflow = false; for (HeapIter h; h; ++h) { ptr p = *h; if (marked(p)) { // Markiertes Objekt. for (SuccIter s(p); s; ++s) { ptr q = *s; if (q && !marked(q)) { // Unmarkierter Nachfolger. mark(q); markgraph(q); } } } } } } 219 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.3 Alternative Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.3.3 For tsetzung der Markierung nach . . . 220 Anmerkungen ❐ Die Effizienz des Markierungsalgorithmus hängt wesentlich davon ab, wie häufig ein Stacküberlauf auftritt. ❐ Tritt kein Stacküberlauf auf, so wird jedes lebendige Objekt genau einmal besucht, was optimal ist. ❐ Besitzt der Stack die Größe 0, was prinzipiell möglich ist, so kann markgraph lediglich die direkten Nachfolger von p markieren, und recover muss den Heap im ungünstigsten Fall k -mal durchlaufen, wenn k die maximale „Distanz“ eines Objekts von den Wurzelzeigern bezeichnet. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.4 Informationen für den Markierungsalgorithmus 6 Mark&Sweep-Verfahren 6.4.1 Typdeskriptoren 221 6.4 Informationen für den Markierungsalgorithmus 6.4.1 Typdeskriptoren Zweck ❐ Um die Folgezeiger eines beliebigen dynamischen Objekts durchlaufen zu können, muss der Markierungsalgorithmus ihre relativen Positionen im Objekt kennen. ❐ Da diese Positionen für jedes Objekt eines bestimmten Typs gleich sind, kann die entsprechende Information in einem statischen Typdeskriptor abgelegt werden, der von jedem dynamischen Objekt dieses Typs referenzier t wird. (Dynamische Arrays benötigen jedoch jeweils einen eigenen Deskriptor, der die Anzahl der Arrayelemente sowie einen Zeiger auf den Deskriptor des Elementtyps enthält.) ❐ Wenn der Typdeskriptor zusätzlich die Größe eines Objekts seines Typs enthält, kann die dynamische Speicherverwaltung anstelle des sonst benötigten Größenfelds (vgl. § 2.3.1) den Zeiger auf den Deskriptor verwalten. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.4 Informationen für den Markierungsalgorithmus 6 Mark&Sweep-Verfahren 6.4.1 Typdeskriptoren 222 Herkunft ❐ Wenn automatische Speicherbereinigung ein integraler Bestandteil der Programmiersprache ist (wie z. B. in Java und C#), werden die Typdeskriptoren vom Compiler zur Verfügung gestellt. ❐ Andernfalls muss man sie als Programmierer entweder explizit selbst erzeugen (z. B. in C) oder durch geeignete Sprachmittel automatisch erzeugen lassen (z. B. durch trickreiche Anwendung von Konstruktoren in C++, vgl. § 6.5). ❐ Alternativ kann man Typdeskriptoren auch von einem geeigneten Präcompiler erzeugen lassen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.4 Informationen für den Markierungsalgorithmus 6 Mark&Sweep-Verfahren 6.4.2 Prozedur- und Moduldeskriptoren 223 6.4.2 Prozedur- und Moduldeskriptoren Zweck ❐ Um die Menge aller Wurzelzeiger durchlaufen zu können, muss der Markierungsalgorithmus die Adressen aller globalen und lokalen Zeigervariablen kennen. ❐ Da die relativen Positionen von lokalen Variablen für jeden Aufruf einer Prozedur (Funktion, Methode, Konstruktor etc.) gleich sind, kann die entsprechende Information in einem statischen Prozedurdeskriptor abgelegt werden, der von jedem Ausführungskontext (activation record) dieser Prozedur referenzier t wird. ❐ Durch die übliche Verkettung von Ausführungskontexten auf dem Aufrufstack können somit die Deskriptoren aller momentan aktiven Prozeduren gefunden werden. ❐ Ein Prozedurdeskriptor besteht (ebenso wie ein Typdeskriptor) aus einer Folge von relativen Adressen (offsets), an denen sich Zeigervariablen der Prozedur (bzw. Zeigerkomponenten eines Objekts) befinden. ❐ Analog können die Positionen von globalen Zeigervariablen eines Moduls (oder einer Klasse o. ä.) durch entsprechende Moduldeskriptoren beschrieben werden, die beim Laden bzw. Initialisieren des Moduls in eine globale Liste eingehängt werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.4 Informationen für den Markierungsalgorithmus 6 Mark&Sweep-Verfahren 6.4.2 Prozedur- und Moduldeskriptoren 224 Herkunft ❐ Ebenso wie Typdeskriptoren, sollten Prozedur- und Moduldeskriptoren möglichst vom Compiler zur Verfügung gestellt werden. ❐ In C++ kann man die Wurzelmenge wiederum mit Hilfe von Konstruktoren und Destruktoren verwalten, sofern man konsequent „ver packte“ Zeigertypen verwendet (vgl. § 6.5). ❐ In anderen Sprachen ohne entsprechende Compiler-Unterstützung muss man die Wurzelmenge meist durch konser vative Verfahren approximieren (vgl. § 6.6). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.4 Informationen für den Markierungsalgorithmus 6 Mark&Sweep-Verfahren 6.4.3 Anonyme Zeiger 225 6.4.3 Anonyme Zeiger ❐ Neben explizit deklarier ten Variablen können während der Ausführung eines Programms u. U. auch temporäre Objekte existieren, die Zeiger enthalten. (In C++ „erfährt“ man deren Existenz durch einen Konstruktoraufruf.) ❐ Referenzen entsprechen faktisch ebenfalls anonymen Zeigern (über die man als Programmierer in C++ keinerlei Kontrolle hat). ❐ Insbesondere bei stark optimierenden Compilern, befinden sich die aktuellen Wer te lokaler Variablen u. U. gar nicht an ihrem Platz auf dem Stack, sondern in Registern. ❐ Resümee: Eine exakte Bestimmung der Wurzelmenge ohne Unterstützung des Compilers ist im allgemeinen nahezu unmöglich. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.1 Typdeskriptoren 6.5 Mögliche Implementierung in C++ (2) 6.5.1 Typdeskriptoren ❐ Ein Typdeskriptor eines Typs T enthält: ❍ die Größe eines Objekts des Typs in Byte; ❍ die Anzahl der Folgezeiger eines Objekts; ❍ die relativen Adressen aller Folgezeiger in einem Objekt; ❐ Bei seiner Initialisierung wird lediglich die Objektgröße in Byte angegeben. ❐ Die Anzahl der Folgezeiger und ihre relativen Adressen werden später bestimmt (siehe § 6.5.5). 226 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.2 Wurzelmenge 227 // Typdeskriptor. struct Desc { int size; // Objektgröße in Byte. int cnt; // Anzahl und relative int* off; // Adressen der Folgezeiger. // Initialisierung mit Objektgröße n. Desc (int n) : size(n), cnt(0), off(0) {} }; 6.5.2 Wurzelmenge ❐ Die Wurzelmenge enthält die Adressen aller Wurzelzeiger. ❐ Um Adressen effizient hinzufügen und entfernen zu können, sind geeignete Datenstrukturen notwendig, von denen hier abstrahier t wird. // Adresse von r zur Wurzelmenge hinzufügen. void add (ptr& r); // Adresse von r aus der Wurzelmenge entfernen (falls vorhanden). void remove (ptr& r); C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.3 Zeiger auf dynamische Objekte 228 6.5.3 Zeiger auf dynamische Objekte ❐ Dynamische Objekte eines Typs T, die automatisch freigegeben werden sollen, müssen über verpackte Zeiger des Typs gcptr<T> erreichbar sein. ❐ Sobald ein solches Objekt auf diese Weise nicht mehr erreichbar ist, darf es von der automatischen Speicherverwaltung freigegeben werden, selbst wenn es noch gewöhnliche Zeiger oder Referenzen darauf geben sollte (was man prinzipiell nicht hunder tprozentig verhindern kann). ❐ Da im Konstruktor eines ver packten Zeigers nicht unterschieden werden kann, ob es sich um einen Wurzel- oder Folgezeiger handelt, wird hier vorsichtshalber jeder Zeiger in die Wurzelmenge eingetragen. Folgezeiger werden später an anderer Stelle wieder entfernt (vgl. § 6.5.4). ❐ Entsprechend wird ein ver packter Zeiger in seinem Destruktor wieder aus der Wurzelmenge entfernt, sofern er sich noch darin befindet. ❐ Der kopierende Zuweisungsoperator ist trivial (d. h. er verrichtet keine zusätzlichen Aufgaben) und kann daher auch weggelassen werden (was u. U. zu effizienterem Code führt). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.3 Zeiger auf dynamische Objekte // Verpackter Zeiger auf dynamische Objekte des Typs T. template <typename T> struct gcptr { ptr p; // Echter Zeiger. // Initialisierung mit Objektadresse q bzw. als Nullzeiger. gcptr (ptr q = 0) : p(q) { add(p); } // Kopierkonstruktor. gcptr (const gcptr& q) : p(q.p) { add(p); } // Kopierender Zuweisungsoperator. gcptr& operator= (const gcptr& q) { p = q.p; return *this; } // Destruktor. ˜gcptr () { remove(p); } // Zugriffsoperator. T* operator−> () const { return (T*)(p); } }; 229 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.4 Objekterzeugung 230 6.5.4 Objekterzeugung ❐ Die Template-Funktion gcnew erzeugt ein neues dynamisches Objekt des Typs T, initialisier t es als Kopie von x und liefer t einen ver packten Zeiger darauf zurück. ❐ Beim ersten Aufruf für einen bestimmten Typ T wird der Typdeskriptor desc mit der Objektgröße n initialisier t. ❐ Der Speicherplatz für das Objekt wird mit Hilfe einer Funktion gcalloc beschafft, die analog zu malloc funktionier t (vgl. § 2.3.2), aber anstelle der Objektgröße einen Typdeskriptor-Zeiger als Argument erhält (über den die Objektgröße ermittelt werden kann) und diesen anstelle des Größenfelds speichert. ❐ Bei vollem Heap stößt gcalloc eine Speicherbereinigung an (d. h. führt markall(); recover(); sweep() aus) und/oder vergrößer t den Heap. ❐ Falls trotzdem nicht genügend Speicherplatz verfügbar ist, liefer t gcnew einen (verpackten) Nullzeiger. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.4 Objekterzeugung 231 ❐ Anschließend wird der Speicherbereich p durch Aufruf des Kopierkonstruktors T(x) initialisier t. (Wenn der Operator new eine Adresse p als Argument erhält, beschafft er keinen neuen Speicherplatz, sondern führ t lediglich einen Konstruktoraufruf an dieser Adresse durch.) ❐ Durch diesen Konstruktoraufruf werden für alle gcptr-Komponenten von T ebenfalls Kopierkonstruktoren aufgerufen, die die Adressen dieser ver packten Zeiger in die Wurzelmenge eintragen (siehe § 6.5.3). ❐ Da es sich bei diesen Zeigern jedoch um Folgezeiger handelt, werden sie anschließend wieder aus der Wurzelmenge entfernt (vgl. § 6.5.5). // Dynamisches Objekt als Kopie von x erzeugen // und verpackten Zeiger darauf liefern. template <typename T> gcptr<T> gcnew (const T& x) { // Typdeskriptor für T. const int n = sizeof(T); static Desc desc(n); C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.4 Objekterzeugung // Speicherplatz beschaffen und mit Null vorinitialisieren. ptr p = gcalloc(&desc); if (!p) return 0; memset(p, 0, n); // Verpackten Zeiger auf das Objekt setzen // und Objekt initialisieren. gcptr<T> gcp(p); new (p) T (x); // Folgezeiger des Objekts, die durch die Initialisierung // des Objekts in die Wurzelmenge geraten sind, entfernen. // Dabei ggf. Typdeskriptor vervollständigen. removesucc(p); // Verpackten Zeiger zurückliefern. return gcp; } 232 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.5 Ver vollständigung des Typdeskriptors 233 6.5.5 Vervollständigung des Typdeskriptors ❐ Zur Entfernung der Folgezeiger des Objekts p aus der Wurzelmenge wird eine Funktion removesucc verwendet, die alle Zeiger aus der Wurzelmenge entfernt, deren Adressen sich innerhalb des Objekts p befinden. ❐ Bei dieser Gelegenheit kann removesucc auch die Anzahl dieser Folgezeiger und ihre relativen Adressen ermitteln und diese Information im Typdeskriptor von p ablegen, sofern sie dort noch nicht vorliegt. ❐ Andernfalls kann die Information des Typdeskriptors über Anzahl und Adressen der Folgezeiger dazu verwendet werden, die Suche nach den zu entfernenden Adressen zu beschleunigen. // Adressen der Folgezeiger des Objekts p // aus der Wurzelmenge entfernen. // Ggf. Anzahl und relative Adressen dieser // Zeiger im Typdeskriptor von p ablegen. void removesucc (ptr p); C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.6 Schutz des Objekts während seiner . . . 234 6.5.6 Schutz des Objekts während seiner Initialisierung ❐ Da die Initialisierung des Objekts ein aufwendiger Vorgang sein kann, in dessen Verlauf weitere dynamische Objekte erzeugt werden können, kann währenddessen auch eine (oder sogar mehrere) Speicherbereinigung(en) stattfinden. ❐ Damit das neue Objekt hierbei nicht freigegeben wird, wird bereits vor der Initialisierung ein verpackter Zeiger darauf gesetzt, über den es für den Markierungsalgorithmus erreichbar ist. ❐ Wenn der Typdeskriptor des Objekts noch unvollständig ist, besitzt das Objekt aus Sicht des Markierungsalgorithmus keine Nachfolger. Da die Folgezeiger des Objekts zu diesem Zeitpunkt aber noch als Wurzelzeiger betrachtet werden (siehe § 6.5.3), werden eventuell bereits vorhandene Nachfolgerobjekte auf diesem Wege gefunden. ❐ Damit das Objekt während seiner Initialisierung keine zufälligen Folgezeiger besitzt, die den Markierungsalgorithmus durcheinander bringen würden, wird sein Speicherplatz vorher mit Null initialisiert. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.7 Implementierung der Iteratoren 235 6.5.7 Implementierung der Iteratoren Wurzelzeiger ❐ Ein Iterator über alle Wurzelzeiger iterier t in geeigneter Weise über die Wurzelmenge, wobei die konkrete Implementierung natürlich stark von der zugrunde liegenden Datenstruktur abhängt. Folgezeiger ❐ Ein Iterator über alle Folgezeiger eines Objekts speichert intern: ❍ die Adresse p des Objekts; ❍ den Zeiger d auf den Typdeskriptor des Objekts (damit er aus Effizienzgründen nur einmal bestimmt werden muss); ❍ den aktuellen Iterationsindex i. ❐ Mit diesen Informationen lässt sich die Iteratorschnittstelle aus § 6.2.1 leicht implementieren: C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.7 Implementierung der Iteratoren 236 // Typdeskriptor−Zeiger des Objekts p liefern. Desc* desc (ptr p); // Iterator über alle Folgezeiger des Objekts p ab Index i. struct SuccIter { ptr p; // Zeiger auf Objekt. Desc* d; // Zeiger auf Typdeskriptor des Objekts. int i; // Aktueller Iterationsindex. // Iterator initialisieren. SuccIter (ptr p, int i = 0) : p(p), d(desc(p)), i(i) {} // Gibt es weitere Elemente? operator bool () const { return i < d−>cnt; } // Aktuelles Element liefern. ptr& operator* () const { return *(ptr*)(p + d−>off[i]); } // Iterator weitersetzen. SuccIter& operator++ () { i++; return *this; } }; C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.5 Mögliche Implementierung in C++ (2) 6 Mark&Sweep-Verfahren 6.5.7 Implementierung der Iteratoren 237 Heapobjekte ❐ Ein Iterator über alle Heapobjekte speichert intern die Adresse des aktuellen Objekts p und liefer t diese als Resultat des Zugriffsoperators *. ❐ Um den Iterator weiterzusetzen, wird zur Adresse p die Größe dieses Objekts addiert, die man entweder über den Typdeskriptor des Objekts erhält oder − wenn es sich um einen freien Speicherbereich handelt − über ein Größenfeld. ❐ Damit dies auch dann korrekt funktioniert, wenn der Heap aus mehreren Blöcken besteht (vgl. 2.3.2), muss das letzte Objekt eines Blocks ein Dummy-Objekt sein, dessen Größe formal der Adressdifferenz zum ersten Objekt des nächsten Blocks entspricht. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.6 Konser vative Speicherbereinigung 6 Mark&Sweep-Verfahren 6.6.1 Idee 238 6.6 Konservative Speicherbereinigung 6.6.1 Idee ❐ Wenn die Wurzelmenge nicht exakt bestimmt werden kann und/oder Typdeskriptoren nicht zur Verfügung stehen, kann man beide Informationen konser vativ (d. h. im Zweifelsfall übervorsichtig) approximieren. ❐ Zur Approximation der Wurzelmenge wird das globale Datensegment sowie der bzw. die aktuellen Aufrufstacks (und evtl. Registerinhalte) Wor t für Wor t nach allen Wer ten durchsucht, die einen Zeiger auf ein dynamisches Objekt darstellen könnten. ❐ Ebenso wird jedes erreichbare dynamische Objekt Wor t für Wor t nach potentiellen Folgezeigern durchsucht. ❐ Auf diese Weise findet man auf jeden Fall alle lebendigen Objekte, markiert aber u. U. auch fälschlicherweise einige tote Objekte. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.6 Konser vative Speicherbereinigung 6 Mark&Sweep-Verfahren 6.6.1 Idee 239 Notwendige Kriterien für Zeiger auf dynamische Objekte ❐ Die Adresse muss in den Heap verweisen (was durch geeignete Verwaltungsinformation der Speicherverwaltung festgestellt werden kann). ❐ Der Heapblock, in den die Adresse verweist, muss der Speicherverwaltung bekannt sein. ❐ Die relative Adresse in diesem Heapblock muss ein Vielfaches der Größe der in diesem Block gespeicher ten Objekte sein (d. h. in jedem Heapblock werden nur Objekte einer bestimmten Größe gespeichert). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.6 Konser vative Speicherbereinigung 6 Mark&Sweep-Verfahren 6.6.2 Probleme 240 6.6.2 Probleme ❐ Objekte, die nur über innere Zeiger erreichbar sind, z. B.: Rational* p = new Rational [10]; int i = 10; while (i−−) process(*p++); ❐ Durch Adressarithmetik o. ä. entstellte oder verborgene Zeiger, z. B. (vgl. § 1.3.3): void traverse (Node* n) { if (!n) return; ulong x = (ulong)(n−>left); Node* left = (Node*)(x & ˜1); traverse(left); traverse(n−>right); } ❐ Datenbereiche mit uninitialisiertem oder veraltetem Inhalt (häufig auch Stackframes) ❐ aggressiv optimierende Compiler ❐ usw. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.6 Konser vative Speicherbereinigung 6 Mark&Sweep-Verfahren 6.6.3 Lösungsmöglichkeiten 241 6.6.3 Lösungsmöglichkeiten ❐ Für den Programmierer : ❍ Verzicht auf innere Zeiger (oder auf Objekte, die nur über innere Zeiger erreichbar sind). ❍ Kennzeichnung von atomaren dynamischen Objekten, die per definitionem keine Zeiger enthalten. ❐ Für den Kollektor: ❍ Verwaltung von Ausschlusslisten (black lists). ❍ Initialer Aufruf des Kollektors vor der ersten Speicherzuteilung, um falsche Referenzen frühzeitig zu erkennen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.6 Konser vative Speicherbereinigung 6 Mark&Sweep-Verfahren 6.6.4 Mögliche Anwendungsbereiche 242 6.6.4 Mögliche Anwendungsbereiche ❐ Automatische Speicherbereinigung in vollkommen „unkooperativen“ Umgebungen, z. B. in C und C++. ❐ Vereinfachte Speicherbereinigung in „kooperativen“ Umgebungen, z. B. in Java. ❐ Behandlung des „native method stacks“ in Java. ❐ Speicherbereinigung in Sprachen, die von ihrem Compiler nach C übersetzt werden. Hier kann der (Prä-)Compiler zumindest Typdeskriptoren für dynamische Objekte erzeugen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.7 Weitere Aspekte 6 Mark&Sweep-Verfahren 6.7.1 Markierung in separaten Bitvektoren 243 6.7 Weitere Aspekte 6.7.1 Markierung in separaten Bitvektoren Vorteile ❐ Heap-Speicherseiten werden durch den Markierungsalgorithmus nicht veränder t und müssen daher bei Verdrängung nicht zurückgeschrieben werden. ❐ Ebenso müssen lebendige Objekte in der Reinigungsphase weder gelesen noch veränder t werden, was sich ebenfalls positiv auf die virtuelle Speicherverwaltung auswirken kann. ❐ Die Markierungsbits können gruppenweise gelesen, überprüft und zurückgesetzt werden. ❐ Dynamische Objekte werden durch Markierungsbits nicht unnötig aufgebläht. ❐ Bei einem konservativen Kollektor besteht keine Gefahr, dass durch das Markieren versehentlich Anwendungsdaten überschrieben werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.7 Weitere Aspekte 6 Mark&Sweep-Verfahren 6.7.2 Verzöger te Reinigungsphase 244 Nachteile ❐ Zusätzlicher Verwaltungsaufwand. ❐ Der Zugriff auf ein Bit eines Bitvektors ist u. U. wesentlich ineffizienter als der direkte Zugriff auf ein Bit im Speicher (insbesondere wenn der Bitvektor als Hashtabelle implementier t wird, um Platz zu sparen). 6.7.2 Verzögerte Reinigungsphase Prinzip ❐ Die Reinigungsphase wird nicht auf einmal unmittelbar nach der Markierungsphase durchgeführ t, sondern stückweise bei jeder nachfolgenden Speicherzuteilung. ❐ Bei jeder Speicherzuteilung werden zum Beispiel so lange dynamische Objekte abgearbeitet, bis man ein passendes totes Objekt gefunden hat, das neu zugeteilt werden kann. Vorteil ❐ Das Anwendungsprogramm muss nur für die Dauer der Markierungsphase unterbrochen werden und kann somit früher for tgesetzt werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.8 Bewertung 6 Mark&Sweep-Verfahren 245 6.8 Bewertung Pro ❐ Keine Probleme mit zyklischen Strukturen. ❐ Kein Zusatzaufwand bei normalen Anweisungen des Anwendungsprogramms. ❐ Der durchschnittliche Zusatzaufwand pro Zeiteinheit für die Speicherbereinigung ist geringer als bei der Verwendung von Referenzzählern. ❐ Geringer Platzbedarf für Verwaltungsinformation (1 Markierungsbit pro Objekt, das z. B. noch in das Größenfeld oder den Zeiger auf den Typdeskriptor der unterliegenden Speicherverwaltung passt; bestimmte Markierungsalgorithmen benötigen aber u. U. mehr Platz). ❐ Funktionier t als konser vatives Verfahren selbst für vollkommen „unkooperative“ Sprachen wie C, wo es weder Unterstützung durch Compiler und Laufzeitsystem noch Mechanismen wie Konstruktoren o. ä. zur Implementierung auf Anwendungsebene gibt. ❐ Der Gesamtplatzbedarf ist halb so groß wie bei einem kopierenden Verfahren (vgl. Kap. 7). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.8 Bewertung 6 Mark&Sweep-Verfahren 246 Contra ❐ Es handelt sich um ein unterbrechendes Verfahren, von dem zumindest die Markierungsphase vollständig ablaufen muss, bevor die Anwendung for tfahren kann. (Für inkrementelle Markierungsalgorithmen [vgl. § 6.9] trifft dies nicht zu; bei ihnen entsteht jedoch Zusatzaufwand bei allen Zeigeroperationen des Anwendungsprogramms.) ❐ Markierungsalgorithmen verlangen Kompromisse zwischen Performance und Speicherplatz. ❐ Tote Objekte werden verzöger t freigegeben → Aufräumarbeiten werden nicht sofor t ausgeführ t. ❐ Die Effizienz des Kollektors (Anzahl wiedergewonnener Bytes pro Zeiteinheit) sinkt drastisch, wenn der Heap sehr voll ist. ❐ In der Reinigungsphase muss der gesamte Heap − d. h. insbesondere auch die „uninteressanten“ toten Objekte − durchlaufen werden. ❐ Die Kosten für Speicherzuteilung sind − im Vergleich zu kopierenden Verfahren (vgl. § 7) − relativ hoch, da der Heap fragmentier t wird. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.8 Bewertung 6 Mark&Sweep-Verfahren Resümee ❐ Trotz gewisser Nachteile eines der am häufigsten verwendeten Verfahren. 247 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.1 Motivation 248 6.9 Inkrementelle Markierungsalgorithmen 6.9.1 Motivation ❐ Die vollständige Ausführung der Markierungsphase kann zu einer unkontrollierbaren Unterbrechung des Anwendungsprogramms führen, was insbesondere für interaktive Anwendungen und Echtzeitsysteme problematisch ist. ❐ Für derar tige Anwendungen akzeptier t man lieber einen gleichmäßigen und kalkulierbaren Zusatzaufwand während der gesamten Programmausführung als einzelne längere Unterbrechungen. ❐ Referenzzähler-basier te Verfahren erfüllen diese Anforderungen zwar, sind aber im allgemeinen nicht in der Lage, zyklische Strukturen zurück zu gewinnen. ❐ Daher muss man den Markierungsalgorithmus so modifizieren, dass er seine Arbeit inkrementell verrichtet, d. h. quasi parallel zum eigentlichen Anwendungsprogramm arbeitet. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.2 Problem 249 6.9.2 Problem ❐ Wenn das Anwendungsprogramm während der Markierungsphase weiter läuft, kann es die Struktur des Objektgraphen u. U. so verändern, dass der Markierungsalgorithmus lebendige Objekte „übersieht“. Beispiel ❐ Gegeben seien zwei direkt erreichbare Objekte A und B sowie ein Objekt C, das zunächst nur indirekt über B erreichbar ist. ❐ Zu diesem Zeitpunkt wird A vom Markierungsalgorithmus markier t. ❐ Anschließend kopier t das Anwendungsprogramm den Zeiger auf C von B nach A und entfernt ihn bei B, d. h. jetzt ist C nur noch indirekt über A erreichbar. ❐ Zu diesem Zeitpunkt wird B vom Markierungsalgorithmus markier t. ❐ Da der Markierungsalgorithmus A bereits abgearbeitet hat, übersieht er das erreichbare Objekt C. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.2 Problem A B C A B C Lösungsansatz ❐ Das Anwendungsprogramm muss mit dem Markierungsalgorithmus kooperieren, indem es ihm Hinweise auf Änderungen in der Struktur des Objektgraphen gibt. 250 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.3 Prinzip 251 6.9.3 Prinzip ❐ Einführung von drei Objektzuständen, die anschaulich durch Farben repräsentiert werden: ❍ weiß: Objekt ist unmarkier t. ❍ grau: Objekt ist markier t, muss vom Markierungsalgorithmus aber noch einmal besucht werden, weil seine Nachfolger möglicherweise noch nicht markier t sind. ❍ schwarz : Objekt ist markier t und muss nicht mehr besucht werden, weil seine Nachfolger ebenfalls markier t (d. h. entweder grau oder schwarz) sind. ❐ Aus diesen Definitionen folgt die Invariante, dass es zu keinem Zeitpunkt einen Schwarz-weiß-Zeiger , d. h. einen Zeiger von einem schwarzen auf ein weißes Objekt, gibt bzw. geben darf. ❐ Wenn das Anwendungsprogramm durch eine Zeigeroperation einen Schwarz-weißZeiger erzeugen würde, muss es gleichzeitig das schwarze Ausgangsobjekt oder das weiße Zielobjekt des Zeigers grau verfärben, um die Invariante nicht zu verletzen. ❐ Anmerkung: Beim iterativen Markierungsalgorithmus in § 6.3.2 sind die grauen Objekte genau die Objekte auf dem Markierungsstack. Falls der Stack übergelaufen ist, sucht die Funktion recover aus § 6.3.3 genau nach grauen Objekten. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.4 Fragen 252 6.9.4 Fragen ❐ Wie behandelt man dynamische Änderungen der Wurzelmenge? ❐ Wie läuft die Markierungsphase inkrementell ab? ❐ Wie erkennt man das Ende der Markierungsphase? ❐ Terminier t die Markierungsphase unter allen Umständen? ❐ Soll das Anwendungsprogramm das Ausgangs- oder das Zielobjekt eines Schwarzweiß-Zeigers grau verfärben? ❐ Welche Farbe erhalten neu erzeugte Objekte? ❐ Wann werden tote Objekte spätestens freigegeben? C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.5 Mögliche Antwor ten 253 6.9.5 Mögliche Antworten Behandlung von Wurzelzeigern ❐ Zu Beginn der Markierungsphase werden alle Objekte, die direkt über einen Wurzelzeiger erreichbar sind, schattier t , d. h. grau gefärbt, sofern sie noch weiß sind. (Graue und schwarze Objekte bleiben beim Schattieren unveränder t.) ❐ Dieser Vorgang kann inkrementell durchgeführt werden, d. h. durch Operationen des Anwendungsprogramms unterbrochen werden. ❐ Wenn sich der Wer t eines Wurzelzeigers während der Markierungsphase änder t oder ein neuer Wurzelzeiger erzeugt wird, wird das neue Zielobjekt durch das Anwendungsprogramm ebenfalls schattiert. ❐ Auf diese Weise ist sichergestellt, dass alle direkt erreichbaren Objekte entweder grau oder schwarz sind. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.5 Mögliche Antwor ten 254 Markierungsphase ❐ In jedem Schritt der Markierungsphase werden alle Nachfolger eines grauen Objekts schattier t und das graue Objekt selbst schwarz gefärbt. ❐ Jeder derar tige Schritt wird normalerweise atomar, d. h. ohne Unterbrechung durch das Anwendungsprogramm ausgeführt. Bei sorgfältiger Analyse der möglichen Wechselwirkungen, kann er aber u. U. auch inkrementell ausgeführt werden. ❐ Anmerkung: Ein solcher Schritt entspricht genau einer Iteration der äußeren dowhile-Schleife der Funktion markgraph aus § 6.3.2: ❍ Die Nachfolger des grauen Objekts p werden (ggf.) markier t und (ggf.) auf den Markierungsstack gelegt, was genau dem Schattieren dieser Objekte entspricht. ❍ Das Objekt p änder t dadurch implizit seine Farbe von grau nach schwarz. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.5 Mögliche Antwor ten 255 Ende der Markierungsphase ❐ Behauptung: ❍ Wenn es kein graues Objekt mehr gibt, sind alle erreichbaren Objekte schwarz, d. h. die Markierungsphase kann beendet werden. ❍ Insbesondere ist der so erreichte Zustand stabil , d. h. es können keine grauen Objekte mehr hinzu kommen. ❐ Begründung: ❍ Wenn es in diesem Zustand ein erreichbares weißes Objekt gäbe, müsste es einen Pfad (d. h. eine Kette von Folgezeigern) von einem direkt erreichbaren Objekt zu diesem Objekt geben. ❍ Aufgrund der Invariante, dass es keine Schwarz-weiß-Zeiger gibt, und der Tatsache, dass keine grauen Objekte mehr existieren, müssen alle Objekte auf diesem Pfad weiß sein. ❍ Dies ist ein Widerspruch zu der früher erwähnten Tatsache, dass alle direkt erreichbaren Objekte grau oder schwarz sind. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.5 Mögliche Antwor ten 256 Vermeidung von Schwarz-weiß-Zeigern ❐ Wenn das Anwendungsprogramm beim Erzeugen eines Schwarz-weiß-Zeigers das weiße Zielobjekt grau verfärbt, wird die Farbe eines Objekts im Laufe der Markierungsphase niemals heller (Monotonieprinzip). ❐ Da jedes graue Objekt im Laufe der Markierungsphase schwarz wird und anschließend schwarz bleibt, sind früher oder später alle grauen Objekte verschwunden, d. h. die Markierungsphase terminiert unter allen Umständen. ❐ Wenn das Anwendungsprogramm beim Erzeugen eines Schwarz-weiß-Zeigers jedoch das schwarze Ausgangsobjekt grau verfärben würde, könnte ein schwarzes Objekt später wieder grau werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.5 Mögliche Antwor ten 257 ❐ Tatsächlich könnte die Farbe eines Objekts dann unter ungünstigen Umständen beliebig oft zwischen schwarz und grau hin und her wechseln: 1. Gegeben seien wieder zwei direkt erreichbare Objekte A und B sowie ein zunächst nur indirekt über B erreichbares Objekt C. (Eventuell besitzen A und B weitere Folgezeiger.) 2. Zu Beginn der Markierungsphase werden die beiden direkt erreichbaren Objekte A und B schattiert, d. h. grau gefärbt. 3. Im ersten Schritt der eigentlichen Markierungsphase wird das graue Objekt A schwarz gefärbt. 4. Anschließend kopier t das Anwendungsprogramm den Zeiger auf das weiße Objekt C von B nach A und entfernt den Zeiger bei B. Da A schwarz und C weiß ist, wird A hierbei wieder grau gefärbt. 5. Im zweiten Schritt der Markierungsphase wird jetzt das graue Objekt B schwarz gefärbt. 6. Anschließend kopier t das Anwendungsprogramm den Zeiger auf das weiße Objekt C wieder von A nach B und entfernt den Zeiger bei A. Da B jetzt schwarz und C nach wie vor weiß ist, wird B hierbei wieder grau gefärbt. 7. Weiter bei Schritt 3. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.5 Mögliche Antwor ten 258 Erzeugung neuer Objekte ❐ Da neue Objekte u. U. schnell wieder „sterben“ können, sollten sie nach Möglichkeit weiß gefärbt werden, damit sie möglichst in der nächsten Reinigungsphase freigegeben werden. ❐ Außerhalb der Markierungsphase stellt dies kein Problem dar, da zu Beginn der Markierungsphase alle Objekte weiß sein sollen. ❐ Während der Markierungsphase würde die Erzeugung eines neuen weißen Objekts jedoch der obigen Behauptung widersprechen, weil das Anwendungsprogramm auf diese Weise jederzeit ein erreichbares weißes Objekt erzeugen könnte. ❐ Wenn man neue Objekte während der Markierungsphase grau färben würde, könnte jederzeit ein neues graues Objekt entstehen, was ebenfalls der Behauptung widerspricht. (Dies wäre außerdem ineffizient, weil ein graues Objekt ohnehin früher oder später schwarz gefärbt werden muss.) ❐ Daher müssen neue Objekte während der Markierungsphase schwarz gefärbt werden. ❐ Da ein Objekt zum Zeitpunkt seiner Erzeugung noch keine (gültigen) Zeiger auf andere Objekte besitzt, kann die Invariante „Keine Schwarz-weiß-Zeiger“ dadurch nicht verletzt werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.5 Mögliche Antwor ten 259 Freigabe toter Objekte ❐ Wenn ein bereits markier tes Objekt während der Markierungsphase unerreichbar wird, wird es in der anschließenden Reinigungsphase nicht freigegeben, sondern irr tümlich als lebendig betrachtet. ❐ Da ein einmal unerreichbares Objekt anschließend jedoch nie wieder erreichbar wird, ist sichergestellt, dass ein solches Objekt in der nächsten Markierungsphase nicht wieder markier t wird und daher spätestens in der nächsten Reinigungsphase freigegeben wird. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.6 Implementierungsaspekte 260 6.9.6 Implementierungsaspekte Kodierung der Farben ❐ Zur Unterscheidung der Farben schwarz und weiß verwendet man wie bisher ein Markierungsbit pro Objekt. ❐ Zur Kennzeichnung der Zwischenfarbe grau gibt es verschiedene Möglichkeiten: ❍ Man verwendet ein zweites Bit pro Objekt. ❍ Da graue Objekte vom Markierungsalgorithmus noch einmal besucht werden müssen, legt man sie sofor t auf den Markierungsstack und definiert ein Objekt als grau, wenn es markier t ist und auf dem Markierungsstack liegt (vgl. § 6.3.2). ❍ Da ein Objekt per definitionem genau dann grau ist, wenn es selbst markier t ist, aber noch unmarkier te Nachfolger besitzt, kann man prinzipiell auf eine explizite Kennzeichnung grauer Objekte verzichten. Bei einem Durchlauf durch den Heap identifiziert man graue Objekte einfach anhand dieser charakteristischen Eigenschaft (vgl. auch § 6.3.3). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.6 Implementierungsaspekte 261 Vermeidung von Schwarz-weiß-Zeigern ❐ Damit das Anwendungsprogramm bei der Erzeugung eines Schwarz-weiß-Zeigers das Zielobjekt grau verfärben kann, müssen − wie bei Referenzzähler-basier ten Verfahren − alle Zeigeroperationen überprüft werden. ❐ In C++ kann man hierfür wieder ver packte Zeigertypen mit überladenen Operatoren verwenden. ❐ Da die Farbe des Ausgangsobjekts einer Zeigeroperation nicht direkt bestimmbar ist, wird das Zielobjekt grundsätzlich schattiert, d. h. markier t und auf den Markierungsstack gelegt, sofern es noch nicht markier t ist. Verflechtung von Anwendung und Speicherbereinigung ❐ Entweder laufen Anwendungsprogramm und Speicherbereinigung echt parallel ❐ oder das Anwendungsprogramm führt bei jeder Speicheranforderung einige Iterationen der Markierungs- oder Reinigungsphase der Speicherbereinigung aus. ❐ Im ersten Fall müssen die kritischen Abschnitte der Markierungsphase durch geeignete Synchronisationsmechanismen geschützt werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.9 Inkrementelle Markierungsalgorithmen 6 Mark&Sweep-Verfahren 6.9.7 Bewertung 262 6.9.7 Bewertung ❐ Das Problem der unkontrollier ten Unterbrechungen wird (weitgehend) gelöst, wodurch das Verfahren prinzipiell echtzeitfähig wird. (Wenn jeder Schritt der Markierungsphase atomar ablaufen muss, besteht bei Objekten mit sehr vielen Folgezeigern allerdings immer noch die Gefahr unkontrollier ter Unterbrechungen.) ❐ Dies wird durch einen relativ hohen Zusatzaufwand bei allen Zeigeroperationen des Anwendungsprogramms erkauft. ❐ Außerdem werden tote Objekte z. T. erst in der nächsten Reinigungsphase freigegeben. ❐ Bei der Konzeption und Implementierung eines konkreten Verfahrens muss sehr sorgfältig auf Korrektheitskriterien geachtet werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 6.10 Literaturhinweise 6 Mark&Sweep-Verfahren 263 6.10 Literaturhinweise ❐ Verschiedene Markierungsalgorithmen ❍ D. E. Knuth: The Art of Computer Programming. Volume I: Fundamental Algorithms (Second Edition). Addison-Wesley, 1973. ❐ Konservative Speicherbereinigung ❍ H. Boehm, M. Weiser : “Garbage Collection in an Uncooperative Environment.” Software—Practice and Experience 18 (9) September 1988, 807− −820. ❍ http://www.hboehm.info/gc/ ❐ Inkrementelle Markierungsalgorithmen ❍ E. W. Dijkstra, L. Lamport, A. J. Mar tin, C. S. Scholten, E. F. M. Steffens: “On-theFly Garbage Collection: An Exercise in Cooperation.” Communications of the ACM 21 (11) November 1978, 966− −975. ❍ G. L. Steele: “Multiprocessing Compactifying Garbage Collection.” Communications of the ACM 18 (9) September 1975, 495− −508. (Corrigendum in 19 (6) June 1976, 354) C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.1 Prinzip 7 Kopierende Verfahren 264 7 Kopierende Verfahren 7.1 Prinzip ❐ Der Heap wird in zwei Hälften unter teilt, von denen zu jedem Zeitpunkt nur eine benutzt wird. ❐ Wenn die momentan benutzte Hälfte voll ist, werden alle lebendigen Objekte an den Anfang der anderen Hälfte kopier t und alle Zeiger auf die Objekte entsprechend angepasst . ❐ Dann werden die Rollen der beiden Hälften vertauscht , d. h. anschließend wird die bisher unbenutzte Hälfte benutzt und die bisher benutzte Hälfte „weggeworfen“. ❐ Der Kopiervorgang beginnt − ähnlich wie bei einem Mark&Sweep-Verfahren − bei den direkt über Wurzelzeiger erreichbaren Objekten und wird von dort − rekursiv oder iterativ − zu allen indirekt über Folgezeiger erreichbaren Objekten for tgesetzt. ❐ Nachdem ein Objekt kopier t wurde − aber bevor seine Nachfolger kopier t werden − , wird seine neue Adresse im alten Objekt hinterlegt, damit alle Zeiger auf das Objekt korrekt angepasst werden können. ❐ Für die Zuteilung neuer dynamischer Objekte braucht man keinerlei Freispeicherlisten o. ä.; es werden einfach die nächsten freien Bytes der aktuellen Heaphälfte verwendet (vgl. § 2.1.3). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.2 Prinzipielle Implementierung in C++ 7 Kopierende Verfahren 7.2.1 Speicherverwaltung 265 7.2 Prinzipielle Implementierung in C++ 7.2.1 Speicherverwaltung Vorbereitungen (vgl. § 2.3.1) // Adresse eines beliebigen Objekts. typedef char* ptr; // Typ mit maximaler Ausrichtung. union Maxalign { long l; // bool, char, wchar_t, short, int, long. long double d; // float, double, long double. char* p; // Datenzeiger. void (*f) (); // Funktionszeiger. }; // Hilfsstruktur zur Bestimmung der Ausrichtung. struct Dummy { Maxalign x; char y; }; C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.2 Prinzipielle Implementierung in C++ 7 Kopierende Verfahren 7.2.1 Speicherverwaltung 266 // x auf ein Vielfaches der Zweierpotenz a aufrunden. int align (int x, int a) { return (x + (a−1)) & ˜(a−1); } // Konstanten. const int A = sizeof(Dummy) − sizeof(Maxalign); // Max. Ausrichtung. const int P = sizeof(ptr); // Größe eines Zeigers. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.2 Prinzipielle Implementierung in C++ 7 Kopierende Verfahren 7.2.1 Speicherverwaltung Zweigeteilter Heap // Gesamtgröße des Heaps. // H/2 muss ein Vielfaches von A sein. const int H_ = 1000000; const int H = align(H_, 2*A); // Verschnitt am Anfang jeder Heaphälfte wegen Ausrichtung. const int W = align(P, A) − P; // Heap mit maximal ausgerichteter Anfangsadresse. ptr heap = (ptr)newmem(H); // Anfang und Ende der beiden Heaphälften. ptr bottom [] = { heap + W, heap + H/2 + W }; ptr top [] = { heap + H/2, heap + H }; // Index der momentan benutzten Heaphälfte. int act = 0; // Zeiger auf das erste freie Byte. ptr free = bottom[act]; 267 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.2 Prinzipielle Implementierung in C++ 7 Kopierende Verfahren 7.2.1 Speicherverwaltung Typdeskriptoren (vgl. § 6.5.1) ❐ Analog zum Größenfeld in § 2.3.1, befindet sich am Anfang jedes dynamischen Objekts − für die Anwendung unsichtbar − ein Zeiger auf den zugehörigen Typdeskriptor, der für die Implementierung von SuccIter benötigt wird (vgl. § 6.5.7). // Typdeskriptor. struct Desc { int size; // Objektgröße in Byte. int cnt; // Anzahl und relative int* off; // Adressen der Folgezeiger. }; // Typdeskriptor−Zeiger von p abfragen bzw. auf d setzen. Desc*& desc (ptr p) { return ((Desc**)(p))[−1]; } void desc (ptr p, Desc* d) { desc(p) = d; } // Größe des Objekts p abfragen. int size (ptr p) { return desc(p)−>size; } 268 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.2 Prinzipielle Implementierung in C++ 7 Kopierende Verfahren 7.2.1 Speicherverwaltung Speicheranforderung // Dynamisches Objekt mit Typdeskriptor−Zeiger d zuteilen. ptr gcalloc (Desc* d) { // Größe anpassen. int s = align(d−>size + P, A); // Wenn nötig, Speicher bereinigen. if (free + s > top[act]) copy(); // Wenn möglich, Speicher zuteilen. if (free + s <= top[act]) { ptr p = free + P; free += s; desc(p, d); return p; } else { return 0; } } 269 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.2 Prinzipielle Implementierung in C++ 7 Kopierende Verfahren 7.2.2 Rekursives Kopieren lebendiger Objekte 7.2.2 Rekursives Kopieren lebendiger Objekte ❐ Nachdem ein Objekt kopier t wurde, wird seine neue Adresse anstelle des Typdeskriptor-Zeigers gespeicher t. ❐ Um diesen Fall erkennen zu können, wird zusätzlich das niedrigste Bit des entsprechenden Zeigerwer ts gesetzt. // Typdefinition zur Abkürzung. typedef unsigned long ulong; // Neue Adresse des Objekts p abfragen bzw. auf f setzen. ptr forward (ptr p) { ulong u = (ulong)desc(p); if (u & 1) return (ptr)(u & ˜1); else return 0; } void forward (ptr p, ptr f) { desc(p, (Desc*)((ulong)(f) | 1)); } 270 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.2 Prinzipielle Implementierung in C++ 7 Kopierende Verfahren 7.2.2 Rekursives Kopieren lebendiger Objekte 271 // Objekt p ggf. rekursiv kopieren und Zeiger p aktualisieren. void copyobj (ptr& p) { // Hierfür wird p per Referenz übergeben. if (!p) { // Nichts zu tun. return; } else if (ptr f = forward(p)) { // Objekt wurde bereits kopiert. // Lediglich Zeiger p aktualisieren. p = f; } else { // Objekt selbst kopieren, neue Adresse hinterlegen // und Zeiger p aktualisieren. int s = align(size(p) + P, A); memcpy(free, p − P, s); forward(p, free + P); p = free + P; free += s; // Nachfolger im neuen Objekt rekursiv kopieren. for (SuccIter s(p); s; ++s) copyobj(*s); // *s hat Typ ptr& } } C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.2 Prinzipielle Implementierung in C++ 7 Kopierende Verfahren 7.2.3 Iteratives Kopieren lebendiger Objekte // Alle lebendigen Objekte kopieren. void copy () { // Heaphälften vertauschen. act = 1 − act; free = bottom[act]; // Direkt erreichbare Objekte rekursiv kopieren. for (RootIter r; r; ++r) copyobj(*r); // *r hat Typ ptr& } 7.2.3 Iteratives Kopieren lebendiger Objekte ❐ Die Funktion copyobj kopier t nur das Objekt p selbst. ❐ In der Funktion copy werden zunächst alle direkt erreichbaren Objekte kopier t. ❐ Anschließend wird die neue Heaphälfte Objekt für Objekt durchlaufen, um die Nachfolger aller bereits kopier ten Objekte zu kopieren. ❐ Da die hierbei kopier ten Objekte am aktuellen Ende der neuen Heaphälfte (Zeiger free) angefügt werden, werden ihre Nachfolger später ebenfalls kopier t usw. (vgl. Breitensuche). 272 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.2 Prinzipielle Implementierung in C++ 7 Kopierende Verfahren 7.2.3 Iteratives Kopieren lebendiger Objekte // Objekt p ggf. kopieren und Zeiger p aktualisieren. void copyobj (ptr& p) { if (!p) { // Nichts zu tun. return; } else if (ptr f = forward(p)) { // Objekt wurde bereits kopiert. // Lediglich Zeiger p aktualisieren. p = f; } else { // Objekt kopieren, neue Adresse hinterlegen // und Zeiger p aktualisieren. int s = align(size(p) + P, A); memcpy(free, p − P, s); forward(p, free + P); p = free + P; free += s; } } 273 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.2 Prinzipielle Implementierung in C++ 7 Kopierende Verfahren 7.2.3 Iteratives Kopieren lebendiger Objekte // Alle lebendigen Objekte kopieren. void copy () { // Heaphälften vertauschen. act = 1 − act; free = bottom[act]; ptr p = free + P; // Direkt erreichbare Objekte kopieren. for (RootIter r; r; ++r) copyobj(*r); // Indirekt erreichbare Objekte kopieren. for (; p < free; p += align(size(p) + P, A)) { // Nachfolger von Objekt p kopieren. for (SuccIter s(p); s; ++s) copyobj(*s); } } 274 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.3 Effizienzbetrachtungen 7 Kopierende Verfahren 7.3.1 Vergleich mit Mark&Sweep-Verfahren 7.3 Effizienzbetrachtungen 7.3.1 Vergleich mit Mark&Sweep-Verfahren ❐ Gegeben: ❍ H = Größe des Heaps ❍ L = Platzbedarf aller lebendigen Objekte ❐ Zeit- bzw. CPU-Aufwand für eine Kollektion: ❍ Copy: t = a L ❍ Mark&Sweep: t = b L + c H mit geeigneten Konstanten a, b, c ❐ Wiedergewonnener Speicherplatz: ❍ Copy: m = H / 2 − L ❍ Mark&Sweep: m = H − L 275 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.3 Effizienzbetrachtungen 7 Kopierende Verfahren 7.3.1 Vergleich mit Mark&Sweep-Verfahren 276 ❐ Effizienz des Verfahrens, d. h. Anzahl wiedergewonnener Bytes pro Zeit- bzw. CPUEinheit: 1 1 H /2−L = − aL 2ar a 1−r H −L = ❍ Mark&Sweep: e = br +c bL +cH ❍ Copy: e = mit r = L / H = Anteil der lebendigen Objekte ❐ Effizienz e in Abhängigkeit von der Heapbelegung r : e Copy Mark&Sweep 1/c 0 r 0.5 1 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.3 Effizienzbetrachtungen 7 Kopierende Verfahren 7.3.1 Vergleich mit Mark&Sweep-Verfahren 277 Beobachtung ❐ Die Effizienz beider Verfahren geht gegen Null, wenn der Heap (bzw. eine Heaphälfte) sehr voll wird. ❐ Die Effizienz eines Mark&Sweep-Verfahrens ist grundsätzlich beschränkt, während die Effizienz eines kopierenden Verfahrens prinzipiell beliebig erhöht werden kann, indem man die Heapbelegung r verkleiner t, d. h. die Heapgröße H vergrößer t. ❐ Aber: Wenn man die Reinigungsphase verzöger t und mit der Neuzuteilung von Objekten kombinier t, so lässt sich auch die Effizienz eines Mark&Sweep-Verfahrens beliebig erhöhen. Bei beiden Verfahren muss für die Gesamtlaufzeit eines Programms der Aufwand für die Zuteilung aller jemals erzeugten Objekte mitberücksichtigt werden! C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.3 Effizienzbetrachtungen 7 Kopierende Verfahren 7.3.2 Effizienz bei sehr großem Heap 278 7.3.2 Effizienz bei sehr großem Heap ❐ Der Kehrwer t t1 von e gibt an, wieviel Zeit bzw. Instruktionen das Verfahren im Durchschnitt zur Wiedergewinnung eines Bytes benötigt: t1 = a aL = H / 2L − 1 H /2−L ❐ Entsprechend gibt sa ts = s t1 = H / 2L − 1 den durchschnittlichen Aufwand zur Wiedergewinnung eines Objekts der Größe s an. ❐ Macht man die Heapgröße H ausreichend groß, so wird ts kleiner als der Aufwand zur expliziten Freigabe eines Objekts (selbst wenn diese nur eine einzige Maschineninstruktion erfordern würde), d. h. automatische Speicherbereinigung ist dann effizienter als manuelle Speicherfreigabe! ❐ Insbesondere ist dynamische Speicherverwaltung dann genauso effizient wie oder sogar effizienter als Stackverwaltung! ❐ Aber: Die vorangegangenen Überlegungen ignorieren die Kosten der virtuellen Speicherverwaltung! In der Praxis kann ein kleiner Heap, der vollständig im Hauptspeicher gehalten werden kann, wesentlich effizienter sein als ein großer, dessen Speicherseiten immer wieder ein- und ausgelagert werden müssen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.4 Weitere Aspekte 7 Kopierende Verfahren 7.4.1 Interaktion mit der virtuellen Speicher. . . 279 7.4 Weitere Aspekte 7.4.1 Interaktion mit der virtuellen Speicherverwaltung ❐ Wenn sehr große Objekte auf eigene Speicherseiten gelegt werden, können sie einfach und effizient durch memor y remapping von einer Heaphälfte in die andere „kopier t“ werden. ❐ Ist dies nicht möglich, sollten sehr große Objekte in separaten Heapbereichen gespeicher t werden, die nicht kopier t, sondern z. B. mittels Mark&Sweep bereinigt werden. ❐ Dasselbe gilt eventuell auch für Objekte, von denen man weiß, dass sie sehr lange leben werden und daher häufig kopier t werden müssten (vgl. auch § 7.6). ❐ Die Speicherseiten der „alten“ Heaphälfte müssen − obwohl sie durch das Speichern der neuen Objektadressen veränder t wurden − bei Verdrängung nicht auf Platte geschrieben werden, da sie nur „Müll“ enthalten. ❐ Ebenso müssen bis jetzt unbenutzte Speicherseiten der aktuellen Heaphälfte bei Einlagerung nicht von Platte gelesen werden, da sie ebenfalls nur „Müll“ enthalten, der gleich anschließend durch neue Anwendungsdaten überschrieben wird. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.4 Weitere Aspekte 7 Kopierende Verfahren 7.4.2 Gruppierung von Objekten 280 7.4.2 Gruppierung von Objekten ❐ Unmittelbar nacheinander erzeugte Objekte − die häufig über Folgezeiger miteinander in Beziehung stehen und später auch häufig zusammen gebraucht werden − liegen anfangs benachbart im Heap. ❐ Dadurch ist die Wahrscheinlichkeit relativ groß, dass derar tige Objekte auf derselben Speicherseite liegen und daher bei Bedarf zusammen eingelagert werden. ❐ Nach einem Kopiervorgang ist die ursprüngliche Anordnung der Objekte im Heap aber u. U. vollkommen veränder t, was sich negativ auf die Gesamtperformance auswirken kann. ❐ Erfahrungsgemäß bleiben Objektbeziehungen bei einem (rekursiven) Depth-first Kopiervorgang (vgl. Tiefensuche) besser erhalten als bei einer Breadth-first -Strategie (vgl. Breitensuche), wie sie beim iterativem Kopieren verwendet wird. ❐ Da beim rekursiven Kopieren aber die Gefahr eines Stacküberlaufs besteht, kann man versuchen, den iterativen Algorithmus so zu modifizieren, dass die Objekte zumindest annähernd depth-first kopier t werden. ❐ Alternativ besteht die Möglichkeit, ein geeignetes Mark&Compact-Verfahren zu verwenden (vgl. Kap. 8). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.5 Bewertung 7 Kopierende Verfahren 281 7.5 Bewertung Pro ❐ Bei einer Speicherbereinigung müssen nur die lebendigen Objekte bearbeitet werden, nicht der gesamte Heap. ❐ Die Speicherzuteilung ist maximal einfach und effizient. ❐ Abgesehen von dem Zeiger auf den Typdeskriptor − den man in objektorientier ten Sprachen auch für andere Zwecke benötigt (z. B. für dynamisch gebundene Methoden und dynamische Typtests) − , benötigen dynamische Objekte keinerlei zusätzliche Verwaltungsinformation. ❐ Die „Kondensierung“ des Speichers kann sich positiv auf die virtuelle Speicherverwaltung auswirken, weil die lebendigen Objekte dicht zusammen bleiben. Contra ❐ Das Kopieren von Objekten ist − zumindest für größere Objekte − aufwendiger als Markieren. ❐ Lang lebende Objekte müssen häufig kopier t werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.5 Bewertung 7 Kopierende Verfahren 282 ❐ Das Verfahren braucht prinzipiell doppelt so viel Platz im Heap wie ein Mark&SweepVerfahren. (Bei virtueller Speicherverwaltung kann die momentan unbenutzte Heaphälfte jedoch ausgelager t werden, so dass der gesamte Platz nur während der Kopier phase gebraucht wird.) ❐ Die bei der Kondensierung des Speichers erfolgende zufällige Umordnung von Objekten kann sich negativ auf die virtuelle Speicherverwaltung auswirken, weil logisch zusammengehörende (und ursprünglich benachbarte) Objekte nicht mehr zusammen liegen. ❐ Funktionier t nicht als konser vatives Verfahren, da man zum Aktualisieren von Zeigern diese exakt kennen muss. ❐ Anwendungsprogramme dürfen keine „nackten“ Objektadressen verwenden, da sich diese im Laufe der Zeit ändern können. (Insbesondere darf man in C++ keine Elementfunktionen benutzen, da diese immer die „nackte“ Objektadresse this verwenden.) C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.5 Bewertung 7 Kopierende Verfahren 283 Resümee ❐ Grundsätzlich eines der am weitesten verbreiteten Verfahren. ❐ Da in vielen Anwendungen der Anteil der überlebenden Objekte relativ klein ist, ist das Verfahren dort sehr beliebt. ❐ In „disziplinierten“ Programmiersprachen (wie z. B. Java oder C#) stellt das „Verbiegen“ von Zeigern kein Problem dar. ❐ Für „undisziplinierte“ Programmiersprachen (wie z. B. C und C++) ist das Verfahren jedoch nicht geeignet (d. h. § 7.2 zeigt wirklich nur das Implementierungsprinzip). C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.6 Generationsverfahren 7 Kopierende Verfahren 7.6.1 Idee 284 7.6 Generationsverfahren 7.6.1 Idee ❐ Die meisten Objekte „sterben jung“. ❐ Je älter ein Objekt wird (d. h. je mehr Speicherbereinigungen es bereits überlebt hat), desto geringer wird die Wahrscheinlichkeit, dass es stirbt (d. h. desto höher wird die Wahrscheinlichkeit, dass es auch die nächste Speicherbereinigung überlebt). ❐ Daher ist es vor teilhaft, die Menge der dynamischen Objekte in zwei oder mehr Generationen zu unterteilen, die idealerweise unabhängig voneinander bereinigt werden können (die „junge“ Generation häufig, die „alte“ seltener). ❐ Inter-Generations-Zeiger , insbesondere Zeiger von „alten“ auf „junge“ Objekte sind zwar selten, müssen aber gesondert behandelt werden. ❐ Bei der Bereinigung der jungen Generation können Wurzel- und Folgezeiger, die in die alte Generation verweisen, ignorier t werden; Inter-Generations-Zeiger, die von der alten in die junge Generation verweisen, müssen aber wie Wurzelzeiger behandelt werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.6 Generationsverfahren 7 Kopierende Verfahren 7.6.2 Erkennung und Behandlung von . . . 285 7.6.2 Erkennung und Behandlung von Alt-jung-Zeigern ❐ Inter-Generations-Zeiger von der alten in die junge Generation können in zwei unterschiedlichen Situationen entstehen: ❍ Kopieren von jungen Objekten (mit Folgezeigern auf andere junge Objekte) in die alte Generation: Da dies von der Speicherverwaltung selbst ausgeführt wird, können die dabei entstehenden Alt-jung-Zeiger direkt erkannt und notiert werden. ❍ Zuweisung (von Zeigern auf junge Objekte) an Folgezeiger alter Objekte: Da dies von der Anwendung ausgeführt wird, muss sie die Speicherverwaltung darüber informieren. ❐ Card Marking: ❍ Unter teilung des Heaps in „Karten“ (z. B. 512 Byte groß) ❍ Card Map: Bit- oder Byte-Vektor mit einem Eintrag pro Karte ❍ Bei einer Zuweisung an einen Folgezeiger markier t die Anwendung die Karte, in der sich der Zeiger befindet. ❍ Beim Bereinigen der jungen Generation werden alle markier ten Kar ten nach Zeigern in die junge Generation durchsucht, die dann wie Wurzelzeiger behandelt werden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.6 Generationsverfahren 7 Kopierende Verfahren 7.6.2 Erkennung und Behandlung von . . . 286 ❐ Wichtig: ❍ Zuweisungen an globale und lokale Variablen (die erfahrungsgemäß einen Großteil der Zeigerzuweisungen ausmachen) sind unkritisch und erfordern daher keinen Zusatzaufwand. ❍ Die erstmalige Initialisierung von Folgezeigern eines Objekts ist ebenfalls unkritisch, weil sich das Objekt zu diesem Zeitpunkt in der jungen Generation befindet und somit noch keine Alt-jung-Zeiger entstehen können. ❍ Daher kann das Erzeugen eines neuen Objekts „billiger“ sein als das Ändern eines alten! C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.6 Generationsverfahren 7 Kopierende Verfahren 7.6.3 Implementierung in Oracle’s HotSpot JVM 287 7.6.3 Implementierung in Oracle’s HotSpot Java Virtual Machine ❐ Unter teilung des Heaps in eine (kleinere) junge und eine (größere) alte Generation ❐ Unter teilung der jungen Generation in eine benutzte und eine unbenutzte „Hälfte“ sowie einen zusätzlichen (relativ großen) „Garten Eden“ ❐ Erzeugung neuer Objekte in Eden ❐ Häufige Bereinigung der jungen Generation durch: ❍ Kopieren lebendiger Objekte von Eden in die unbenutzte Hälfte ❍ Kopieren lebendiger Objekte von der benutzten Hälfte -- in die unbenutzte Hälfte (wenn die Objekte noch „jung“ sind) -- in die alte Generation (wenn sie schon mehrmals kopier t wurden) ❍ Vertauschen von benutzter und unbenutzter Hälfte Sollte die unbenutzte Hälfte zu klein sein, werden Objekte gleich in die alte Generation kopier t, die ggf. vergrößer t wird. ❐ Gelegentliche Bereinigung der alten Generation mit Mark&Sweep oder Mark&Compact. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.6 Generationsverfahren 7 Kopierende Verfahren 7.6.3 Implementierung in Oracle’s HotSpot JVM junge Generation „Eden“ „neugeborene“ Objekte benutzte Hälfte junge Objekte Wurzelzeiger AltjungZeiger unbenutzte Hälfte leer Folgezeiger alte Generation alte Objekte 288 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.6 Generationsverfahren 7 Kopierende Verfahren 7.6.4 Bewertung 289 7.6.4 Bewertung ❐ Für viele Anwendungen kann die durchschnittliche Unterbrechungszeit für eine Speicherbereinigung erheblich reduziert werden. ❐ Lang lebende Objekte werden wesentlich seltener kopier t. ❐ Die Sonderbehandlung von Inter-Generations-Zeigern erforder t Zusatzaufwand bei (bestimmten) Zeigeroperationen des Anwendungsprogramms. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 7.7 Literaturhinweise 7 Kopierende Verfahren 290 7.7 Literaturhinweise ❐ Erster rekursiver Kopieralgorithmus ❍ R. R. Fenichel, J. C. Yochelson: “A Lisp Garbage Collector for Virtual Memory Computer Systems.” Communications of the ACM 12 (11) November 1969, 611− −612. ❐ Iterativer Kopieralgorithmus ❍ C. J. Cheney: “A Non-Recursive List Compacting Algorithm.” Communications of the ACM 13 (11) November 1970, 677− −678. ❐ Effizienzvergleich mit Stackverwaltung ❍ A. W. Appel: “Garbage Collection Can Be Faster Than Stack Allocation.” Information Processing Letters 25 (4) 1987, 275− −279. ❐ Nearly depth-first copying ❍ D. A. Moon: “Garbage Collection in a Large LISP System.” In: G. L. Steele (ed.): Conf. Record of the 1984 ACM Symp. on Lisp and Functional Programming (Austin, Texas, August 1984). ACM Press, 235− −246. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 8.1 Prinzip 8 Mark&Compact-Verfahren 8 Mark&Compact-Verfahren 8.1 Prinzip ❐ Markierungsphase (mark ): ❍ Markierung lebendiger Objekte wie bei Mark&Sweep-Verfahren. ❐ Kondensierungsphase (compact ): ❍ Die lebendigen Objekte werden am Anfang des Heaps zusammengeschoben. ❍ Hierfür muss der Heap u. U. mehrmals durchlaufen werden. 291 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 8.2 Motivation 8 Mark&Compact-Verfahren 292 8.2 Motivation ❐ Kombination der Vor teile von Mark&Sweep- und kopierenden Verfahren: ❍ Gesamtplatzbedarf wie bei Mark&Sweep (keine zwei Heaphälften erforderlich). ❍ Kondensierung der lebendigen Objekte wie bei kopierenden Verfahren (keine Fragmentierung des Heaps, sehr effiziente Speicherzuteilung). ❍ Im Gegensatz zu kopierenden Verfahren bleibt die Anordnung der Objekte bei den meisten Verfahren sogar exakt erhalten. ❐ Nachteil: ❍ Die Verfahren sind grundsätzlich aufwendiger als Mark&Sweep- und kopierende Verfahren. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 8.3 Algorithmen (Auswahl) 8 Mark&Compact-Verfahren 8.3.1 Zwei-Finger-Algorithmus 293 8.3 Algorithmen (Auswahl) 8.3.1 Zwei-Finger-Algorithmus Voraussetzung ❐ Alle dynamischen Objekte besitzen dieselbe Größe, d. h. der Heap ist faktisch ein Array von Objekten (vgl. § 2.1.2). ❐ Ist dies nicht der Fall, muss man den Heap in mehrere Bereiche unterteilen, in denen jeweils nur Objekte einer bestimmen Größe gespeichert werden, und das Verfahren auf jeden Bereich separat anwenden. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 8.3 Algorithmen (Auswahl) 8 Mark&Compact-Verfahren 8.3.1 Zwei-Finger-Algorithmus 294 Ablauf ❐ Phase 1: Lebendige Objekte markieren und zählen → Anzahl n. ❐ Phase 2a: Objekte mit Index i ≥ n in freie Slots mit Index j < n kopieren (und die neue Adresse an der alten Stelle hinterlegen). Hierfür verwendet man zwei „Finger“: ❍ Der linke Finger j bewegt sich von links nach rechts und zeigt jeweils auf den nächsten freien Slot. ❍ Der rechte Finger i bewegt sich von rechts nach links und zeigt jeweils auf das nächste zu kopierende lebendige Objekt. ❍ Wenn sich die Finger treffen, ist man fer tig. ❐ Phase 2b: Zeiger auf Objekte mit Index i ≥ n aktualisieren. Bewertung + Einfach zu implementierendes, effizientes Verfahren. − Zufällige Anordnung der Objekte nach der Kondensierung. − Direkt nur für Objekte einer festen Größe verwendbar. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 8.3 Algorithmen (Auswahl) 8 Mark&Compact-Verfahren 8.3.2 Lisp-2-Algorithmus 295 8.3.2 Lisp-2-Algorithmus Ablauf ❐ Phase 1: Lebendige Objekte markieren. ❐ Phase 2a: Neue Adressen der lebendigen Objekte berechnen und in einer zusätzlichen Zeigerkomponente der Objekte speichern: ❍ Da die lebendigen Objekte später am Anfang des Heaps zusammengeschoben werden, ergibt sich die neue Adresse eines Objekts als Summe der Größen aller lebendigen Objekte mit kleineren Adressen. ❍ Die zusätzliche Zeigerkomponente dient gleichzeitig als Markierung und kann u. U. in der Markierungsphase zur Stackverkettung verwendet werden (vgl. Übungsaufgabe). ❐ Phase 2b: Zeiger aktualisieren. ❐ Phase 2c: Objekte an ihre neuen Adressen verschieben und die Vorwär tszeiger für die nächste Markierungsphase löschen. C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 8.3 Algorithmen (Auswahl) 8 Mark&Compact-Verfahren 8.3.2 Lisp-2-Algorithmus Bewertung + Reihenfolge der Objekte bleibt unveränder t. − Jedes dynamische Objekt braucht Platz für eine zusätzliche Zeigerkomponente (sofern man sie nicht bereits für die Markierung verwendet). − In der Kondensierungsphase muss der Heap dreimal durchlaufen werden. 296 C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 8.3 Algorithmen (Auswahl) 8 Mark&Compact-Verfahren 8.3.3 Tabellenbasier te Algorithmen 297 8.3.3 Tabellenbasierte Algorithmen Prinzip ❐ Die für die Zeigeraktualisierung benötigte Information wird in einer Tabelle gesammelt, die in den „Heap-Löchern“ gespeichert werden kann, aber u. U. mehrmals verschoben (bzw. „weggerollt“) werden muss und dabei u. U. in Unordnung gerät. Bewertung + Reihenfolge der Objekte bleibt unveränder t. + Dynamische Objekte brauchen keinen zusätzlichen Platz für Verwaltungsinformation. − Die Zeigeraktualisierung ist aufwendiger, weil die o. g. Tabelle u. U. sor tier t und wiederholt binär durchsucht werden muss. (Wenn im Heap genügend Platz vorhanden ist, kann dies durch eine zusätzliche Hashtabelle verbesser t werden.) C. Heinlein: Automat. dyn. Speicherverwaltung (SS 2015) 8.4 Literaturhinweise 8 Mark&Compact-Verfahren 298 8.4 Literaturhinweise ❐ Zwei-Finger-Algorithmus ❍ R. A. Saunders: “The LISP System for the Q-32 Computer.” In: E. C. Berkeley, D. G. Bobrow (eds.): The Programming Language LISP: Its Operation and Applications (Four th Edition, 1974). Information International, Inc., Cambridge, MA, 1964, 220− −231. ❐ Lisp-2-Algorithmus ❍ D. E. Knuth: The Art of Computer Programming. Volume I: Fundamental Algorithms (Second Edition). Addison-Wesley, 1973. ❐ Tabellenbasier te Algorithmen ❍ B. K. Haddon, W. M. Waite: “A Compaction Procedure for Variable Length Storage Elements.” The Computer Journal 10, August 1967, 162− −165.
© Copyright 2024