2 Prologsysteme

2 Prologsysteme 7
2.1 Entwicklung von Prolog 7
2.1.1 Système-Q 7
2.1.2 Prolog 0 8
2.1.3 Prolog und Logik 10
2.2 Vergleich der Spracheigenschaften 10
2.2.1 Referenzielle Transparenz 10
2.2.2 Metasprachlichkeit 11
2.2.2.1 Prolog 12
2.2.2.2 Funktionale Sprachen 12
2.2.2.3 Imperative Sprachen 12
2.2.2.4 Maschinensprachen 12
2.3 Prologs Implementierungsmodell 13
2.3.1 Deterministisches Prolog 13
2.3.2 Nichtdeterministisches Prolog 13
2.4 Anforderungen an die Speicherverwaltung 13
2.4.1 Zeitliches Speichermodell 14
2.4.2 Flüchtige Datenstrukturen 14
2.4.2.1 Implementierungskosten 14
2.4.2.2 Beispiele 16
2.4.2.2.2 Metainterpreter 16
2.4.2.2.2 Matrizenmultiplikation 18
2.4.2.2.3 Sortieren 18
2.4.2.3 Unterstützung des Programmierstils 19
2.4.3 Permanente Datenstrukturen 19
2.4.4 Redundante Darstellung 20

2 Prologsysteme

In diesem Kapitel wird die geschichtliche Entwicklung der Programmiersprache Prolog kurz angerissen, um dann auf die Probleme bei der Implementierung einzugehen. Es wird auf die für Prolog und wenige andere Programmiersprachen charakteristische Eigenschaft der referenziellen Transparenz eingegangen, und gezeigt, wie dadurch der Programmierer unterstützt wird. Das typische Implementierungsmodell für Prolog wird vorgestellt und die Notwendigkeit einer Speicherbereinigung neben der Verwaltung über Stacks begründet. Beispiele, die zeigen, wann eine Verwaltung mittels eines heaps und einer dazugehörigen Speicherverwaltung einen klareren Programmierstil unterstützt, werden vorgestellt. Dabei wird sich herausstellen, daß man hier einerseits mit sehr kurzlebigen -flüchtigen (volatile, transient)- und andereseits mit permanenten Datenstrukturen zu rechnen hat. Die Anforderungen an eine Speicherbereinigung werden hierauf formuliert.

2.1 Entwicklung von Prolog

Prolog (programmation en logique) stellt eine besonders einfache Vereinigung vieler Konzepte, die Ende der 60-er Jahre diskutiert wurden, dar. Auf der einen Seite sind Forschungsergebnisse im Bereich der Logik, Kowalskis Vereinfachung von Robinsons Resolution, auf der anderen Seite praktische Implementierungstechniken aus Bereichen des Übersetzerbaus Pate gestanden. Colmerauer, der über Jahre mit der Implementierung von Compilern beschäftigt war, erkannte die Mächtigkeit der von van Wijngaarden entwickelten W-Grammatik. W-Grammatiken können Typ 0 Sprachen erzeugen und damit implizit jede Berechnung darstellen. Bestechend an den W-Grammatiken ist, daß man durch Regeln eine möglicherweise nichtdeterministische Berechnung darstellen kann. Colmerauer verwendete diesen Formalismus probehalber zur Übersetzung von französischen und englischen Sätzen [ChaCo69]. Ein Problem der W-Grammatiken stellt ihre aufwendige Implementierung dar.

2.1.1 Système-Q

In der Folge entwickelte er Système-Q, das als der Vorgänger von Prolog bezeichnet werden kann. Hier wurden bereits das Berechnen in mehrere Richtungen und Nichtdeterminismus, aber noch nicht die Unifikation realisiert. Nicht unähnlich von Chomsky Typ 0 Grammatiken konnten Terme umgeformt werden. Damit hatte Colmerauer unabhängig von den Entwicklungen innerhalb der formalen Logik ein System geschaffen, das schon fast alle Eigenschaften von Prolog enthält. Auch heute noch werden in Kanada mit diesem System englische Wetterberichte ins Französische übersetzt.

Erst nach dieser Entwicklung (1970) erfuhr Colmerauer in Marseille von den Arbeiten Robinsons. Mit Phillipe Roussel entwickelte er den Begriff der formellen Gleichheit für die Unifikation. Dadurch galt für Terme nicht mehr das Assoziativgesetz, dafür ließ sich die Unifkation aber wesentlich effizienter realisieren. Kowalski hatte sich, als reiner Logiker, die Einbindung des Axioms der Assoziativität in die Horn-Klauseln noch vorgestellt. In [Kowa88] beschreibt Kowalski die Entwicklungen jener Zeit. Auch wenn diese Eigenschaft gerade für Zeichenfolgen elementar ist, wurde erst in den letzten Jahren diese Idee wieder aufgegriffen [ComLe88 und Referenzen]. Colmerauer überarbeitete sein Système Q vollständig, um es in Einklang mit den Formalismen der Logik zu bringen. Anstatt der Produktionen ähnlich den Typ 0 Grammatiken in Système Q wurden Horn-Klauseln verwendet. Eine Horn-Klausel ist entweder eine Klausel eines definiten Programms oder ein definites Ziel [Llo87]. Horn Klauseln besitzen maximal ein positives Literal. Vom Standpunkt der Grammatiken aus betrachtet wurden dadurch die Produktionen kontextfrei, während in Typ 0 Grammatiken auch kontextabhängige Produktionen verwendet werden können. Die Ausführung eines Programms entspricht nun dem Beweisen eines definiten Ziels unter Verwendung der SLD-Resolution (Linear resoultion with Selection function of Definite clauses). Colmerauer entschied sich für eine Selektionsfunktion mit Tiefensuche und die der Auswahl der Literale nach ihrer Ordnung in einer Klausel von links nach rechts. Dadurch war es möglich, eine sehr effiziente Implementierung, die den Prozeduraufrufen in herkömmlichen Sprachen ähnelt, zu realisieren.

Hornklauseln:
A :-
	B1,...,Bn        Klausel eines definiten Programms,
:- B1,...,Bn             Definites Ziel
keine Hornklausel:
:- A1,A2 ,  B1,...,Bn

2.1.2 Prolog 0

Die erste Prolog-Implementierung -Prolog 0- wurde von Phillipe Roussell im Jahre 1971 in Algol W fertiggestellt. 1972 wurde das erste größere Prologprogramm, eine natürlichsprachige Schnittstelle, in Prolog 0 entwickelt. Prolog 0 besaß bereits ein sicheres Prädikat zur Feststellung der formellen Ungleichheit ( dif/2 ), das erst jetzt, abgesehen von den Prologsystemen aus Marseille, wieder Eingang in neuere Prologsysteme findet. In Prolog 0 kann man noch viel deutlicher als im heutigen Prolog die Beziehung zu den Horn-Klauseln erkennen. Positive und negative Literale werden durch ein Vorzeichen unterschieden.
+VATER(PETER,PAUL) ;;
+MUTTER(MARIA,JAKOB) ;;
+GATTE(PETER,MARIA) ;;
+BRUDER(*Z,*Y) -VATER(*X,*Y ) -VATER(*X,*Z) -DIF(*Y,*Z) ;;
+VATER(*X,*Z) -GATTE(*X,*Y) -MUTTER(*Y,*Z) ;;
Anfrage: -BRUDER(PAUL,*X) / +ANTWORT(*X) ..
Antwort: +ANTWORT(JAKOB) ;.

	Prolog 0- Programm

Beim Beweis eines Programms wurden ausgehend von der Anfrage stets ein negatives Literal (das Ziel) mit einem positiven Literal (Kopf einer Klausel) unifiziert, und durch die negativen Literale dieser Klausel (Rumpf) ersetzt. Ziele, die nicht direkt bewiesen werden konnten -etwa -DIF(*Y,*Z)- wurden in der Liste der negativen Literale (Zielliste) belassen. Dadurch konnte auf einfache aber nicht sehr effiziente Weise das Aufschieben von Zielen und damit eine sichere Implementierung des dif/2 realisiert werden. Insofern war dieser Interpreter schon wesentlich fortgeschrittener als die meisten heutigen Systeme. Die Arbeitsweise eines Prologinterpreters läßt sich also wie folgt schematisch darstellen:

In der weiteren Folge wurde Prolog vor allem durch eine große Menge von vordefinierten Prädikaten erweitert. Der eigentliche Mechanismus wurde nicht mehr verändert. Durch die Übersetzungstechnik von D.H.D. Warren [Warr77,Warr83] konnte Prolog mit ähnlicher Effizienz wie andere symbolische Sprachen implementiert werden. Dadurch waren die Voraussetzungen für einen breiteren Einsatz von Prolog gegeben. Auf der anderen Seite entdeckten immer mehr Forscher, die zuvor an dem bis heute noch immer nicht erreichten Ziel der automatischen Verfikation imperativer Programme gearbeitet hatten, die Schönheit von Prolog, das bis zu einem gewissen Grad nur mehr die Zusicherungen (assertions) aber nicht mehr die dazwischenstehenden obskuren imperativen Programmteile, darstellt. Prolog wurde so bei einer immer größer werdenden Gruppe von Theoretikern populär, die erkannten, daß sie nur in diesem Rahmen, dem Bereich der Logik-Programmierung, theoretische Erkenntnisse rasch in die Praxis umsetzen konnten, ohne dabei allzu lange mit den Implementierungsdetails ihrer Ideen aufgehalten zu werden. Der Begriff der Unifikation etwa, der sich in Prolog nur auf einfache Terme bezieht, ist eine Hintertür, um in Prolog noch weitere interessante Gleichungssysteme zu verwenden. So wurde in Marseilles mittlerweile Prolog III entwickelt, das auch Gleichungen über rationale Zahlen lösen kann. Allgemein gehören diese Erweiterungen zum sog. CLP-Schema (Constraint Logic Programming) [JaLa87]. Prolog stieß in viele Bereiche der theoretischen Informatik, etwa der algebraischen Spezifikation [GoMe87] oder der Programmtransformation [Hog81] vor, in denen praktische Implementierungen rasch verwirklicht werden können.

Trotz des theoretischen Umfelds, das Prolog umgibt; der Logik-Programmierung, konnte Prolog auch in anderen Bereichen der Informatik, etwa beim VLSI-Entwurf seine Verwendbarkeit beweisen. Anwendungen, die aus dem Bereich der künstlichen Intelligenz -dem ursprünglichen Anwendungsgebiet von Prolog- stammen, vor allem Expertensysteme und die Verarbeitung natürlicher Sprache dürfen natürlich nicht unerwähnt bleiben. Die Verwendung von Prolog hat sich, ausgehend von einigen Universitäten und vielen Pionieren, in den letzten fünf Jahren auf den industriellen Bereich ausgedehnt, sodaß seit Frühjahr 1988 an einer internationalen Prolog-Standardisierung innerhalb der ISO SC22 WG17 [Der89] gearbeitet wird.

2.1.3 Prolog und Logik

Immer wieder wird vor allem von theoretischer Seite her kritisiert, daß Prolog eine zu einfache Berechnungsstrategie besitzt, die bei der Konstruktion des SLD-Beweisbaumes nicht immer alle Lösungen finden kann. Auch Negation kann nur in beschränktem Ausmaß verwendet werden;.Betrachtet man Prolog aus der geschichtlichen Perspektive, so ist dies gar nicht verwunderlich: Prolog ist primär das effizient implementierbare Produkt eines Übersetzerbauers und nicht Derivat aus der formalen Logik. Systeme, die Beweise finden, gab es schon in den 60-er Jahren; keines dieser Systeme fand aber wegen der enormen Ineffizienz jemals weitere Verbreitung. Prolog ist hingegen eine Programmiersprache, mit der man mit ähnlichem Aufwand wie in prozeduralen Sprachen arbeitet. Systeme, die eine faire Berechnungsstrategie verwenden, können in Prolog mit minimalem Programmieraufwand realisiert werden.

Prolog hat also mit Prädikatenlogik nur sehr wenig zu tun. Aber vielleicht hat sich Philippe Roussel nur verhört, als seine Frau in einem Zug zwischen Paris und Marseille den Namen Prolog erfand und Prolog steht in Wirklichkeit für programmation la logique.

2.2 Vergleich der Spracheigenschaften

Im folgenden werden einige unbestrittene Eigenschaften von Prolog im Vergleich mit anderen Programmiersprachen hervorgestrichen. Um Prolog auf gleicher Ebene mit ihnen zu messen, werden nur jene Aspekte erwähnt, die auch in anderen Programmiersprachen Eingang gefunden haben oder finden sollten.

2.2.1 Referenzielle Transparenz

Der Begriff der referenziellen Transparenz wurde anfänglich im Bereich der Funktionalen Sprachen, wie Hope [Fie88] geprägt. Dort versteht man darunter, daß ein Funktionsausdruck unabhängig vom Zeitpunkt seiner Interpretation stets denselben Wert hat. Auf imperative Programmiersprachen übertragen müßten die Objekte der Sprache auch unabhängig von ihrem Kontext bzw. Sichtbarkeits-/ Gültigkeitsbereich (scope) als ein Wert verstanden werden können. Referenzen auf Werte (z.B. Variablen) können im Gegensatz dazu in Programmiersprachen mit vorwiegend imperativem Programmierstil (z.B. Pascal, Lisp) nur aufgrund ihres zeitlichen Verhaltens -ihrer Geschichte- verstanden werden; derartige Sprachen werden daher auch referentiell opak genannt. In einem prozeduralen Programm verletzt selbst ein simples Programm wie "x := expr; x := x + 1;" die Transparenz der dargestellten Werte, da hier der zeitliche Zustand einer Variable zum Verständnis des Programms benötigt wird. Insbesondere ist es in einer prozeduralen Sprache unmöglich, direkt auf einen früheren Wert einer Variable zuzugreifen. Wenn man in einem prozeduralen Programm "sicher" mit Werten arbeiten will, muß man sie bei jeder Änderung vollständig kopieren. Typisch dafür sind etwa Stringmanipulationen in C mittels strcpy. Es gibt kaum imperative Programmiersprachen, bei denen man sich bemüht hätte, diese unangenehme Eigenschaft auf die notwendigsten Bereiche zu beschränken. Eine der wenigen Ausnahmen stellt hier CLU [LisGu86] dar: Durch eigene unveränderbare Datentypen können applikativen Sprachen ähnliche Objekte verwendet werden. Anstatt einer herkömmlichen Zuweisung wird ein neues verändertes Objekt geschaffen. Für den Fall, daß zeitliche Übergänge durch die Problemstellung zu modellieren sind, sollten zumindest Abstrakte Datentypen, die u.U. algebraisch beschrieben werden können, verwendet werden. Bezogen auf Prolog können wir den Begriff der referentiellen Transparenz ebenso verwenden. Es ergeben sich hier allerdings einige Unterschiede zu den rein Funktionalen Sprachen.

2.2.2 Metasprachlichkeit

Unabhängig davon, welche Sprache wir sprechen oder verwenden, können wir uns sicher sein, daß wir durch neues Wissen eine bessere oder einfachere Beschreibungsform, daher eine neue Sprache, benötigen werden. Dies ist ein üblicher Prozeß, der eine Stufe weiterer Erkenntnis in der neuen Sprache zum Ausdruck bringt. In allen Lebensbereichen und Wissenschaften entstehen so neue Sprachen, die einer gezielten Kommunikation dienen. Dies gilt auch für Beschreibungssprachen in der Informatik. Wichtig erscheint es daher, neue Sprachen mit möglichst geringem Aufwand beschreiben und implementieren zu können. So, wie die Alltagssprache zur Einführung in eine Wissenschaftssprache verwendet wird, sollten auch Programmiersprachen die Definition von mächtigeren oder spezialisierteren Programmiersprachen ermöglichen. Damit hätten wir zugleich auch eine verwendbare Implementierung der neuen Sprache. Metasprachliche Abstraktionen ermöglichen die Definition, Implementierung von Sprachen; die Bearbeitung ganz allgemein (etwa Erklärung, Analyse). Ein gutes Maß zur Abschätzung, der Eignung einer Sprache zur Verwirklichung von metasprachlichen Abstraktionen, stellt die Realisierung der Sprache -insbesondere der Programmiersprache- durch sich selbst dar. Dadurch kann man auch Sprachen ganz unterschiedlicher Ebenen auf ihren Anspruch, eine general purpose language zu sein, untersuchen. Wenn es in einer Sprache nur sehr schwer möglich ist, sich selbst zu beschreiben, wird es auch sehr schwer möglich sein, essentielle Erweiterungen für sie zu finden. Da Programmiersprachen selbst formal definiert werden, wird man meist den Abstrakten Syntaxbaum als Ausgang dieser Untersuchung verwenden.

2.2.2.1 Prolog

In Prolog wird der Abstrakte Syntaxbaum eines vorhandenen Programms klauselweise mittels clause/2 erzeugt. Mittels sog. Metainterpreter wird dieser Struktur eine deklarative bzw. operationale Semantik gegeben. Diese Technik wird in Prolog ausgiebig [StSh86] verwendet. Unter einem Metainterpreter versteht man in der Tradition der Logikprogrammierung ein Programm, das eine ähnliche Sprache ausführt, in der das Programm geschrieben worden ist. In weiterer Folge spricht man hier auch von meta-level architectures, in [Har88] findet sich eine aktuelle Klassifikation über die möglichen konzeptuellen Ausprägungen. Man kann zudem in Prolog zwischen verschiedenen Ebenen der Beschreibung wählen. Für gewisse Anwendungen wird man etwa nur die Klauseln in einer anderen Reihenfolge interpretieren wollen, nur die Unifikation verändern oder neue Datentypen einführen wollen. Je weiter man Prolog durch sich selbst beschreibt, umso komplexer wird diese Darstellung werden. Zumeist kommt man aber mit dem dreizeiligen Grundgerüst aus. Eine komplette Beschreibung wird aber auch in Prolog eine entsprechende Größe besitzen. Die vollständige Spezifikation von Prolog in Hornklauseln mit Negation [Der87a] umfaßt momentan zirka 70 ausführlich mit Beispielen kommentierte Seiten [Der89].

2.2.2.2 Funktionale Sprachen

In diesem Bereich hat sich für dasselbe Konzept, insbesondere in der Scheme-Tradition [Abe85], die Bezeichnung metacircular evaluator eingebürgert. Neuere Funktionale Programmiersprachen unterstützen aber kaum metasprachliche Abstraktionen. Offensichtlich waren ihre Erfinder der Meinung, damit bereits genügend mächtige Sprachen implementiert zu haben.

2.2.2.3 Imperative Sprachen

Da die Struktur von herkömmlichen imperativen Programmiersprachen von den sprachlichen Möglichkeiten dieser Programmiersprachen her nur sehr schwer erfaßt werden kann, gibt es kaum praktisch verwendete Realisierungen von metasprachlichen Abstraktionen. Es wäre zwar denkbar, den Syntaxbaum der Sprache als eigenen Datentyp einzuführen, die Operationen auf derartigen Strukturen sind aber durch des Fehlen referentieller Transparenz extrem fehleranfällig. Nur in ADA hat man versucht, den Abstrakten Syntaxbaum einheitlich zu beschreiben [Go83,Ros85,Ze]. Zeigermanipulationen können, da sie meist nicht durch einen Abstrakten Datentyp geschützt sind, bei einem syntaktisch richtigen Programm zu einer undefinierten Operation führen. Selbst bei der Definition derartiger Programmiersprachen zog man es lieber vor, eine neue Sprache -mit referentieller Transparenz- eigens zur Beschreibung zu entwickeln (z.B. VDL für PL 1 [Oll71 und Referenzen], W-Grammatik für Algol68 [Wij75]), als die Sprache durch sich selbst zu beschreiben. Daraus kann man ersehen, wie groß die Kluft zwischen der ungenügenden Mächtigkeit der Sprachmittel und der ausufernden Fülle von semantischen Details ist. Die einzige verwendete Realisierung metasprachlicher Konzepte stellen symbolische Debugger dar. Hier möchte man auch in imperativen Programmiersprachen den Ablauf des Programms durch ein anderes Programm erklärt bekommen.

2.2.2.4 Maschinensprachen

Bei den Instruktionsformaten von Prozessoren findet der Abstrakte Syntaxbaum seine Entsprechung. Das Verhältnis zwischen Sprachmitteln und Semantik kann hier wieder als verhältnismäßig ausgeglichen bezeichnet werden. So benötigt Knuth zur Beschreibung seiner Maschinensprache MIX in MIX selbst 311 Assemblerzeilen [Knu73]. Knuth bezeichnet ein derartiges Programm als Simulator. Auch die Bezeichnung Emulator wird häufig verwendet. Emulatoren werden zur Implementierung des Verhaltens eines Prozessors auf einem anderen eingesetzt. Zur Messung gewisser Leistungsmerkmale (Speicherdurchsatz, Lokalität etc.) für einen konkreten geplanten Prozessor dienen ebenfalls derartige Programme.

2.3 Prologs Implementierungsmodell

Das im folgenden vorgestellte Implementierungsmodell wird als eine Erweiterung des in prozeduralen Sprachen üblichen Speichermodells motiviert. In fast allen Implementierungen von Prolog wird dieses Modell verwendet.

2.3.1 Deterministisches Prolog

Betrachtet man rein deterministische Programme, so ist der Kontrollfluß von Prolog vergleichbar mit dem einer Prozeduralen Sprache. Man benötigt aso einen Stack um die Aufrufshierarchie und lokale Werte zu verwalten. Die Variablen von Prolog haben aber keine Entsprechung in einer prozeduralen Sprache. Durch die Unifikation kann einer Variable nicht nur ein Wert zugewiesen werden; freie Variablen können auch aneinander gebunden werden. Solange keine zusammengesetzten Terme verwendet werden, kann man die Variablen aber lokal, in ihrem Environment am sog. Environmentstack darstellen. Da die Größe von zusammengesetzten Termen erst zur Laufzeit bestimmt werden kann, müssen sie in einem eigenen Bereich, dem Copystack (WAM-Terminologie: heap), dargestellt werden. Auch in Prozeduralen Sprachen, etwa Algol 68 [Bran71] oder CLU [LiGu86] werden Strukturen mit dynamischer Größe so dargestellt. Diese Strukturen, deren Lebenszeit mit der Gültigkeit des Prozedur- bzw. Environmentstacks nicht übereinstimmen muß, müssen von einer eigenen Speicherbereinigung reorganisiert werden.

2.3.2 Nichtdeterministisches Prolog

Nichtdeterministische Programme, zum ersten Mal von Floyd [Flo67] als Erweiterung von Algol 60 implementiert, müssen bei jedem Wahlpunkt (choicepoint) den Zustand der Maschine "einfrieren". Da Prolog die vor einem Wahlpunkt erzeugten Datenstrukturen nur mittels Unifikation verändern kann, und dabei nur Variablen gleichsetzen, oder durch Terme instanzieren kann, muß man, im Gegensatz zu Floyds Ansatz, der bei jeder Zuweisung den alten Wert einer Variable speichern muß, nur die gebundenen Variablen registrieren. In den Wahlpunkten müssen die Größen der Stacks und die offenen Alternativen, sowie der davor liegende Wahlpunkt gespeichert werden. Die Wahlpunkte werden gemeinsam mit den Environments auf dem Environmentstack verkettet gespeichert. Am Environmentstack befindet sich somit genau der Teil des Beweisbaumes, von dem aus der restliche, noch nicht besuchte Teil erreicht werden kann. Um das Wiedererfüllen eines Ziels (backtracking) effizient zu unterstützen, werden die Bindungen auf einem eigenen Stack, dem Trail, registriert.

2.4 Anforderungen an die Speicherverwaltung

Die Implementierung von VIP kann bereits alle für die Speicherverwaltung der Kontrollstrukturen erforderlichen Operationen implizit durch seine abstrakte Maschine (VAM [Kral87]) durchführen. Nur die Verwaltung des Copystacks muß mittels einer eigenen zusätzlich zum Interpreter und zur abstrakten Maschine existierenden Speicherbereinigung realisiert werden. Es werden nun einige Anforderung an diese Speicherverwaltung formuliert werden, um die in Kapitel 3 vorgestellten Verfahren auf ihre Eignung zu überprüfen.

2.4.1 Zeitliches Speichermodell

In allen Systemen, in denen Objekte verwaltet werden müssen, kann man unabhängig von einer konkreten Implementierung der Speicherbereinigung eine Klassifizierung der Objekte nach ihrer Lebensdauer, also der Zeit wie lange das Objekt benötigt wird, vornehmen. Ungar [Un88] hat hier im Umfeld von Smalltalk eine Klassifizierung vorgenommen, die auch für Prolog anwendbar ist. Man betrachtet hier die Lebenszeit von Objekten bezogen auf zwei beliebige Zeitpunkte während der Laufzeit des Systems.

In einer konkreten Realisierung wird zu den beiden Zeitpunkte GC1 und GC2 eine Speicherbereinigung die nicht mehr benötigten Objekte entfernen. Eine charakteristische Eigenschaft der Verfahren ist ihre Abhängigkeit von diesen vier Klassen. Es stellt sich bei den meisten Systemen heraus, daß man vor allem mit einer großen Menge von flüchtigen (transient) und permanenten Datenstrukturen zu rechnen hat. Je älter ein Objekt ist, umso eher wird es auch weiterexistieren. Je nach Wahl der Größe des Intervalls kann man die neu hinzukommenden Objekte (arrival objects) und die nicht mehr referenzierten Objekte zu einer dieser Klassen zuordnen.

2.4.2 Flüchtige Datenstrukturen

Gelegentlich kann die Lebensdauer von flüchtigen Datenstrukturen bereits aus der Problemstellung bzw. dem Programm erkannt werden. In einigen Fällen wird man daher versuchen, flüchtige Datenstrukturen direkt durch einen Stack darzustellen. Dennoch kann man bei genügend großem Speicher zeigen, daß auch die Realisierung eines Stacks unter der Annahme, daß für jede push- und pop-Operation eine konstante Zeitspanne benötigt wird, mittels flüchtiger Datenstrukturen und einer von der Anzahl der flüchtigen Datenstrukturen unabhängigen Speicherbereinigung eine effizientere Implementierung ergibt.

2.4.2.1 Implementierungskosten

Eine Speicherbereinigung mittels eines Kopieralgorithmus (siehe Kapitel 3.3.6) kostet abhängig von der Anzahl A der erreichbaren Zellen (c +ds)A. Sie ist also unabhängig von der Anzahl der nichtreferenzierten Zellen bzw. von der Größe des gesamten Speichers M. Unter der Annahme, daß die Anzahl der erreichbaren Zellen zum Zeitpunkt jeder GC gleich bleibt, können die Kosten pro Zelle, die für sie benötigte Zeit pro GC berechnet werden. Falls die Speicherbereinigung erst eingesetzt wird, wenn bereits der gesamte Speicher aufgebraucht ist, so ist die Anzahl der zwischen zwei Speicherbereinigungen allokierten Zellen gleich G:

G = M/s - A

Die Kosten pro Zelle g daher:

g = ( (c +ds)A ) / (M/s - A)

Zum Anlegen einer Zelle benötigen beide Implementierungen eines Stacks gleich viel Zeit. Die Freigabe eines Elements auf einem traditionell implementierten Stack benötigt die Zeit f. Bei einer Speicherverwaltung mit Speicherbereinigung ist keinerlei Mehraufwand erforderlich. Die beiden Versionen werden also unter folgenden Bedingungen den gleichen Aufwand pro angelegter Zelle benötigen:

g = ( (c +ds)A ) / (M/s - A) , f = g, f = (c +ds) / (M/sA - 1) = (c + ds)/f + 1

Falls also M/sA größer als (c + ds)/f +1 gemacht werden kann, wird eine Speicherbereinigung effizienter als eine Stackverwaltung sein. Bei den Annahmen von [Appel 87] muß der Speicher 8 mal größer als die Anzahl der verwendeten Elemente sein. Bei einer Verdopplung der momentanen Speichergröße, werden sich die Kosten bei M >> A also halbieren!

G	.............	Anzahl der Zellen, die zwischen zwei GC angelegt werden.
A	.............	Anzahl der erreichbaren Zellen
M	.............	Größe des vorhandenen Speichers
c	.............	Aufwand pro Zelle
d	.............	Aufwand pro Zeiger
s	.............	Zellgröße
g	.............	Kosten pro Zelle
f	.............	Kosten für eine pop-Operation

Andrew Appel [Appel 87] begründet so, daß es zur Unterstützung von applikativen Sprachen wesentlich interessanter ist, mehr Speicher zur Verfügung zu haben, als durch spezielle Hardware oder aufwendige Analysen des Compilers den Prozessor beim Anlegen von Speicherelementen zu unterstützen. Die einzige Voraussetzung ist, daß die Speicherbereinigung unabhängig von der Anzahl der flüchtigen Datenstrukturen ist. Natürlich können durch eine derartige Speicherverwaltung möglicherweise sehr lange Pausen entstehen. Adaptive Verfahren, bei denen ähnlich Annahmen für ihren Aufwand gelten, werden in Kapitel 3.3.6.3 vorgestellt. Der Vergleich ist natürlich nur dann zulässig, wenn pro pop-Operation tatsächlich zumindest eine Zeit von f > 0 benötigt wird.

Bei der Speicherverwaltung von Prolog ist es allerdings noch immer interessant, das in Kapitel 2.3 vorgestellte Implementierungsmodell zu verwenden. Durch die zeitliche Abhängigkeit der Datenstrukturen in Prolog kann man bei backtracking größere Teile des Stacks sofort freigeben. Man kann also pro pop-Operation mit einem wesentlich geringeren Aufwand rechnen, der nicht mehr proportional zur Anzahl der freigegebenen Elemente ist. Deswegen kann daher durch einen größeren Speicher wohl nie diese Effizienz erreicht werden. Dennoch experimentiert man auch in Prolog mit derartigen Konfigurationen [Bek84, Bek86]. Für die Verwaltung der Speicherzellen am Copystack gilt allerdings allgemein, daß ihre Kosten durch eine Vergrößerung des Speichers reduziert werden können.

Flüchtige Datenstrukturen werden gerade auf neueren Systemen, die mit Daten-caches ausgestattet sind, sehr effizient unterstützt: Bei ihrer Erzeugung werden sie, da dadurch auf Speicherzellen geschrieben, aber nicht gelesen wird, im cache angelegt. Es erfolgt keinerlei Zugriff auf den eigentlichen Hauptspeicher. Während der kurz auf ihre Erzeugung folgenden Verwendung werden sie sich noch immer im cache befinden. Es ist daher nicht erforderlich, sie aus dem Hauptspeicher einzulesen. Erst lange nach ihrer Verwendung werden die flüchtigen Datenstrukturen in den Hauptspeicher hinausgeschrieben werden, da der Prozessor nunmehr auf anderen Speicherzellen arbeitet (cache flush). Bei größerem cache (ab ca. 64K) kann man sogar erwarten, daß die Speicherbereinigung, falls sie sich nur auf einen kleineren Bereich des Arbeitsspeichers bezieht, die "toten" Strukturen noch immer im cache antrifft. Der eigentliche Mehraufwand liegt so nur mehr in der Speicherbereinigung, die dafür verantwortlich ist, daß sich die Maschine bezüglich des paging des virtuellen Speichers noch immer lokal verhält.

2.4.2.2 Beispiele

2.4.2.2.1 Metainterpreter

Es wird nun anhand des einfachsten Metainterpreters gezeigt, wie hier flüchtige Datenstrukturen, die von der Speicherbereinigung verwaltet werden müssen, anfallen. Dieser Metainterpreter kann nur eine Untermenge von Prolog verarbeiten, vordefinierte Prädikate müßten gesondert behandelt werden.
solve(true).
solve((X,Y)) :-
	solve(X),
	solve(Y).
solve(X) :-
	clause(X,Y),
	solve(Y).
Damit dieser Interpreter zumindest auch sich selbst interpretieren kann, müßte clause/2 sich wie folgt verhalten:
clause(H,B) :-
	H \= clause(_,_),
	real_clause(H,B).
clause(clause(H,B),B) :-
	real_clause(H,B).

p(X,X).
p(X,Z) :-
	q(X,Y),
	p(Y,Z).
q(a,b).
Während des Beweisens von :- p(X,b). durchläuft der Prologinterpreter den entsprechenden SLD-Baum:

Beweis mittels Prolog-Resolution und die durch den Metainterpreter angelegten Strukturen zum Zeitpunkt der zweiten gefundenen Lösung

Der Metainterpreter wird nun bei :- solve(p(X,b)). all jene Ziele als Datenstrukturen durch clause/2 erzeugen, die von der Anfrage aus zu dem aktuellen Knoten führen. Dabei werden nur bei Regeln Strukturen erzeugt. Eine direkte Ausführung des Programms hätte nur einen einzigen Wahlpunkt mit den dazugehörigen Variablen erzeugt. Die neu angelegten Strukturen ,/2 und q/2 werden für die nachfolgenden Berechnungen nicht mehr benötigt. Verglichen mit der direkten Ausführung wird sich daher der Platzbedarf bei folgenden Elementen unterscheiden:

Die Kontrollstruktur ,/2 wird im Prologinterpreter durch structure sharing der Darstellung der Klauseln realisiert, d.h. daß die gleichbleibenden Teile einer Klausel nur einmal in der Datenbasis dargestellt werden. Die sich ändernden Teile, also die Variablen werden gesondert abgespeichert. Der Metainterpreter muß diese Struktur aber bei jedem neuen Ziel erzeugen. Die aktuellen Ziele werden durch den Prologinterpreter mit Hilfe des Environmentstacks realisiert. Der Metainterpreter muß auch hier jedes einzelne anfallende Ziel als Term im Copystack besitzen. Das Stackframe einer deterministischen Klausel kann bei Aufruf des letzten Ziels durch lastcall-Optimierung entfernt werden. Der Metainterpreter hat hier direkt keine Entsprechung. Allerdings können sämtliche angefallenen Kontrollstrukturen, insofern sie nicht mehr wegen eines Wahlpunktes benötigt werden, durch die Speicherbereinigung entfernt werden. Dabei kann der Metainterpreter sogar mehr Elemente entfernen als dies bei einem Prologinterpreter der Fall ist: Der Prologinterpreter muß jedes Environment als eine Einheit behandeln. Wenn also bereits gewisse Variablen darin nicht mehr benötigt werden, könnte der Prologinterpreter sie höchstens freigeben, falls diese am "Rand" des Environments vorkommen. Bei der WAM (siehe unten) wird dieser Vorgang stack trimming genannt. Im Gegensatz dazu muß der Metainterpreter nur die einzelnen Ziele als Strukturen verwalten; die Variablen werden dadurch implizit mitverwaltet. Der Metainterpreter könnte im besten Fall in Bezug auf die Größe des Environmentstacks um einen von der Größe des Programms abhängigen Faktor weniger Platz als das direkt exekutierte Programm verbrauchen.

In diesem Beispiel wurde gezeigt, wie die Speicherbereinigung für einen Metainterpreter den Großteil der typischen Prolog-Optimierungen nachvollzieht.

2.4.2.2.2 Matrizenmultiplikation

Flüchtige Datenstrukturen treten nicht nur in der Rolle von Kontrollstrukturen, sondern auch als Zwischenergebnisse einer Berechnung, auf. Da wir uns auf ein kleines Beispiel beschränken wollen, das genau jenen Fall aufzeigt, wo flüchtige Datenstrukturen auftreten, wurde ein eher funktionales Programm gewählt. Bei nichtdeterministischen Programmen ist es ja wesentlich schwieriger, flüchtige Datenstrukturen, die nach erfolgter Berechnung noch am Copystack verbleiben, zu erzeugen, da ja bei Wiedererfüllung eines Ziels (backtracking) ohnehin die Zwischenergebnisse durch die Anordnung am Stack wegfallen.

Der übliche Ansatz, Programme zu entwickeln, zerlegt ein Problem in Teilprobleme, die sich zum Teil rekursiv formulieren lassen. Im Unterschied zu Prozeduralen Sprachen kann aber in applikativen Sprachen das Teilproblem vollkommen unabhängig von der eigentlichen Aufgabe verstanden werden. Zwischenergebnisse oder Parameter können, falls sie bereits bekannt sind, nicht mehr verändert werden. Falls Zwischenergebnisse noch nicht Teil des Endergebnisses sind, werden sie nach erfolgter Berechnung nicht mehr benötigt. Hier fallen dann flüchtige Datenstrukturen, die durch eine Speicherbereinigung erkannt werden müssen, an.

Es soll etwa die Multiplikation von zweidimensionalen Matrizen implementiert werden. Matrizen seien durch Listen von Listen dargestellt, die zeilenweise die Matrix darstellen. Die Anfrage :- matrix_multiply([[3,5],[2,9]],[[6,2],[7,1]],M). sollte M = [[53,11],[75,13]] liefern. Die Zeilen der ersten Matrix sollen mit den Spalten der zweiten für alle Kombinationen errechnet werden. Da es aber nicht direkt möglich ist, die zweite Matrix spaltenweise zu bearbeiten, müßte man ein komplexeres Verfahren entwickeln. Innerhalb der Implementierung einer Bibliothek von Vektor- und Matritzenoperationen seien nun bereits mehrere Prädikate implementiert worden, etwa transpose/2 zur Transponierung einer Matrix...

transpose(M,[]) :-
	nullrows(M).
transpose(M1,[Row|M2]) :-
	makerow(M1,Row,M3),
	transpose(M3,M2).

makerow([],[],[]).
makerow([[X|R1]|M1],[X|Row],[R1|M2]) :-
	makerow(M1,Row,M2).

nullrows([]).
nullrows([[]|M]) :-
	nullrows(M).

:- transpose(L,[[6,2],[7,1]]).
    L = [[6,7],[2,1]].

... sowie das Skalarprodukt zweier Vektoren:

dot_prod([],[],0).
dot_prod([X|Xs],[Y|Ys],S) :-
	dot_prod(Xs,Ys,T),
	S is T + (X * Y).
Mittels dieser beiden Prädikate kann bei der Multiplikation von Matrizen eine transponierte Matrix verwendet werden, um Zeilen und Spalten mit dem Skalarprodukt zu kombinieren.
matrix_multiply(MA,MB,MR) :-
	transpose(MB,TB),
	mul_rows_cols(MA,TB,MR).

mul_rows_cols([],_,[]).
mul_rows_cols([X|M1],T,[Y|M2]) :-
	mul_onerow_cols(T,X,Y),
	mul_rows_cols(M1,T,M2).

mul_onerow_cols([],_,[]).
mul_onerow_cols([Col|Cols],Row,[El|Els]) :-
	dot_product(Col,Row,El),
	mul_onerow_cols(Cols,Row,Els).
Die transponierte Matrix L = [[6,7],[2,1]] bildet das flüchtige Zwischenergebnis erzeugt durch das Unterziel :-transpose([[6,2],[7,1]],L). Dieses Zwischenergebnis stellt einen typischen Fall für eine flüchtige Datenstruktur dar: Direkt, nachdem sie erzeugt worden ist, wird sie für einige wenige Operationen verwendet. Nach mul_rows_cols/3 wird diese Datenstruktur nicht mehr benötigt. Natürlich könnte das Erzeugen des Zwischenergebnisses in diesem Beispiel durch entsprechende Programmtransformation umgangen werden; bei diesem Beispiel wäre etwa fusion [Fea87] angebracht.

2.4.2.2.3 Sortieren

Bei der Entwicklung größerer Programme kann man aber nur sehr lokal die Techniken der Programmtransformation anwenden -allein schon wegen der komplexen Analysen. Falls die vorhandenen Datenstrukturen den Zugriff auf die gewünschten Elemente nicht nur, wie in diesem Beispiel, komplizieren, sondern auch wesentlich ineffizienter machen, etwa weil die Elemente bei jedem Zugriff in einem Baum gesucht werden müssen, ist es naheliegend, Zwischenergebnisse zu verwenden. Als Beispiel sei hier das Sortieren einer Liste genannt. Auch in Prolog verwendet man häufig das zu quicksort analoge Programm.
quicksort([],L,L).
quicksort([Key|List],Tail,Sort) :-
	partition(Key,List,L1,L2),
	quicksort(L2,Tail,Between),
	quicksort(L1,[Key|Between],Sort).

partition(Key,[],[],[]).
partition(Key,[E|L],[E|L1],L2):-
	E =< Key,
	!,
	partition(Key,L,L1,L2).
partition(Key,[E|L],L1,[E|L2]) :-
	partition(Key,L,L1,L2).
Man hat hier nicht nur mit einem Aufwand n log n mit n der Anzahl der Elemente der Liste zu rechnen, sondern auch mit zirka n ld n Listenelementen als flüchtige Zwischenergebnisse; im worst case mir n2. Natürlich kann man auch in Prolog ohne unnötige Zwischenergebnisse eine Liste mittels dem bekannten Prädikat slowsort/2 sortieren:
slowsort(L,K) :-
	permutation(L,K),
	ordered(K).

Hier ist es wohl offensichtlich, daß flüchtige Datenstrukturen als Zwischenergebnisse, die schlimmstenfalls einen Mehraufwand von n2 für die nachfolgende Speicherbereinigung bedeuten, sehr gerne in Kauf genommen werden.

2.4.2.3 Unterstützung des Programmierstils

Speicher für kurzlebige Datenstrukturen unterstützen applikative Programmiersprachen. Flüchtige Datenstrukturen übernehmen häufig Aufgaben von Programmen in herkömmlichen Programmiersprachen: Sie "steuern" den eigentlichen Programmablauf. In der einfachsten Variante, beim Arbeiten mit Listen, übernehmen sie oft die Aufgabe von Iteratoren. Anstatt z.B. eine for Schleife zu programmieren, wird eine Operation auf alle Elemente einer Liste angewendet. Für den Programmierer entfallen dadurch die Definition und Verwaltung der Schleifenvariablen. Die Programme werden so von redundanten Kontrollinformationen befreit, die Information des eigentlichen Ablaufes bleibt lokal in den Datenstrukturen. Die (syntaktisch) schönsten Beispiele dafür finden sich in funktionalen Programmiersprachen, wie etwa Hope. Aber auch in Prolog wird diese Technik, verallgemeinert auf beliebige u.U. unendliche Bäume [Colm82], verwendet.

Durch die verbesserten Möglichkeiten neuerer imperativer Programmiersprachen bei der Unterstützung von Datenabstraktion können etwa Iterationen auch dort einfacher realisiert werden. Grenzen werden aber durch die strenge zeitliche Folge der Instruktionen gesetzt. CLU stellt hier die konsequenteste Realisierung dar, aber auch in ADA können derartige Iterationen klar dargestellt werden.

2.4.3 Permanente Datenstrukturen

Permanente Datenstrukturen treten primär bei sehr langlebigen Systemen etwa Datenbanksystemen auf. Objekte, die die Zeitdauer einer Applikation überleben, werden persistent genannt. Für sie sollte möglichst eine andere Speicherbereinigung als für die flüchtigen Datenstrukturen realisiert werden. Bezogen auf die Copystackverwaltung in Prolog werden alle vor einem Wahlpunkt angelegten Datenstrukturen zumindest bis zur Wiedererfüllung (backtracking) dieser Alternative benötigt. Eine Speicherbereinigung für den Copystack sollte daher auf diese Eigenschaft Rücksicht nehmen.

2.4.4 Redundante Darstellung

In Kapitel 2.3 wurde das übliche Implementierungsmodell für Prolog vorgestellt, dabei wurde angenommen, daß jeder Term durch eine konkrete Datenstruktur realisiert werden muß. In den ersten Implementierungen hat man daher versucht durch structure sharing die unveränderbaren Teile, die bereits in den Klauseln erkannt werden können, nur genau einmal darzustellen. Auch in einer Implementierung durch structure copying kann durch eine Komprimierung der redundanten Darstellungen der Copystack beträchtlich reduziert werden. Voraussetzung für diese Erweiterung einer konventionellen Speicherbereinigung ist die referentielle Transparenz der Terme. In Prolog hat man keine Möglichkeit zwei Terme, die identisch sind, für die also das Ziel :- X == Y. erfüllt ist, voneinander zu unterscheiden, obwohl sie eine unterschiedliche Repräsentation besitzen können. Es gibt auch kein Prologsystem, das ein vordefiniertes Prädikat zur Feststellung der tatsächlichen Repräsentation ermöglicht. Prologterme können daher unabhängig von ihrer zeitlichen Entwicklung repräsentiert werden. Man kann etwa nach der erfolgreichen Unifikation von zwei Termen mit einer unterschiedlichen Repräsentation nur eine der beiden Repräsentationen weiterverwenden.