13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 363
4.3 Algorithmen in prozeduraler Formulierung
Dieser Abschnitt behandelt Algorithmen in proze- duraler Formulierung und deren Analyse bzgl.
- Speicherbedarf - Laufzeit
Er liefert eine Einblick in die Speicherverwaltung bei prozeduralen Programmen und gibt eine
Ausblick auf weitere algorithmische Problemstellungen und Methoden.
Vorgehen:
- Einführung in die Algorithmenanalyse - Speicherverwaltung
- Laufzeitverhalten
- Prozedurale Algorithmen und deren Analyse - Klassifizierung und Entwicklung von Algorithmen
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 364
4.3.1 Einführung in die Algorithmenanalyse
Zwei zentrale Größen zur Beurteilung von Effizienz:
- Speicher(platz)bedarf (in kByte od. Speicherzellen) - Ausführungs-/Laufzeit (in Sekunden od. Anzahl
ausgeführter Operationen) Bei einem installierten Programm kann man
diese Größen zu gegebenen Eingaben messen bzw. berechnen.
Wir betrachten im Folgenden nur den Fall, dass alle Eingaben am Anfang der Ausführung erfolgen und alle Ausgaben am Ende.
Begriffsklärung: (Speicherbedarf/Laufzeit)
Sei P ein installiertes Programm und M die Menge der zulässigen Eingaben von P.
Der (max.) Speicherbedarf von P ist eine Funktion sb : M kByte
Man spricht von der Raumkomplexität von P.
Die Laufzeit von P ist eine Funktion:
lz : M Sekunden
Man spricht von der Zeitkomplexität von P.
Bemerkung:
In der Praxis ist die Bestimmung von Speicherbedarf und Laufzeit im Allg. nicht einfach:
1. Messen:
- kann nur endlich viele Funktionswerte liefern;
- wird ggf. durch Systemgegebenheiten beeinflusst.
2. Berechnen ausgehend vom Programm:
- setzt Kenntnisse der Übersetzung und des Systems voraus.
Beispiel: (Messen der Laufzeit)
fun primfaktoren (n:int) =
if n<0 then primfaktoren (~n) else if n<=1 then []
else primfakteinb n 2 and primfakteinb n f =
(* 2 <= f <= n *) if n = f
then [f]
else if n mod f = 0
then f::(primfakteinb (n div f) f) else primfakteinb n (f+1)
Ausgewählte Messergebnisse:
Eingabe Laufzeit in Sek.
Gemessen - unter PolyML
- für Betriebssystem XX
- auf einem Rechner der Bauart YY.
12345678 < 1
10000000 < 1
123456789 < 1
100000000 < 1
1234567890 < 1
1000000000 < 1
12345678901 ~ 2
10000000000 < 1
123456789012 > 20000
100000000000 < 1
1234567890123 ~ 15
1000000000000 < 1
12345678901234 > 20000 10000000000000000000000 < 1
Beobachtung:
Laufzeit hängt im Allg. in komplexer Weise von der Eingabe ab.
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 367
Begriffsklärung: (Kriterien zur Effizienz)
Die Gesamteffizienz eines installierten Programms hängt ab von
- der Speicherbedarfsfunktion sb, - der Laufzeitfunktion lz und
- der Häufigkeit, mit der bestimmte Eingaben auftreten.
Bei häufig auftretenden Eingabewerten ist effizientes Verhalten wichtiger als bei seltenen Eingabewerten.
Bemerkung:
• Ein präziser Umgang mit dem obigen Effizienzbegriff ist in der Praxis schwierig:
- sb und lz sind kaum zu bestimmen;
- die Häufigkeitsverteilung der Eingaben ist oft nicht genau bekannt;
- meist möchte man die Effizienz eines Programms unabhängig von seiner Installation betrachten.
• Die Kriterien bilden aber die Grundlage für:
- den Effizienzvergleich von Programmen - die informelle Beurteilung von Effizienz - abstraktere Effizienzbegriffe
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 368
Abstraktere Effizienzbegriffe:
Statt ein installiertes Programm zu betrachten, sieht man üblicherweise von Details ab und betrachtet Effizienz nur näherungsweise.
Vereinfachungen:
1. Betrachte nicht die Eingabewerte selbst, sondern nur ihre Größe (z.B. Länge von Listen, Stellen- anzahl bei Zahlen).
2. Vernachlässige die Häufigkeitsverteilung der Daten.
3. Vernachlässige konstanten Aufwand, also Aufwand, der unabhängig von den Eingabedaten entsteht.
4. Betrachte nur das Wachstum von sb und lz und vernachlässige konstante Faktoren (Abstraktion von der Leistung eines Rechners und den
Implementierungseigenschaften einer Programmier- sprache).
5. Betrachte nur obere und untere Schranken für sb und lz.
Beispiel: (Analyse der Laufzeit)
Wir analysieren eine Implementierung des Algorithmus
„Sortieren durch Auswahl“ (engl. selection sort) .
Eingabe: Feld f von ganzen Zahlen.
Aufgabe: Sortiere das Feld f aufsteigend.
Algorithmische Idee:
- Bestimme eine Komponente mit Indexixminvon f, die ein minimales Element von f[1] ... f[f.length]
enthält.
- Vertausche f[ixmin] und f[1].
- Sortiere dann den Bereich f[2] ... f[f.length] analog.
void main( String[] arg ) {
int[] feld = new int[arg.length];
for( int i = 0; i<feld.length; i++) { feld[i] = Integer.parseInt( arg[i] );
}
sortieren( feld );
for( int i = 0; i<feld.length; i++) { println( feld[i] );
} } }
Mögliche Hauptprozedur:
void sortieren(/*nonnull*/ int[] f) { bereichsort(f,0,f.length-1);
}
void bereichsort( int[] f, int ug, int og) { if (ug >= og) return;
int ixmin = auswaehlen(f,ug,og);
// Vertauschen int temp = f[ug];
f[ug] = f[ixmin];
f[ixmin] = temp;
// Sortieren des restlichen Felds:
bereichsort(f, ug+1, og);
}
/* Liefert Index mit minimalem Element im Bereich f[ug] .. f[og] von Feld f */
int auswaehlen( int[] f, int ug, int og) { int ixmin = ug;
for( int j = ug+1; j <= og; j++ ) { if( f[j] < f[ixmin] ) {
ixmin = j;
} }
return ixmin;
} }
Rekursive Fassung des Sortierens durch Auswahl:
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 371
void sortieren( /*nonnull*/ int[] f ) { for( int i = 0; i<f.length-1; i++ ) {
// bestimme Komponente mit kleinstem // Element in f[i] ... f[f.length-1]
int ixmin = i;
for( int j = i+1; j<f.length; j++ ) { if( f[j] < f[ixmin] ) {
ixmin = j;
} }
// vertausche die Elemente an den // Positionen i und ixmin
int temp = f[i];
f[i] = f[ixmin];
f[ixmin] = temp;
} } }
Iterative Fassung des Sortierens durch Auswahl:
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 372
Analyse der Laufzeit von sortieren:
In Abhängigkeit von der Größe N des Feldes schätzen wir die Anzahl A(N) der Operationen/
Rechenschritte für den ungünstigsten Fall ab.
Eine Operation ist:
- ein Vergleich - eine Zuweisung
- eine Addition/Subtraktion Vereinfachende Annahme:
- Alle Operationen brauchen die gleiche Zeit.
- Andere Aspekte der Ausführung werden vernachlässigt (Speicherverwaltung, Sprünge) Aufwand B(i,N) der inneren Schleife:
B(i,N)≤ (2+1) + (N-i-1) * (2+2)
Aufwand A(N) des gesamten Rumpfes für N≥2:
A(N) = 3 + Σ (1 + B(i,N) + 3 + 2 )
≤3 + Σ (9 + (N-i) * 4 ) = 3 + (N-1)*9 + 4*Σ i
= 9*N – 6 + 2*N*(N-1) = 2*N + 7*N – 6 A(0) = A(1) = 3
i=0 N-2
i=1 N-1
i=1 N-1
2
O-Notation
Häufig interessiert man sich nur für die Größenordnung des Wachstums der
Aufwandsfunktion A(N), die den Aufwand an Speicher bzw. Zeit in Abhängigkeit von der Problemgröße N beschreibt.
Begriffsklärung: (obere Schranke)
einer Aufwandsfunktion A, wenn gilt:
Es gibt c,d in Nat, sodass für alle N in Nat gilt:
A(N) ≤ c * f(N) + d
Man sagt auch, A wächst wie f bzw. ist von der Größenordnung f. Die Menge aller Funktionen von der Größenordnung f bezeichnet man mit O(f) : O(f) = { g | ∃c,d in Nat:∀N in Nat: g(N)≤c* f(N) + d } .
Bemerkung:
• Entsprechend definiert man auch untere Schranken.
• Vertiefung in „Entwurf und Analyse von Algorithmen“
• Meist schreibt man O(N) statt O(λN.N), O(N ) statt O(λN.N ), O(log N) statt O(log), usw.
2 2
Eine Funktion f: Nat
R
+ heißt obere SchrankeDer Zeitaufwand A vom Sortieren durch Auswahl ist
und damit in O(N ); denn mit c= 3 und d = 6 gilt:
A(N) ≤ 3 * N + 6 A(N)≤2*N + 7*N - 62
Wichtige Komplexitätsklassen:
Beispiel: (Bestimmung oberer Schranken)
2 2
Kompl.klasse Bezeichnung Beispiel
O(1) konstant Hashverfahren
O(log N) logarithmisch binäre Suche in Bäumen
O(N) linear sequentielle Suche
O(N * log N) n log n gute Sortierverfahren O(N ) quadratisch einfache Sortierverfahren O(N ) kubisch Matrixmultiplikation O(2 ) exponentiell Optimierungverfahren
2 3 N
Algorithmen mit einem Aufwand in O(N ) , k≥2, nennt man polynomisch oder engl. polynomial.
k
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 375
Diskussion der O-Notation:
• Die O-Notation liefert eine grobe Klassifikation.
• In der Praxis können konstante Faktoren und Programmiersprachen-spezifische Aspekte entscheidend sein (siehe Beispiel unten).
• Weitere Aspekte für die Effizienzbetrachtung:
- Antwortzeiten interaktiver Softwaresysteme - Kommunikationszeiten
Beispiel: (zur obigen Diskussion)
Wir betrachten zwei Versionen eines Programms, das eine Datei liest und wieder ausgibt.
Die Versionen illustrieren insbesondere:
• Abhängigkeit von programmiersprachlichen Aspekten (hier: Komplexität von scheinbaren und wirklichen Grundoperationen)
• Notwendigkeit des Verständnisses technischer Aspekte zur Beurteilung nicht-funktionaler Eigen- schaften
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 376
Abstrakte Schnittstelle zum Lesen von Dateien:
/* Ein Verbund vom Typ DateiLesePosition repräsentiert eine Position in einer Datei.
An dieser Position kann man das nächste Zeichen lesen.
*/
class DateiLesePosition { ...
}
/* Öffnet Datei mit Namendnameund liefert Referenz vom Typ DateiLesePosition, die die Anfangsposition in der Datei repräsentiert.
Gibt es Fehler beim Öffnen, wirdnull zurückgegeben.
*/
DateiLesePosition oeffneDatei( String dname ){
...
}
/* Ist die aktuelle Position am Dateiende, wird das EOT-Zeichen ‘\4‘ geliefert. Andernfalls wird das Zeichen an aktueller Position geliefert und die Position eins weiter geschaltet.
*/
char naechstesZeichen( DateiLesePosition d ) { ...
}
Ineffiziente Programmversion: Datei-Lesen Version 1 public static void main( String[] a ) {
if( arg.length == 1 ) {
DateiLesePosition dlp = oeffneDatei(a[0]);
if( dlp == null ) {
println("Fehler: Datei existiert nicht");
return;
}
String s = "";
char c = naechstesZeichen(dlp);
int count = 1;
while( c != '\4' ){ // '\4' ist EOT s = s + c;
c = naechstesZeichen(dlp);
count++;
if( count%1000==0 ) println(count);
}
println("Datei Inhalt:");
println( s );
} else {
println("Usage: java DateiLesen <file>");
} }
Bemerkung:
Obige Schnittstelle arbeitet nur für Dateien korrekt, die das EOT-Zeichen (end of transmission) nicht enthalten.
public static void main( String[] a ) { if( arg.length == 1 ) {
DateiLesePosition dlp = oeffneDatei(a[0]);
if( dlp == null ) {
println("Fehler: Datei existiert nicht");
return;
}
final int feldgroesse = 100000;
String s = "";
char[] cfeld = new char[feldgroesse];
char c = naechstesZeichen(dlp);
int count = 1;
int index = 0;
while( c != '\4' ){ // '\4' ist EOT cfeld[index] = c;
c = naechstesZeichen(dlp);
count++;
index++;
if( count%1000==0 ) println(count);
if( index == feldgroesse ) { index = 0;
s = s + new String(cfeld);
} }
println("Datei Inhalt:");
println( s );
} else {
println("Usage: java DateiLesen <file>");
} }
Effizientere Programmversion: Datei-Lesen Version 2
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 379
Gemessene Zeit zum Lesen einer 150 kByte großen Datei auf einem AMD Opteron Dual Core 270 Rechner:
V1: 76,1 s V2: 0,4 s Grund:
„+“ auf String kopiert Argumente; dadurch ergibt sich insgesamt für V1 eine quadratische Laufzeit- komplexität in Abhängigkeit von der Seitengröße.
V2 hat zwar theoretisch die gleiche Komplexität, aber diese wirkt sich nur bei sehr großen Dateien aus.
Bemerkung:
Bei der Betrachtung der Komplexität vergisst man leicht, dass sich die Komplexitätsklassen nur auf das asymptotische Verhalten beziehen.
Für kleine N (die man in der Realität oft hat) kann das ganz anders aussehen.
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 380
4.3.2 Speicherverwaltung
Speicher ist eine wichtige Ressource für Softwaresysteme. Viele nicht-funktionale Eigenschaften hängen vom angemessenen Umgang mit Speicher ab.
Wir betrachten grundlegende Aspekte der Speicherverwaltung:
- Einführung: Speicher in der Programmierung - Automatische Speicherbereinigung
- Deallokation von Objekten
Einführung
Wir betrachten hier nur den Speicher, der für die Ausführung von Programmen benötigt wird, und zwar in Form eines Byte-adressierbaren virtuellen Adressraums.
Wichtige Fragen:
- Wofür wird Speicher benötigt?
- Wie ist der Speicher organisiert?
- Wie wird der Speicher verwaltet?
- Wie viel Speicher braucht ein Programm?
Wofür Speicher benötigt wird:
Beispiele: (Verwendung von Speicher)
Speicher wird benötigt für:
- das Programm (unabhängig von Eingabedaten) - Konstanten
- Variablen (global, prozedurlokal, objektlokal) - Verwaltung von Prozedur-/Methodenaufrufen
1. Programm, Konstanten und globale Variable:
Speicherbedarf ist relativ einfach zu bestimmen, da unabhängig von der Eingabe.
String s;
String ss;
public class SpeicherIllustration1 {
public static void main( String[] ins ) { s = ins[0] + " war die Eingabe";
ss = "Zu Seiteneffekten lesen "
+ "Sie die Dokumentation\n"
+ "und fragen Sie Ihren Tutor "
+ "oder Professor";
println( s + "\n" + ss );
} }
2. Verbundkomponenten/Objektlokale Variablen:
Speicherbedarf hängt von der Eingabe ab. Lebens- dauer der Verbunde erstreckt sich von der Erzeugung bis zum Ende des Programmablaufs.
class IntList { int fst;
IntList rst;
}
IntList empty() { return new IntList();
}
IntList append( int i, IntList xl ) { IntList il = new IntList();
il.fst = i;
il.rst = xl;
return il;
}
public class SpeicherIllustration2 {
public static void main( String[] ins ) { IntList il = append(3,empty());
il = append( 134, il );
il = append( 9, il );
int i = Integer.parseInt( ins[0] );
while( i > 0 ) {
il = append( i, il );
i--;
} } }
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 383
3. Lokale Variablen und Prozeduraufrufverwaltung:
Speicherbedarf hängt von der Eingabe ab. Lebens- dauer der lokalen Variablen erstreckt sich vom Prozeduraufruf bis zum Ende der Prozedur- ausführung.
Als Beispiel betrachten wir die rekursive Fassung des Sortierens durch Auswahl (vgl. Folie 370).
Speicher wird benötigt für jede Prozedurinkarnation (vgl. Begriffsklärung auf Folie 321) und zwar
- für den Rückgabewert und die aktuellen Parameter, - für die Aufrufverwaltung (z.B. Aufrufstelle),
- für die lokalen Variablen.
Speicherbereich für eine Prozedurinkarnation:
Rückgabewert Aktuelle Parameter Verwaltungsinformation Lokale Variable
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 384
Speicherorganisation:
Die Speicherorganisation ist bei den meisten prozeduralen bzw. objektorientierten Programmier- sprachen ähnlich:
virtueller Adressraum
BS-Kern
Programm
Halde
Laufzeitkeller
globale Größen globale, statische Variablen, Konstanten, ...
Zwischenergebnisse, prozedurlokale
Größen, Objekte mit be- schränkter Lebensdauer (dynamische) Verbunde, Objekte, ...
Wie Speicher verwaltet wird:
1. Globaler Speicher und Keller werden automatisch verwaltet (der Übersetzer erzeugt dafür Code).
2. Je nach Programmiersprache wird die Halde (engl. heap) unterschiedlich verwaltet:
- mit automatischer Speicherbereinigung, - durch den Programmierer (Deallokation).
Operationen zur Verwaltung der Halde:
Anfordern von Speicher bei Objekterzeugung:
liefere Speicherbereich ausreichender Größe.
Freigabe von Speicher:
- Wenn kein Speicher mehr verfügbar, gebe die Speicherbereiche von Objekten frei, die nicht mehr erreichbar sind.
- Gebe Speicher von Objekten auf Anweisung des Programms frei (Deallokation).
Beachte:
Die Speicherverwaltung kostet auch Laufzeit.
Fazit:
Automatische Speicherbereinigung
1. Wie viel Speicher ein Programm in Abhängigkeit von der Eingabe genau braucht, hängt von Details der Sprachimplementierung und Plattform ab.
2. Mit einem generellen Verständnis der relevanten Techniken lässt sich der Speicherbedarf aber gut abschätzen.
Begriffsklärung: (Autom. Speicherbereinigung)
Verfahren zur automatischen Speicherbereinigung (engl. automatic garbage collection) ermitteln periodisch oder bei Bedarf, welche Objekte nicht mehr erreichbar (s.u.) sind und geben deren Speicherplatz frei.
Weiteres Ziel ist es, den freien Speicher zu kompaktifizieren.
Immer mehr Programmiersprachen bieten
automatische Speicherbereinigung (insbesondere funktionale, logische und objektorientierte Sprachen):
• Vereinfachung der Programmierung
• Aufwand an Speicher und Zeit ist vertretbar.
• Sicherheitsaspekte
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 387
Beispiel: (Autom. Speicherbereinigung)
void main( String[] ins ) { int count = 1;
while( true ) {
int[] feld = new int[1000000];
println(count++);
} }
1. Programm, das unerreichbare Objekte erzeugt:
Dies Programm bekommt keine Speicherprobleme.
2. Programm, dessen erzeugte Objekte erreichbar sind:
Dies Programm terminiert mit OutOfMemoryError.
class ListOfArray { int[] elem;
ListOfArray next;
}
void main( String[] ins ) { ListOfArray la = null;
int count = 1;
while( true ) {
ListOfArray tmp = new ListOfArray();
tmp.elem = new int[1000000];
tmp.next = la;
la = tmp;
println(count++);
} }
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 388
Begriffsklärung: (Erreichbarkeit)
Ein Verbund bzw. Objekt X heißt von einer Variablen v direkt erreichbar, wenn v eine Referenz auf X enthält.
X heißt von v erreichbar, wenn es von v direkt erreichbar ist oder wenn es einen Verbund/ ein Objekt Y mit Komponente w gibt, so dass X von w direkt erreichbar ist und Y von v erreichbar ist.
Die Menge der Wurzelvariablen zu einem Ausfüh- rungszustand A umfasst alle globalen Variablen sowie die aktuell im Keller vorhandenen lokalen Variablen und Parameter.
Ein Objekt heißt erreichbar in einem Ausführungs- zustand A, wenn es von einer Wurzelvariablen zu A erreichbar ist.
Verbunde/Objekte können nur von Wurzelvariablen oder von Verbundkomponenten/Instanzvariablen referenziert werden.
Bemerkung:
Deallokation von Verbunden/Objekten
Begriffsklärung: (De-/Allokation)
Die Bereitstellung des Speicherbereichs bei der Erzeugung von Verbunden und Objekten nennt man Allokation (engl. allocation). Die Freigabe solcher Speicherbereiche Deallokation (engl.
deallocation).
Die meisten prozeduralen Programmiersprachen unterstützen De-/Allokation durch den Programmierer:
• Vorteil:
- ermöglicht effiziente Benutzung von Speicher
• Nachteile:
- zusätzlicher Programmieraufwand - potentielle Fehlerquelle
- führt leicht zu Sicherheitslücken
Wir betrachten hier De-/Allokation von Objekten in C++.
Verbunde und Zeiger in C++:
C++ unterscheidet zwischen Verbunden und und Zeigern/Referenzen auf Verbunde.
#include <iostream>
class Punkt { public:
int x;
int y;
};
Punkt* puenktchen() { Punkt* p = new Punkt();
p->x = 1;
p->y = 2;
Punkt q;
q.x = 3;
q.y = 4;
return p;
}
int main() {
Punkt* r = puenktchen();
cout << "X-Koordinate: " << r->x << '\n';
cout << "Y-Koordinate: " << r->y << '\n';
return 0;
}
Beispiel: (Verbunde und Zeiger)
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 391
Wie in Java, alloziert der Operator „new“ in C++
Speicher für neue Verbunde. Als Ergebnis liefert er einen Zeiger auf den neuen Verbund.
Ist K ein Verbundtyp, dann bezeichnet in C++
K* den Typ der Zeiger auf Verbunde vom Typ K.
Den Speicherplatz, den man mitnewalloziert hat, kann man durch Aufruf des Operatorsdeletewieder freigeben, wenn er nicht mehr gebraucht wird.
// ... wie auf Folie 390 int main() {
Punkt* r = puenktchen();
cout << "X-Koordinate: " << r->x << '\n';
cout << "Y-Koordinate: " << r->y << '\n';
delete r;
return 0;
}
Beispiel: (Verbunde und Zeiger, Fortsetzung)
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 392
Zum Vergleich mit Java (s. Folie 387) betrachten wir zwei Varianten eines C++ Programms mit und ohne Deallokation. Sei Klasse Vektor gegeben:
Beispiel: (Wirkung der Deallokation)
class Vektor { public:
int elems [1000000];
};
int main() { while( true ) {
Vektor* vp = new Vektor();
}
return 0;
}
Folgendes Programm führt zu einem Abbruch wegen Speicherüberlaufs:
Deallokation der Vektorverbunde verhindert den Speicherüberlauf:
int main() { while( true ) {
Vektor* vp = new Vektor();
// mache irgendwas mit dem Vektor:
delete vp;
}
return 0;
}
Verwendung von Deallokation:
Ein Programmteil P kann einen Speicherbereich freigeben, wenn:
- P den Speicherbereich nicht mehr benötigt, - P den Speicherbereich kontrolliert, d.h. sicher sein
kann, dass er von keiner anderen Stelle benötigt wird.
Weitere Aspekte der Deallokation:
• Deallokation wird in einigen Sprachen durch weitere Sprachmittel unterstützt (z.B. Destruktoren in C++).
• Deallokation und automatische Speicherbereinigung lassen sich kombinieren:
- Anweisungen an den Garbage Collector - Soft und weak references in Java
• Deallokation bezieht sich nicht nur auf Speicher-, sondern auch auf andere Ressourcen.
• Ein systematischer Umgang mit einer Ressource bedeutet zu klären,
- wer die Ressource kontrollieren soll, - wer Zugriff auf die Ressource erhält.
4.3.3 Laufzeitverhalten
Dieser Abschnitt ergänzt die in 4.3.1 vorgestellten Aspekte zum Laufzeitverhalten.
Das Laufzeitverhalten eines Programms wird bestimmt durch:
- die Anweisungen des Programms - den Übersetzer
- die Laufzeitumgebung, insbesondere die die Speicherverwaltung
- die Systemumgebung.
Speicherverwaltung kostet Zeit:
Speicherverwaltung ist aufwendig, wenn der ver- fügbare Speicher knapp wird:
- Garbage Collector muss häufig aufgerufen werden.
- Das Aufsuchen ausreichend großer freier Speicher- bereiche wird aufwendiger.
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 395
Systemumgebung beeinflusst das Laufzeitverhalten:
Insgesamt ist der Aufwand in der Praxis nicht leicht abzuschätzen, weil
- der Speicherverbrauch der Bibliotheksklassen und anderer fremder Programmteile häufig nicht klar spezifiziert ist;
- die Details der Speicherverwaltung eine wichtige Rolle spielen.
Problematisch ist das insbesondere bei Echtzeit- anforderungen.
Zur Gesamtbeurteilung des Laufzeitverhaltens eines Softwaresystems muss auch die Systemumgebung berücksichtigt werden:
- Benutzerinteraktion - Anzahl von Benutzern - Kommunikationszeiten
- Laufzeitverhalten der Plattform - Interaktion mit anderen Systemen
13.12.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 396
Fazit:
- Präzise Bestimmung der Effizienz ist im Allg.
schwierig und von vielen technischen Aspekten abhängig; aber auch nur bei ausgewählten Anwendungen nötig.
- Durch geeignete Abstraktion kann man nachvoll- ziehbare Aussagen über die Effizienz eines Algorithmus‘, Programms oder Softwaresystems machen.