Vorlesungsfolien

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.