œbersetzerbau: Band 3: Analyse und Transformation

186

Transcript of œbersetzerbau: Band 3: Analyse und Transformation

Page 1: œbersetzerbau: Band 3: Analyse und Transformation
Page 2: œbersetzerbau: Band 3: Analyse und Transformation

eXamen.press

Page 3: œbersetzerbau: Band 3: Analyse und Transformation

eXamen.press ist eine Reihe, die Theorie undPraxis aus allen Bereichen der Informatik fürdie Hochschulausbildung vermittelt.

Page 4: œbersetzerbau: Band 3: Analyse und Transformation

123

Helmut Seidl Reinhard Wilhelm

Übersetzerbau

·Sebastian Hack

·

Band 3: Analyse und Transformation

Page 5: œbersetzerbau: Band 3: Analyse und Transformation

Helmut SeidlTechnische Universität MünchenInstitut für Informatik – I2Boltzmannstr. 385748 [email protected]

Sebastian HackUniversität des SaarlandesFB Informatik66041 Saarbrü[email protected]

Reinhard WilhelmUniversität des SaarlandesFB Informatik66041 Saarbrü[email protected]

Das vorliegende Buch ist als Neuauflage aus dem Buch Wilhelm, R.; Maurer, D. Übersetzerbau:Theorie, Konstruktion, Generierung hervorgegangen, das in der 1. Auflage (ISBN 3-540-55704-0) undder 2. Auflage (ISBN 3-540-61692-6) im Springer-Verlag erschien.

ISSN 1614-5216ISBN 978-3-642-03329-2 e-ISBN 978-3-642-03331-5DOI 10.1007/978-3-642-03331-5Springer Heidelberg Dordrecht London New York

Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie;detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.

c© Springer-Verlag Berlin Heidelberg 2010Dieses Werk ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, insbesondere die derÜbersetzung, des Nachdrucks, des Vortrags, der Entnahme von Abbildungen und Tabellen, derFunksendung, der Mikroverfilmung oder der Vervielfältigung auf anderen Wegen und der Speicherungin Datenverarbeitungsanlagen, bleiben, auch bei nur auszugsweiser Verwertung, vorbehalten. EineVervielfältigung dieses Werkes oder von Teilen dieses Werkes ist auch im Einzelfall nur in den Grenzender gesetzlichen Bestimmungen des Urheberrechtsgesetzes der Bundesrepublik Deutschland vom 9.September 1965 in der jeweils geltenden Fassung zulässig. Sie ist grundsätzlich vergütungspflichtig.Zuwiderhandlungen unterliegen den Strafbestimmungen des Urheberrechtsgesetzes.Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werkberechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinneder Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher vonjedermann benutzt werden dürften.

Satz: Druckfertige Daten der AutorenUmschlaggestaltung: KünkelLopka Werbeagentur, Heidelberg

Gedruckt auf säurefreiem Papier

Springer ist Teil der Fachverlagsgruppe Springer Science+Business Media (www.springer.com)

Page 6: œbersetzerbau: Band 3: Analyse und Transformation

Vorwort

Übersetzer für Programmiersprachen müssen nicht nur Programme der Quellspra-che korrekt in Programme der Zielsprache, meist einer Maschinensprache, überset-zen. Darüber hinaus sollen sie häufig auch noch möglichst guten Code erzeugen.Als eine Entwicklermannschaft der IBM unter der Leitung von John W. Backus inden frühen 50er Jahren den ersten Übersetzer für die Programmiersprache FORT-RAN entwarf und realisierte, war der Zielrechner nach heutigen Maßstäben extremklein und extrem langsam. Deshalb ist es kein Wunder, dass die Idee einer optimie-renden Übersetzung aufkam. Diese sollte die bescheidenen Maschinenressourcen sogeschickt wie möglich ausnutzen.

Als imperative Programmiersprache war FORTRAN vor allem für numerischeBerechnungen gedacht. Für diesen Zweck bietet FORTRAN als wichtigste Sprach-konstrukte Felder zur Speicherung von Vektoren und Matrizen an und Schleifen,um Algorithmen darauf zu formulieren. Felder und Schleifen bieten einen großenSpielraum für Programmtransformationen zur Verbesserung der Effizienz. In FORT-RAN sind Felder strukturell recht nahe an den mathematischen Objekten, die manin ihnen speichert. Elemente eines multidimensionalen Felds werden durch mehrfa-che Indizierung mit ganzzahligen Ausdrücken ausgewählt, was zu relativ komplexenAdressberechnungen führt. Einfache numerische Algorithmen verwenden anderer-seits häufig identische Indexausdrücke an unterschiedlichen Stellen des Programms,wofür eine naive Codeerzeugung immer die gleichen Berechnungsfolgen erzeugenwürde. Ebenfalls sehr verbreitet sind Schleifen, bei deren Durchlauf die Indizierungmit konstanter Schrittweite weiter geschaltet wird. Solche Beobachtungen gaben denÜbersetzerbauern Hinweise, wo Optimierungen ansetzen könnten. Sehr bald wurdenTransformationen zur Steigerung der Ausführungseffizienz vorgeschlagen. Unvor-sichtig angewendet, verändern diese jedoch die Semantik des Programms. Deshalbmussten die genauen Voraussetzungen geklärt werden, unter denen die Transforma-tionen überhaupt anwendbar sind. In der Regel hängt die Anwendbarkeit von globa-len Eigenschaften des Programms ab, welche durch eine statische Analyse im Über-setzer ermittelt werden müssen.

Dies war die Geburtsstunde der Datenflussanalyse. Der Name kommt wohl da-her, dass diese Analysen den Fluss von Eigenschaften der Variablenwerte von Pro-

Page 7: œbersetzerbau: Band 3: Analyse und Transformation

grammpunkt zu Programmpunkt untersuchten. Die Theorie zur statischen Analysevon Programmen konnte erst in den 70er Jahren entwickelt werden, als die Seman-tik von Programmiersprachen auf eine solide mathematische Grundlage gestellt war.Den größten Einfluss hatten die beiden Dissertationen von Gary A. Killdall (1972)und von Patrick Cousot (1978). Gary Kildall klärte die verbandstheoretischen Grund-lagen der Datenflussanalyse. Patrick Cousot stellte die entscheidende Beziehung zurSemantik der Programmiersprache her und nannte deshalb die statische Analyse ab-strakte Interpretation. Sein Ansatz ermöglichte es, die Korrektheit statischer Analy-sen zu beweisen und sogar Analysen zu entwerfen, die schon auf Grund ihrer Kon-struktion korrekt sind.

Die Ursprünge von Datenflussanalyse wie von abstrakter Interpretation liegen al-so im Übersetzerbau. Allerdings hat sich die statische Programmanalyse längst vonihrer ersten Anwendung bei der Codeerzeugung emanzipiert und ist zu einer wich-tigen Verifikationsmethode geworden. Heute überprüfen statische Analysen Sicher-heitseigenschaften von Programmen, wie etwa die Abwesenheit von Laufzeitfehlern,oder weisen die partielle Korrektheit von Programmen nach. Sie berechnen Laufzeit-schranken für eingebettete Echtzeitsysteme oder ermitteln Synchronitätseigenschaf-ten nebenläufiger Programme und werden so mehr und mehr zu einem unverzichtba-ren Hilfsmittel bei der Entwicklung zuverlässiger Software.

Dieses Buch behandelt die Phase der Übersetzung, in der die Effizienz des Pro-gramms durch semantikerhaltende Transformationen gesteigert wird. Es stellt dienotwendigen Techniken der statischen Analyse vor. Neben den Analysen werdenauch die Transformationen auf präzise Weise beschrieben. Dazu wird eine kleineKernsprache mit einer einfachen operationellen Semantik eingeführt, auf die sichdie vorgestellten Analysen und Transformationen beziehen.

In dem Band Wilhelm/Seidl: Übersetzerbau – Virtuelle Maschinen wurde der An-spruch realisiert, mehrere Programmierparadigmen zu behandeln. In diesem Bandwerden deshalb neben Analysen und optimierenden Transformationen von impera-tiven Programmen auch solche von funktionalen Programmen beschrieben. Funk-tionale Sprachen basieren semantisch auf dem λ-Kalkül und weisen eine weit ent-wickelte Theorie der Programmtransformationen auf.

Wir wünschen unseren Lesern eine ertragreiche Lektüre.

München und Saarbrücken, im August 2009.

Helmut Seidl, Reinhard Wilhelm und Sebastian Hack

VI Vorwort

Page 8: œbersetzerbau: Band 3: Analyse und Transformation

Allgemeine Literaturhinweise

Die Liste der Monographien, die einen Überblick über Techniken zu statischer Pro-grammanalyse und abstrakter Interpretation geben, ist erstaunlich kurz. Das Buchvon Matthew S. Hecht [Hec77], das die klassischen Ergebnisse zur Datenflussana-lyse zusammenfasst, ist immer noch lesenswert. Der Sammelband von Steven S.Muchnick und Neil D. Jones wenige Jahre später enthält viele originale und ein-flussreiche Beiträge zur Analyse rekursiver Prozeduren und dynamischer Daten-strukturen [MJ81]. Einen ähnlichen Sammelband speziell für deklarative Sprachenhaben Samson Abramsky und Chris Hankin herausgegeben [AH87]. Eine umfassen-de, moderne Darstellung bieten Flemming Nielson, Hanne Riis Nielson und ChrisHankin [NNH99].

Eine Reihe umfassenderer Dastellungen des Übersetzerbaus enthalten ausführli-che Kapitel über Datenflussanalyse [AG04, CT04, ALSU07]. Sehr ausführlich wirddieses Thema auch in Steven S. Muchnick’s Monographie “Advanced Compiler De-sign and Implementation” [Muc97] behandelt. Das Handbuch zum Übersetzerbau,herausgegeben von Y.N. Srikant und Priti Shankar [SS03], behandelt ausführlichCodeerzeugungstechniken für verschiedene Architekturen, bietet aber auch Kapitelüber Datenflussanalyse, Shape-Analyse und spezielle Techniken für objektorientierteProgrammiersprachen.

Die Entwicklung beweisbar korrekter Übersetzer [Ler09, TL09] hat in den letz-ten Jahren auch zu verstärktem Interesse an Korrektheitsbeweisen für Programmop-timierungen geführt. Techniken zur systematischen Ableitung korrekter Programm-transformationen stellen Patrick und Radia Cousot [CC02] vor. Automatisches Be-weisen der Korrektheit optimierender Transformationen behandeln Sorin Lerner[LMC03, LMRC05, KTL09].

VIIVorwort

Page 9: œbersetzerbau: Band 3: Analyse und Transformation

Inhaltsverzeichnis

1 Grundlagen und intraprozedurale Optimierung . . . . . . . . . . . . . . . . . . . 11.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11.2 Vermeidung überflüssiger Berechnungen . . . . . . . . . . . . . . . . . . . . . . . 71.3 Exkurs: Eine operationelle Semantik . . . . . . . . . . . . . . . . . . . . . . . . . . 81.4 Beseitigung von Mehrfachberechnungen . . . . . . . . . . . . . . . . . . . . . . . 111.5 Exkurs: Vollständige Verbände . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161.6 Kleinste Lösung oder MOP–Lösung? . . . . . . . . . . . . . . . . . . . . . . . . . . 271.7 Beseitigung von Zuweisungen an tote Variablen . . . . . . . . . . . . . . . . . 321.8 Beseitigung von Zuweisungen zwischen Variablen . . . . . . . . . . . . . . . 401.9 Konstantenfaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431.10 Intervallanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 541.11 Aliasanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 681.12 Fixpunktalgorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 831.13 Beseitigung teilweiser Redundanzen . . . . . . . . . . . . . . . . . . . . . . . . . . . 901.14 Anwendung: Schleifeninvarianter Code . . . . . . . . . . . . . . . . . . . . . . . . 971.15 Beseitigung teilweise toter Zuweisungen . . . . . . . . . . . . . . . . . . . . . . . 1021.16 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1091.17 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113

2 Interprozedurale Optimierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1152.1 Inlining . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1202.2 Beseitigung letzter Aufrufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1222.3 Interprozedurale Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1242.4 Der funktionale Ansatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1252.5 Interprozedurale Erreichbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1302.6 Bedarfsgetriebene interprozedurale Analyse . . . . . . . . . . . . . . . . . . . . 1312.7 Der Call-String-Ansatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1342.8 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1362.9 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

Page 10: œbersetzerbau: Band 3: Analyse und Transformation

X Inhaltsverzeichnis

3 Optimierung funktionaler Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . 1393.1 Eine einfache funktionale Programmiersprache . . . . . . . . . . . . . . . . . . 1403.2 Einige einfache Optimierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1413.3 Inlining . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1443.4 Spezialisierung rekursiver Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . 1463.5 Eine verbesserte Wertanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1483.6 Beseitigung von Zwischendatenstrukturen . . . . . . . . . . . . . . . . . . . . . . 1533.7 Verbesserung der Auswertungsreihenfolge: Die Striktheitsanalyse . . 1573.8 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1653.9 Literaturhinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168

Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171

Stichwortverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175

Page 11: œbersetzerbau: Band 3: Analyse und Transformation

1

Grundlagen und intraprozedurale Optimierung

1.1 Einführung

In diesem Abschnitt wollen wir einige grundlegende Techniken kennen lernen, mitdenen die Qualität des Codes, den der Übersetzer erzeugt, verbessert werden kann.Das Qualitätsmaß ist hierbei nicht a priori festgelegt. In diesem Buch werden wirvor allem daran interessiert sein, die Ausführungszeit des Programms zu verbessern.Andere Optimierungsziele könnten die Verringerung des benötigten Speicherplatzes,die Reduzierung des Stromverbrauchs oder auch die Verringerung der Lesbarkeit desProgramms („Obfuskierung“) sein.

Eine Strategie, um ein Programm effizienter zu machen, ist, überflüssige Berech-nungen zu vermeiden. Würde die Berechnung eines Ausdrucks mit garantiert glei-chem Ergebnis wiederholt, so kann der Übersetzer diese Wiederholung vermeiden,indem er dafür sorgt, dass das Ergebnis nach der ersten Berechnung abgespeichertwird. Dies ermöglicht es, eine (eventuell) teure Neuberechnung durch ein Nachschla-gen des Werts zu ersetzen.

Laufzeit kann ebenfalls eingespart werden, falls Teilberechnungen bereits zurÜbersetzungszeit ausgeführt werden können. Die Konstantenfaltung versucht, Aus-drücke, deren Werte bereits zur Übersetzungszeit bekannt sind, durch diese Werte zuersetzen. Diese Optimierung unterstützt einen Programmierstil, der mehrmals ver-wendete Programmkonstanten in Variablen mit sprechenden Namen ablegt, um dannalle Vorkommen der Konstanten durch den erhellenderen Variablennamen zu erset-zen. Die Konstantenfaltung vermeidet einen eventuell mit diesem Programmierstilverbundenen Laufzeitnachteil.

Auch Bereichseinschränkungen für die Werte von Variablen können von Nutzensein. Lässt sich zum Beispiel nachweisen, dass der Indexausdruck, mit dem auf einFeld zugegriffen wird, stets einen Wert hat, der innerhalb der Grenzen des Felds liegt,kann eine Überprüfung zur Laufzeit des Programms eingespart werden.

Eine weitere Idee besteht darin, Berechnungen aus einem Bereich, der sehr oftausgeführt wird, in einen Bereich zu verschieben, der seltener ausgeführt wird. Sowird der Übersetzer versuchen, eine Berechnung mit immer gleichem Wert aus ei-ner Schleife heraus zu ziehen. Schließlich kann der Übersetzer versuchen, teure Be-

H. Seidl et al., Übersetzerbau, eXamen.press, DOI 10.1007/978-3-642-03331-5_1,c© Springer-Verlag Berlin Heidelberg 2010

Page 12: œbersetzerbau: Band 3: Analyse und Transformation

2 1 Grundlagen und intraprozedurale Optimierung

rechnungen durch äquivalente billigere zu ersetzen, z.B. eine Multiplikation in einerSchleife durch eine wiederholt ausgeführte Addition. Die Ersetzung von Funktions-aufrufen durch das Einkopieren des Rumpfs an die Aufrufstelle („Inlining“) ergibthäufig neue Möglichkeiten zur Anwendung von optimierenden Transformationen.

Wie wichtig bereits bei sehr einfachen Programmen Optimierungen sind, um ei-nigermaßen guten Code zu erzeugen, zeigt das folgende Beispiel.

Beispiel 1.1.1 Betrachten wir in einer imperativen Programmiersprache ein Pro-gamm, das ein Feld a sortieren soll. In diesem Program könnte es etwa die folgendeFunktion swap geben:

void swap ( int i, int j) {int t;if (a[i] > a[ j]) {

t ← a[ j];a[ j] ← a[i];a[i] ← t;

}}

Die Ineffizienzen dieser Implementierung liegen auf der Hand. Zuerst einmal müs-sen die Adressen a[i], a[ j] je dreimal berechnet werden. Das ergibt insgesamt sechsAdressberechnungen, wo bereits zwei genügen sollten. Dann werden die Wertea[i], a[ j] jeweils zweimal geladen. Das ergibt vier Speicherzugriffe, wo zwei aus-reichen sollten.

Diese Ineffizienzen können beseitigt werden, wenn wir eine Implementierungwählen, wie sie in der Programmiersprache C naheliegen würde. Hier ist die Idee,mithilfe von Zeigern auf die Elemente des Felds zuzugreifen und die mehrmals ver-wendeten Werte zwischenzuspeichern.

void swap (int ∗ p, int ∗ q) {int t, ai, a j;ai ← ∗p; a j ← ∗q;if (ai > a j) {

t ← a j;∗q ← ai;∗p ← t;

}}

Eine genauere Betrachtung dieser Funktion zeigt, dass in dieser Formulierung sogardie Hilfsvariable t eingespart werden kann.

Die zweite Formulierung ist offenbar effizienter. Die ursprüngliche Formulierungist jedoch erheblich intuitiver. Tatsächlich erwarten wir von einer vernünftigen Pro-grammiersprache, dass sie uns erlaubt, intuitive Programme zu schreiben, so wie wirvom Übersetzer erwarten, dass er für diese intuitiven Programme effizienten Codegeneriert. ��

Page 13: œbersetzerbau: Band 3: Analyse und Transformation

1.1 Einführung 3

Optimierungen sind semantikerhaltende Programmtransformationen. Dies bedeutet,dass die Semantik des Programms von der Transformation nicht verändert wird. DieSemantik des Programms ist durch die Definition der Programmiersprache gegebenin der das Programm formuliert ist.

Beispiel 1.1.2 Betrachten wir die Transformation:

y ← f() + f(); ==⇒ y ← 2 ∗ f();

Die Idee dieser „Optimierung“ besteht darin, die Auswertung des zweiten Aufrufsder Funktion f einzusparen. Das Ergebnis dieser Transformation ist aber nur dannäquivalent zum Ausgangsprogramm, wenn die Funktion f beim zweiten Aufruf ga-rantiert das gleiche Ergebnis liefert und außerdem keine Seiteneffekte hat. In einerimperativen Programmiersprache kann das aber nicht unbedingt garantiert werden.��Programm-Verbesserungen sind damit also nicht unter allen Umständen korrekt. Zujeder effizienzsteigernden Transformation gehören i.A. Anwendbarkeitsbedingun-gen, d.h. hinreichende Bedingungen dafür, dass die Transformation die Semantikdes Programms erhält. Für diese Bedingungen werden Methoden benötigt, mit derenHilfe ein Übersetzer automatisch überprüfen kann, ob die Bedingungen erfüllt sind.

Ein sorgfältiges Vorgehen erfordert hier, dass man erstens nachweist, dass dieVoraussetzungen für die Korrektheit der Transformation hinreichend sind, und zwei-tens einen Beweis führt, dass die Analyse, die die Gültigkeit der Voraussetzungennachweisen soll, niemals falsche Antworten liefert. Beide Korrektheitsbeweise müs-sen auf die operationelle Semantik der Programmiersprache Bezug nehmen.

Einzelne Optimerungen erzielen für viele Programmiersprachen Verbesserun-gen. Im Allgemeinen erfordert aber jede Programmiersprache (oder jede Klasse vonProgrammiersprachen) eigene Optimierungen, die die Effizienz der Implementie-rung spezieller Sprachkonstrukte verbessern. Ein Beispiel hierfür ist die Eliminie-rung dynamischer Methodenaufrufe in objektorientierten Sprachen. Statische Auf-rufe ermöglichen aggressives Inlining. Dies ist in objektorientierten Sprachen wegender häufig kleinen Methoden von großer Bedeutung. In FORTRAN spielt Inliningdagegen eine untergeordnete Rolle. Wichtig für FORTRAN ist zum Beispiel die Par-allelisierung/Vektorisierung geschachtelter Schleifen.

Des Weiteren hat der Entwurf der Programmiersprache einen großen Einflussauf die Effizienz und Effektivität der Programmanalysen. Durch Einschränkungender Programmiersprache können Eigenschaften erzwungen werden, deren Gültig-keit sonst nur unter großem Aufwand analysierbar wäre. Ein Hauptproblem der Pro-grammanalyse imperativer Programme ist, die Abhängigkeiten zwischen den ein-zelnen Anweisungen zu ermitteln. Durch den fast uneingeschränkten Gebrauch vonZeigern, gibt es etwa in C wesentlich mehr Möglichkeiten, diese Analysen zu er-schweren als beispielsweise in JAVA.

Beispiel 1.1.3 Betrachten wir noch einmal die Programmiersprache JAVA. Spra-chimmanente Ineffizienzen sind unter anderem die obligatorische Überprüfung von

Page 14: œbersetzerbau: Band 3: Analyse und Transformation

4 1 Grundlagen und intraprozedurale Optimierung

Feldgrenzen. Ebenfalls teuer sind die dynamische Methodenauswahl und die Spei-cherverwaltung für Objekte.

Die Analysierbarkeit wird dadurch erleichtert, dass es keine Zeigerarithmetikgibt und keine Zeiger in den Keller. Negativ dagegen schlägt zu Buche, dass JA-VA dynamisches Nachladen von Klassen unbekannter Herkunft unterstützt. AuchProgrammierkonzepte wie Ausnahmen, Nebenläufigkeit oder gar Selbstinspektion(Reflection) mögen für das praktische Programmieren unerlässlich sein. Für eineautomatische Programmanalyse stellen sie jedoch beträchtliche Herausforderungendar.

Wie sieht es nun mit den formalen Korrektheitsbeweisen aus? Es sind einigeAnstrengungen unternommen worden, für JAVA eine formalisierte Semantik bereitzu stellen. Explizite Korrektheitsbeweise sind jedoch eher die Ausnahme — wasnicht unbedingt an der prinzipiellen Unmöglichkeit liegt, sondern eher an der Größedes Aufwands: es gibt einfach zu viele Sprachkonzepte, die jeweils separat behandeltwerden müssen. ��Aus diesem Grund werden wir in diesem Buch nicht JAVA als Beispielsprache be-nutzen. Stattdessen verwenden wir einen Ausschnitt aus einer imperativen Program-miersprache. Dieser Ausschnitt soll einerseits so einfach wie möglich sein, anderer-seits aber so realistisch, dass er wesentliche Probleme praktischer Übersetzer um-fasst. Unser Programmiersprachenfragment kann man sich als eine Art Zwischen-sprache vorstellen, in die man das ursprüngliche Programm übersetzt hat. Die int-Variablen des Programms stellen wir uns als virtuelle Register vor, denen währendder Codeerzeugung in der Registerzuteilungsphase (nach Möglichkeit) physikali-sche Register zugewiesen werden. Solche Variablen können wir auch einsetzen, umAdressen für indirekte Speicherzugriffe zu speichern. Arithmetische Ausdrücke die-nen dazu, Werte für int-Variablen zu ermitteln. Schließlich sehen wir ein (konzeptuellbeliebig großes) Feld M vor, in dem int-Werte abgelegt werden und aus dem die ab-gelegten Werte wieder geladen werden können. Dieses Feld M können wir uns alsden gesamten (virtuellen) Speicher vorstellen, den das Betriebssystem zur Verfügungstellt.

Die Trennung zwischen Variablen und Speicher mag zunächst künstlich wirken.Ihre Motivation ist die Alias-Freiheit: Sowohl eine Variable x als auch eine Speicher-zelle M[·] bezeichnen einen Behälter, der einen Wert aufnehmen kann. Die Identitätdes Behälters ist bei einem Zugriff M[e] nicht direkt ersichtlich, da sie vom Wertdes Ausdrucks e abhängt. Im Allgemeinen ist es unentscheidbar, ob durch M[e1]und M[e2] derselbe Behälter angesprochen wird. Bei einer Variable ist das nicht derFall: x ist der einzige Name, um auf den mit x assoziierten Behälter zuzugreifen.Dies ist für viele Programmanalysen wichtig: Kann die Analyse für einen schreiben-den Speicherzugriff M[e] ← x die Indentität des Behälters von M[e] nicht ermitteln,kann fortan über den Inhalt des Restes des Speichers keine Annahmen mehr getrof-fen werden – die Analyse verliert an Präzision. Bei Variablen ist dies nicht möglich,da auf ihre Behälter nicht indirekt zugegriffen werden kann.

Page 15: œbersetzerbau: Band 3: Analyse und Transformation

1.1 Einführung 5

• Variablen: x• arithmetische Ausdrücke: e• Zuweisungen: x ← e• lesender Speicherzugriff: x ← M[e]• schreibender Speicherzugriff: M[e1] ← e2

• bedingte Verzweigung: if (e) s1 else s2

• unbedingte Sprünge: goto L

Beachten Sie, dass wir auf explizite Schleifenkonstrukte verzichtet haben. Diese kön-nen wir jedoch mithilfe bedingter Verzweigungen und unbedingter Sprünge an mar-kierte Programmstellen leicht darstellen. Auch haben wir (vorerst) auf Funktionenund Prozeduren verzichtet. Das bedeutet, dass wir uns zuerst einmal auf die Analyseund Optimierung einzelner Funktionen beschränken.

Beispiel 1.1.4 Betrachten wir erneut unsere Funktion swap() aus Beispiel 1.1.1.Wir stellen uns vor, dass der Übersetzer den Rumpf dieser Funktion schematisch inunsere Zwischensprache übersetzt hätte. Dem Feld a entspricht dann ein bestimmterSpeicherbereich in M. In unseren Programmen muss deshalb die Adressberechnungfür die Feldzugriffe explizit gemacht werden.

0 : A1 ← A0 + 1 ∗ i; // A0 = &a[0]1 : R1 ← M[A1]; // R1 = a[i]2 : A2 ← A0 + 1 ∗ j;3 : R2 ← M[A2]; // R2 = a[ j]4 : if (R1 > R2) {5 : A3 ← A0 + 1 ∗ j;6 : t ← M[A3];7 : A4 ← A0 + 1 ∗ j;8 : A5 ← A0 + 1 ∗ i;9 : R3 ← M[A5];10 : M[A4] ← R3;11 : A6 ← A0 + 1 ∗ i;12 : M[A6] ← t;13 : } //

Dabei wird angenommen, dass die Variable A0 die Anfangsadresse des Feldes aenthält. Beachten Sie, dass dieser Code die Ineffizienzen, die wir in Beispiel 1.1.1diskutiert hatten, nun explizit macht. Welche Optimierungen sind anwendbar?Optimierung 1: 1 ∗ R ==⇒ RDer Skalierungsfaktor, den eine automatische Behandlung der Feldindizierung er-zeugt, kann natürlich eingespart werden, wenn er wie in diesem Fall 1 ist.

Optimierung 2: Wiederbenutzung von Teilausdrücken

Page 16: œbersetzerbau: Band 3: Analyse und Transformation

6 1 Grundlagen und intraprozedurale Optimierung

Eine genauere Betrachtung zeigt uns, dass einerseits die Variablen A1, A5 und A6wie auch andererseits die Variablen A2, A3 und A4 jeweils den gleichen Wert erhal-ten:

A1 = A5 = A6 A2 = A3 = A4

Darüber hinaus liefern auch die Speicherzugriffe M[A1] und M[A5] bzw. M[A2]und M[A3] jeweils die gleichen Werte zurückliefern:

M[A1] = M[A5] M[A2] = M[A3]

Deshalb erhalten auch die Variablen R1 und R3 sowie R2 und t jeweils die gleichenWerte:

R1 = R3 R2 = t

Enthält eine Variable x den Wert eines Ausdrucks e, den wir benötigen, kann manden Inhalt von x benutzen, anstatt den Wert von e ein weiteres Mal zu berechnen.Unter Benutzung dieser Information können wir unser Beispielprogramm stark ver-einfachen:

A1 ← A0 + i;R1 ← M[A1];A2 ← A0 + j;R2 ← M[A2];if (R1 > R2) {

M[A2] ← R1;M[A1] ← R2;

}Wir beobachten, dass die Hilfsvariable t wie auch die Variablen A3, A4, A5 und R3überflüssig geworden sind.

Die folgende Tabelle listet unsere Ersparnisse auf:

vorher nachher

+ 6 2

∗ 6 0

Laden 4 2

Speichern 2 2

> 1 1

← 6 2��

Die Optimierungen, die am Beispiel der Funktion swap mit der Hand durchgeführtwurden, sollen nach Möglichkeit automatisch realisiert werden. Dazu werden wir imFolgenden nach und nach die notwendigen Transformationen und Analysen bereit-stellen.

Page 17: œbersetzerbau: Band 3: Analyse und Transformation

1.2 Vermeidung überflüssiger Berechnungen 7

1.2 Vermeidung überflüssiger Berechnungen

In diesem Kapitel beschreiben wir einige Techniken, um Berechnungen, die das Pro-gramm überflüssigerweise ausführt, einzusparen. Wir beginnen mit einer ersten Op-timierung zur Vermeidung von Mehrfachberechnungen oder Redundanzen. Am Bei-spiel dieser ersten Transformation sollen gleichzeitig grundlegende Vorgehenswe-ten erläutert werden. Insbesondere werden wir in möglichst knappen Exkursen eineoperationelle Semantik für unsere Beispiel-Programmiersprache einführen sowie dienotwendigen verbandstheoretischen Grundlagen diskutieren.

Ein beliebter Trick in der Algorithmik beruht darauf, Rechenzeit gegenüber Spei-cherplatz auszuspielen. Wird eine Berechnung ausgeführt, speichert man den be-rechneten Wert ab. Anstatt die gleiche Berechnung später ein weiteres Mal durchzu-führen, wird der bereits berechnete Wert nachgeschlagen. Diese Technik heißt auchMemoisierung.

Beachten Sie die Bedingungen für die Profitabilität einer solchen Transformati-on: Einerseits benötigt man gegebenenfalls zusätzlichen Platz für die Speicherungder Zwischenergebnisse. Zum anderen wird die Neuberechnung nicht ersatzlos ge-strichen, sondern durch das Nachschlagen des Werts ersetzt. Dieses ist billig, fallsder Wert in einem Register liegt; es könnte aber auch teuer sein, wenn er im Spei-cher abgelegt werden muss. Im letzteren Fall könnte eine Neuberechnung eventuellbilliger sein als die Abspeicherung. Zur Vereinfachung werden wir hier diese Artvon Kosten-Nutzen-Analyse nicht durchführen, sondern stets annehmen, dass Nach-schlagen günstiger ist als Neuberechnung.

Die Berechnungen, die wir hier betrachten, sind Auswertungen von Ausdrückene. Das erste Problem besteht darin, eine Mehrfachberechnung zu erkennen.

Beispiel 1.2.1 Betrachten Sie das Programmstück:

z ← 1;y ← M[5];

A : x1 ← y + z ;

. . .B : x2 ← y + z ;

Es sieht so aus, als ob am Programmpunkt B der Wert des Ausdrucks y + z mitgleichem Ergebnis ein weiteres Mal berechnet wird. Dies ist zumindest immer dannder Fall, wenn die zweite Auswertung stets nach der ersten ausgeführt wird und dieVariablen y und z vor der zweiten Auswertung die gleichen Werte wie vor der erstenAuswertung haben. ��Wir stellen fest, dass wir für eine systematische Codeverbesserung in der Lage seinmüssen, folgende Fragen zu beantworten:

• Wird eine Ausdrucksauswertung stets vor einer anderen ausgeführt?

Page 18: œbersetzerbau: Band 3: Analyse und Transformation

8 1 Grundlagen und intraprozedurale Optimierung

• Hat eine Variable an einem Programmpunkt stets den gleichen Wert wie an ei-nem anderen Programmpunkt?

Zur Beantwortung solcher Fragen benötigen wir zunächst eine operationelle Seman-tik, die festlegt, was bei der Programmausführung passieren soll, und desweiteren einVerfahren, das in Programmen Mehrfachberechnungen identifiziert. Beachten Sie,dass wir keineswegs so ambitioniert sind, sämtliche Mehrfachberechnungen aus-findig machen zu wollen. Dies wäre aus allgemeinen Berechenbarkeitsüberlegun-gen heraus auch unmöglich. In der Praxis ist es jedoch oft ausreichend, wenn unserVerfahren wenigstens einige Mehrfachberechnungen identifiziert und niemals Aus-drucksvorkommen als Mehrfachberechnungen klassifiziert, die in Wirklichkeit garkeine sind!

1.3 Exkurs: Eine operationelle Semantik

Als besonders geeignet für Korrektheitsbeweise von Programmanalysen und -opti-mierungen erweist sich ein small-step operationeller Ansatz. Hierbei wird forma-lisiert, was ein Berechnungsschritt ist. Eine Berechnung ergibt sich dann als eineAbfolge von Berechnungsschritten.

Wir beginnen damit, Programme als Kontrollflussgraphen darzustellen. Die Kno-ten dieses Graphen entsprechen den Programmpunkten, die während einer Berech-nung durchlaufen werden. Die Kanten des Graphen entsprechen den einzelnen Be-rechnungsschritten. In unserem Fall sind sie deshalb mit den zugehörigen Aktio-nen beschriftet, d.h. mit zu überprüfenden Bedingungen, mit Zuweisungen, Laden,Speichern oder mit der leeren Anweisung “;”. Einen Ausschnitt aus dem Kontroll-flussgraphen für den Rumpf der Funktion swap zeigt Abb. 1.1. Dabei repräsentieren

start

stop

NonZero (R1 > R2)Zero (R1 > R2)

A1 ← A0 + 1 ∗ i

R1 ← M[A1 ]

A2 ← A0 + 1 ∗ j

R2 ← M[A2 ]

A3 ← A0 + 1 ∗ j

Abb. 1.1. Ein Ausschnitt aus dem Kontrollflussgraphen für swap().

Page 19: œbersetzerbau: Band 3: Analyse und Transformation

1.3 Exkurs: Eine operationelle Semantik 9

Knoten Programmpunkte, start den Programmanfang, stop das Programmende undKanten Berechnungsschritte.Als Kantenbeschriftungen lassen wir zu:

Test : NonZero(e) oder Zero(e)Zuweisung : x ← eLaden : x ← M[e]Speichern : M[e1] ← e2

leere Anweisung : ;

Dabei behalten wir uns vor, Kantenbeschriftungen mit ; auch wegzulassen. An ei-ner bedingten Programmverzweigung soll NonZero(e) die Kante einer bedingtenVerzweigung beschriften, die genommen wird, wenn die Bedingung e zutrifft, d.h.einen Wert verschieden von 0 liefert. Entsprechend steht Zero(e) an der Kante einerbedingten Verzweigung, die genommen wird, wenn die Bedingung e nicht zutrifft,d.h. den Wert 0 liefert.

Berechnungen geschehen entlang von Pfaden. Sie transformieren den aktuellenProgrammzustand. Programmzustände können wir als Paare repräsentieren:

s = (ρ, μ)

Dabei ordnet die Abbildung ρ jeder Variablen des Programms ihren Wert und dieAbbildung μ jeder Adresse im Speicher den Wert der zugehörigen Speicherzelle zu.Wir nehmen der Einfachheit halber an, dass die Werte von Variablen und die Inhaltevon Speicherzellen jeweils ganze Zahlen sind. Deshalb haben die Abbildungen ρ undμ die Funktionalität:

ρ : Vars → int Werte der Variablen

μ : N → int Inhalt des Speichers

Jede Kante k = (u, lab, v) mit Eingangsknoten u, Endknoten v und Beschriftung labdefiniert eine Transformation [[k]] auf den Zuständen. Diese Transformation nennenwir auch den Effekt der Kante. Der Effekt einer Kante ist möglicherweise nur einepartielle Abbildung. Ist ein Kanteneffekt für einen Zustand s nicht definiert, dannbedeutet das, dass die Programmausführung im Zustand s diese Kante nicht ausfüh-ren wird. Dies kann bei Kanten vorkommen, die mit Bedingungen beschriftet sind,aber auch bei Speicherzugriffen, bei denen auf nicht erlaubte Adressen zugegriffenwird.

Die Transformation [[k]] der Kante k = (u, lab, v) hängt nur von ihrer Beschrif-tung lab ab:

[[k]] = [[lab]]

Die Kanteneffekte [[lab]] sind wie folgt definiert:

Page 20: œbersetzerbau: Band 3: Analyse und Transformation

10 1 Grundlagen und intraprozedurale Optimierung

[[; ]] (ρ, μ) = (ρ, μ)

[[NonZero(e)]] (ρ, μ) = (ρ, μ) falls [[e]]ρ �= 0[[Zero(e)]] (ρ, μ) = (ρ, μ) falls [[e]]ρ = 0

[[x ← e]] (ρ, μ) = ( ρ ⊕ {x → [[e]]ρ} , μ)

[[x ← M[e]]] (ρ, μ) = ( ρ ⊕ {x → μ([[e]]ρ)} , μ)

[[M[e1] ← e2]] (ρ, μ) = (ρ, μ ⊕ {[[e1]]ρ → [[e2]]ρ} )

Eine leere Anweisung verändert den Zustand nicht. Bedingungen, NonZero(e) bzw.Zero(e), repräsentieren eine partielle Identität; die zugehörigen Kanteneffekte sindnur definiert, wenn die Auswertung des Ausdrucks e einen Wert ungleich bzw. gleich0 liefert. Sind diese Kanteneffekte jedoch definiert, ändern sie den Zustand nicht.Zur Berechnung des Werts eines Ausdrucks e haben wir eine Hilfsfunktion [[e]] be-nutzt, die Ausdrucksauswertung, die für eine Variablenbelegung ρ den Wert von eberechnet. Wie üblich ist diese Funktion induktiv über die Struktur des Ausdrucks edefiniert. Damit ergibt sich etwa:

[[x + y]] {x → 7, y → −1} = 6[[¬(x = 4)]] {x → 5} = ¬0 = 1

Der Operator ¬ bezeichnet dabei die logische Negation.Eine Zuweisung x ← e modifiziert die Komponente ρ des Zustands; ρ enthält für

die Variable x jetzt den Wert [[e]]ρ, d.h. den Wert, den die Auswertung des Ausdruckse für die Variablenbelegung ρ vor der Zuweisung liefert. Der Speicher M bleibt durchdiese Zuweisung unverändert. Zur formalen Beschreibung der Abänderung von ρ

benutzen wir den Operator ⊕. Dieser Operator modifiziert eine Funktion, indem erihr für ein Argument einen neuen Wert gibt:

ρ ⊕ {x → d}(y) =

{d falls y ≡ xρ(y) sonst

Eine Lade-Operation x ← M[e] behandeln wir analog zu einer Zuweisung – mitdem Unterschied, dass der neue Wert der Variablen x ermittelt wird, indem erst eineAdresse im Speicher bestimmt wird, um anschließend den Wert aus der entsprechen-den Speichherzelle auszulesen.

Am kompliziertesten ist die Semantik des Speicherns, M[e1] ← e2. Hier ändernsich die Werte der Variablen nicht. Stattdessen müssen zuerst die Werte der Teilaus-drücke e1, e2 ermittelt werden. Der Wert von e1 liefert die Adresse im Speicher, anwelcher der Wert des Ausdrucks e2 abgelegt werden soll.

Page 21: œbersetzerbau: Band 3: Analyse und Transformation

1.4 Beseitigung von Mehrfachberechnungen 11

Sowohl bei der Lade- wie der Speicheroperation nehmen wir an, dass der Adress-ausdruck jeweils eine legale Adresse d.h. einen Wert > 0 liefert.

Beispiel 1.3.1 Für die Zuweisung x ← x + 1 und eine Variablenbelegung {x → 5}ergibt sich:

[[x ← x + 1]] ({x → 5}, μ) = (ρ, μ)

wobei:ρ = {x → 5} ⊕ {x → [[x + 1]] {x → 5}}

= {x → 5} ⊕ {x → 6}= {x → 6}

��Damit haben wir festgelegt, was an Kanten im Kontrollflussgraphen passiert. EineBerechnung π des Programms ist ein Pfad im Kontrollflussgraphen, der von einemStartpunkt u zu einem Endpunkt v führt. Ein solcher Pfad ist eine Folge π = k1 . . . kn

von Kanten ki = (ui, labi, ui+1) des Kontrollflussgraphen (i = 1, . . . , n − 1), wobeiu1 = u und un = v. Die zu π gehörende Zustandstransformation [[π ]] ergibt sichdann als Komposition der Kanteneffekte der Kanten von π :

[[π ]] = [[kn]] ◦ . . . ◦ [[k1]]

Beachten Sie, dass die Abbildung [[π ]] nicht für alle Zustände definiert sein muss.Nur dann, wenn [[π ]] für einen Zustand s definiert ist, ist eine Berechnung entlangder Folge von Kanten π möglich.

1.4 Beseitigung von Mehrfachberechnungen

Kehren wir zu unserem Ausgangsproblem zurück, eine Analyse zu finden, die fürjeden Programmpunkt feststellt, ob ein Ausdruck dort neu berechnet werden mussoder ob sein bereits berechneter Wert benutzt werden kann.

Wir betrachten hier die Verfügbarkeit von Ausdrücken in Variablen. Ein Ausdrucke sehen wir nur dann als verfügbar in der Variable x an, wenn er mit Sicherheitausgewertet, der Variablen x zugewiesen und seither weder x, noch eine der in evorkommenden Variablen modifiziert wurde.

Betrachten wir eine Zuweisung x ← e mit x �∈ Vars(e), d.h. x kommt selbstnicht in dem Ausdruck e vor. Sei weiterhin π = k1 . . . kn ein Pfad vom Startpunktdes Programms zu dem Programmpunkt v. Wir sagen, dass e nach Ausführung vonπ in x verfügbar ist, wenn die beiden folgenden Eigenschaften gelten:

• Der Pfad π enthält eine Kante ki, an der eine Zuweisung x ← e ausgeführt wird.• An keiner der Kanten ki+1, . . . , kn wird einer Variablen aus Vars(e) ∪ {x} ein

neuer Wert zugewiesen.

Page 22: œbersetzerbau: Band 3: Analyse und Transformation

12 1 Grundlagen und intraprozedurale Optimierung

Der Einfachheit halber sagen wir dann auch, die Zuweisung x ← e ist nach der Aus-führung von π verfügbar. Andernfalls nennen wir e in x bzw. x ← e entlang π nichtin x verfügbar. Wir nehmen an, dass am Startpunkt des Programms keine Zuweisungverfügbar ist. Ist π der leere Pfad, d.h. π = ε, dann ist auch keine Zuweisung nachAusführung von π verfügbar.

Betrachten wir eine Kante k = (u, lab, v). Nehmen wir an, wir würden bereitsdie Menge A der Zuweisungen kennen, die vor Ausführung der Kante k verfügbarsind. Dann erhalten wir die Menge der nach Ausführung der Kante k verfügbarenZuweisungen, indem wir auf A eine Funktion [[k]]� anwenden. Die Funktion [[k]]�hängt alleine von der Beschriftung der Kante k ab. Im Gegensatz zu dem Effekt [[k]]der Kante der operationellen Semantik nennen wir den Effekt der Kante, die wirfür die Analyse konstruieren, abstrakt. Im Folgenden wollen wir diese abstraktenKanteneffekte [[k]]� = [[lab]]� konstruieren.

Sei Ass die Menge aller (uns interessierenden) Zuweisungen x ← e des Pro-gramms mit x �∈ Vars(e). Nehmen wir an, am Startpunkt u der Kante k = (u, lab, v)stünde die Menge A ⊆ Ass zur Verfügung. Dann lässt sich die Menge der nach demDurchlaufen der Kante verfügbaren Zuweisungen wie folgt ermitteln:

[[; ]]� A = A[[NonZero(e)]]� A = [[Zero(e)]]� A = A

[[x ← e]]� A =

{(A\Occ(x)) ∪ {x ← e} falls x �∈ Vars(e)A\Occ(x) andernfalls

[[x ← M[e]]]� A = A\Occ(x)[[M[e1] ← e2]]� A = A

wobei Occ(x) die Menge aller Zuweisungen bezeichnet, in denen x entweder alslinke Seite oder in dem Ausdruck auf der rechten Seite vorkommt. Eine leere An-weisung oder eine Bedingung verändert die Verfügbarkeit einer Zuweisung nicht.Bei einer Zuweisung wird der Wert der rechten Seite berechnet und der linken Seitezugewiesen. Deshalb müssen alle Zuweisungen entfernt werden, welche die Variableauf der linken Seite der Zuweisung enthalten. Anschließend muss diese zu A hinzugefügt werden, sofern die linke Seite nicht in der rechten vorkommt. Der abstrakteKanteneffekt für das Laden aus dem Speicher sieht analog aus, während bei einemAbspeichern keine Variablen modifiziert werden. Hier ändert sich A nicht.

An jeder Kante ändert sich die Menge der verfügbaren Zuweisungen durch Her-ausnehmen oder Hinzufügen von Elementen. Die abstrakten Effekte, die wir für jedeeinzelne Kante definiert haben, setzen wir zu der abstrakten Transformation [[π ]]�,die zu einem Pfad π = k1 . . . kn gehört, wie folgt zusammen:

[[π ]]� = [[kn]]� ◦ . . . ◦ [[k1]]�

Die Menge der nach Ausführung des Pfads π vom Startknoten zum Programmpunktv verfügbaren Zuweisungen ergibt sich deshalb als

[[π ]]�∅ = [[kn]]�(. . . ([[k1]]� ∅) . . .)

Page 23: œbersetzerbau: Band 3: Analyse und Transformation

1.4 Beseitigung von Mehrfachberechnungen 13

Damit kann ein einzelner Pfad π daraufhin untersucht werden, welche Zuweisungenentlang π zur Verfügung stehen. In einem Programm wird es jedoch typischerwei-se mehrere Pfade geben, die einen Programmpunkt v erreichen. Welcher von diesenbei der Programmausführung ausgewählt wird, kann von der Eingabe abhängen undist deshalb zur Übersetzungszeit meist unbekannt. Wir betrachten eine Zuweisungx ← e als sicher verfügbar an einem Programmpunkt v, wenn sie auf allen Pfadenvom Startpunkt zum Programmpunkt v verfügbar ist. Andernfalls ist x ← e mögli-cherweise nicht verfügbar. Die Menge der am Programmpunkt v sicher verfügbarenZuweisungen ist deshalb gegeben durch:

A∗[v] =⋂{[[π ]]�∅ | π : start →∗ v}

Dabei bezeichnet start →∗ v die Menge aller Pfade vom Startpunkt start des Pro-gramms zum Programmpunkt v.

Im Moment wollen wir die Frage hintenanstellen, wie man die Mengen A∗[v]berechnen kann. Stattdessen wollen wir zuerst einmal überlegen, wie sich diese In-formation für eine optimierende Transformation des Programms ausnutzen lässt.

Transformation RE:

Wir ersetzen eine Zuweisung x ← e durch x ← y, wenn eine Zuweisung y ← eam Programmpunkt u vor dieser Zuweisung definitiv verfügbar, d.h. in der MengeA∗[u] enthalten ist. Dies formalisiert die folgende Graphersetzungsregel:

u u

x ← ey ← e ∈ A∗[u]

x ← y

Analoge Regeln verwenden wir, um die Ausdrücke in Bedingungen, beim Laden ausdem Speicher und beim Schreiben in den Speicher gegebenenfalls durch Variablen-zugriffe zu ersetzen.

Die Transformation RE nennen wir auch Beseitigung von Redundanzen (englisch:Redundancy Elimination. Wir sehen, die tatsächliche Transformation ist sehr ein-fach. Aufwändig dagegen kann es sein, die für die Transformation notwendigen Pro-grammeigenschaften zu berechnen.

Beispiel 1.4.1 Betrachten wir das folgende kurze Programmstück:

x ← y + 3;x ← 7;z ← y + 3;

Vor der Programmausführung ist x ← y + 3 nicht verfügbar. Nach der ersten Zu-weisung ist diese stets verfügbar. Da die zweite Zuweisung jedoch den Wert von xüberschreibt, kann die zweite Zuweisung nicht vereinfacht werden. ��

Page 24: œbersetzerbau: Band 3: Analyse und Transformation

14 1 Grundlagen und intraprozedurale Optimierung

Beispiel 1.4.2 Betrachten wir die Implementierung der Anweisung a[7]-−; in unse-rer Beispiel-Sprache. Nehmen wir dabei an, dass sich die Anfangsadresse des Feldsa in der Variable A befindet. Den ursprünglichen Kontrollfluss-Graphen zu dem Pro-grammfragment zusammen mit der Anwendung der Transformation RE zeigt Abb.1.2. Weil bei Erreichen der Zuweisung A2 ← A + 7 die Zuweisung A1 ← A + 7

B1 ← M[A1 ]

A1 ← A + 7

B2 ← B1 − 1

A2 ← A + 7

M[A2 ] ← B2

A1 ← A + 7

B1 ← M[A1 ]

B2 ← B1 − 1

A2 ← A1

M[A2 ] ← B2

Abb. 1.2. Die Transformation RE für a[7]−−;.

verfügbar ist, können wir die rechte Seite A + 7 durch die Variable A1 ersetzen. ��Um die Anwendbarkeit der Transformation RE zu erhöhen, können wir für jedenuns interessierenden Ausdruck eine eigene Variable zur Verfügung stellen. Damitumgehen wir das Problem, dass ein Ausdruck zwar berechnet wurde, sein Wert abernicht mehr zugreifbar ist, weil die Variable, in der sein Wert abgespeichert wur-de, mittlerweile einen neuen Wert erhielt (vgl. Beispiel 1.4.1). Eine entsprechendeTransformation entwickelt Aufg. 5.

In einer praktischen Implementierung wird der Übersetzer nicht für alle Zuweisun-gen die Verfügbarkeit bestimmen, sondern nur für solche, bei denen die Neuberech-nung der rechten Seite teurer als ein Variablenzugriff ist.Wenden wir uns dem Beweis der Korrektheit der vorgestellten Transformationen zu.Wir können ihn in zwei Teile aufteilen.

1. Den Beweis der Korrektheit der abstrakten Kanteneffekte [[k]]� relativ zur Defi-nition der Verfügbarkeit;

2. Den Beweis der Korrektheit der Ersetzung von Ausdrücken e durch Variablen-zugriffe.

Hier betrachten wir nur den zweiten Punkt. Die Definition der Verfügbarkeit istin gewissem Sinne rein syntaktisch. Sei π ein Pfad im transformierten Programmvom Startpunkt des Programms zu einem Programmpunkt u und sei s = (ρ, μ)der Zustand nach Ausführung der Berechnung π . Sei x ← e eine Zuweisung mit

Page 25: œbersetzerbau: Band 3: Analyse und Transformation

1.4 Beseitigung von Mehrfachberechnungen 15

x �∈ Vars(e). Nehmen wir weiter an, dass x ← e an u verfügbar ist. Dann müssenwir zeigen, dass im Zustand s der Wert der Variablen x gleich dem Wert des Aus-drucks e für die Variablenbelegung ρ ist, d.h. ρ(x) = [[e]]ρ. Diese Eigenschaft wirddurch Induktion über die Länge der Berechnung π bewiesen.

Nehmen wir nun an, an dem Programmpunkt u gebe es eine ausgehende Kantek, an der eine Zuweisung x ← e erfolgt. Nehmen wir weiter an, y ← e sei inA∗[u] enthalten, d.h. verfügbar. Dann ist y ← e insbesondere auch in der Mengeder nach π verfügbaren Zuweisungen enthalten. Folglich gilt ρ(y) = [[e]]ρ. Unterdieser Bedingung kann die Zuweisung x ← e durch x ← y ersetzt werden.

Es bleibt die Preisfrage: Wie berechnen wir die Mengen A∗[u]?

Eine grundlegende Idee besteht darin, ein Ungleichungssystem aufzustellen, dasdiese Werte charakterisiert. In dem Ungleichungssystem sammeln wir Bedingungen,welche die gesuchten Mengen erfüllen müssen:

A[start] ⊆ ∅A[v] ⊆ [[k]]� (A[u]) k = (u, lab, v) Kante

Wir nehmen an, dass am Startpunkt des Programms keinerlei Zuweisungen verfüg-bar sind. Das wird durch die erste Ungleichung ausgedrückt. Weiterhin erzeugt jedeKante k von einem Programmpunkt u zu einem Programmpunkt v eine Ungleichung.Diese Ungleichung beschreibt, wie verfügbare Zuweisungen entlang der Kante k vonu nach v propagiert werden. Die Menge der am Endpunkt v der Kante k verfügba-ren Zuweisungen ist in der Menge der Zuweisungen enthalten, die sich entlang derKante k aus den am Programmpunkt u verfügbaren Zuweisungen ergeben: deshalbdie Inklusionsbeziehung zwischen A[v] und [[k]]� (A[u]).

Beispiel 1.4.3 Betrachten wir als Beispiel ein Programm, das die Fakultätsfunkti-on implementiert (Abb. 1.3). Wir sehen, dass das Ungleichungssystem mithilfe der

3

2

4

5

0

1NonZero(x > 1)Zero(x > 1)

y ← 1

y ← x ∗ y

x ← x − 1

A[0] ⊆ ∅A[1] ⊆ (A[0]\Occ(y)) ∪ {y ← 1}A[1] ⊆ A[4]A[2] ⊆ A[1]A[3] ⊆ A[2]\Occ(y)A[4] ⊆ A[3]\Occ(x)A[5] ⊆ A[1]

Abb. 1.3. Das Ungleichungssystem für die Fakultätsfunktion.

abstrakten Kantentransformationen ganz schematisch aus dem Kontrollflussgraphen

Page 26: œbersetzerbau: Band 3: Analyse und Transformation

16 1 Grundlagen und intraprozedurale Optimierung

gewonnen werden kann. In dem Beispiel lässt sich das Ungleichungssystem wei-ter stark vereinfachen. Die einzige Zuweisung, bei der die Variable der linken Seitenicht auf der rechten Seite vorkommt, ist y ← 1. Der vollständige Verband für ver-fügbare Zuweisungen besteht deshalb nur aus den zwei Elementen ∅ und {y ← 1}.Entsprechend ist Occ(y) = {y ← 1} und Occ(x) = ∅.

Eine triviale Lösung dieses Ungleichungssystems zeigt Abb. 1.4. Diese Lösung

A[0] = A[1] = A[2] = A[3] = A[4] = A[5] = ∅

Abb. 1.4. Eine triviale Lösung für das Ungleichungssystem aus Beispiel 1.4.3.

ist in diesem Fall auch die einzige Lösung. Im allgemeinen kann es jedoch sehr wohlmehrere Lösungen geben. Bei der Verfügbarkeit von Zuweisungen sind wir dannan größt möglichen Mengen interessiert: Je größer das Ergebnis ist, d.h. desto mehrZuweisungen wir als verfügbar nachweisen, desto genauer ist unsere Analyse unddesto mehr Möglichkeiten gibt es zur Optimierung.

Wir fragen uns, ob eine größte Lösung immer existiert, und wenn ja, ob wir sieeffizient berechnen können. ��Um die Fragen nach der Existenz von „besten“ Lösungen von Ungleichungssyste-men und ihrer effizienten Berechnung systematisch beantworten und auf andere Pro-grammanalysen anwenden zu können, verallgemeinern wir die Problemstellung einwenig.

Als erstes beobachten wir dazu, dass die Menge der möglichen Werte für die un-bekannten A[v] eine Halbordnung bzgl. der Teilmengenrelation ⊆ und damit auchbzgl. der Obermengenrelation ⊇ bildet. Diese Halbordnung hat die zusätzliche Ei-genschaft, dass jede Teilmenge X von Werten eine kleinste obere Schranke bzw. einegrößte untere Schranke besitzt, nämlich gerade die Vereinigung bzw. den Durch-schnitt der Mengen in X. Eine Halbordnung mit dieser Zusatzeigenschaft nennt manauch vollständigen Verband.

Weiterhin beobachten wir, dass die abstrakten Kanten-Transformationen [[k]]�monoton sind, d.h. die Ordungsrelation auf Werten erhalten:

[[k]]�(B1) ⊇ [[k]]�(B2) wenn B1 ⊇ B2

1.5 Exkurs: Vollständige Verbände

In diesem Abschnitt sammeln wir grundlegende Begriffe und Sätze über vollständigeVerbände, Lösungen von Ungleichungssystemen und grundlegenden Verfahren, umkleinste Lösungen zu berechnen. Wir beginnen mit den Definitionen von Halbord-nung und vollständigem Verband.

Page 27: œbersetzerbau: Band 3: Analyse und Transformation

1.5 Exkurs: Vollständige Verbände 17

Eine Menge D mit einer Relation � ⊆ D × D nennen wir eine Halbord-nung (Partial Order), falls folgende Eigenschaften für alle a, b, c ∈ D gelten:

a � a Reflexivitata � b ∧ b � a =⇒ a = b Antisymmetriea � b ∧ b � c =⇒ a � c Transitivitat

Das üblicherweise verwendete Symbol � sollte Sie dabei an die typischen Ord-nungsrelationen ≤ auf Zahlen und ⊆ auf Mengen erinnern. Beispiele für Halbord-nungen sind:

1. Die Menge D = 2{a,b,c} aller Teilmengen einer endlichen Grundmenge, hier{a, b, c} mit der Relation ⊆:

a, b, c

a, b a, c b, c

a b c

2. Die Menge aller ganzen Zahlen Z mit der Relation =:

210-1-2

3. Die Menge aller ganzen Zahlen Z mit der Relation ≤:

0-1

12

4. Die Menge aller ganzen Zahlen Z⊥ = Z ∪ {⊥}, erweitert um ein zusätzlichesElement ⊥ mit der Ordnung:

210-1-2

Ein Element d ∈ D heißt obere Schranke für eine Teilmenge X ⊆ D falls

x � d für alle x ∈ X

Das Element d heißt kleinste obere Schranke (englisch: least upper bound oder lub),falls

1. d eine obere Schranke ist und2. d � y für jede obere Schranke y von X gilt.

Page 28: œbersetzerbau: Band 3: Analyse und Transformation

18 1 Grundlagen und intraprozedurale Optimierung

Nicht jede Teilmenge in einer Halbordnung hat notwendigerweise auch eine obereSchranke, geschweige denn eine kleinste obere Schranke. In der Halbordnung Z derganzen Zahlen, ausgestattet mit der natürlichen Ordnung ≤ besitzt etwa die Men-ge {0, 2, 4} die oberen Schranken 4, 5, . . . . Die Menge {0, 2, 4, . . .} aller geradenZahlen besitzt dagegen keine obere Schranke.

Eine Halbordnung D ist ein vollständiger Verband (englisch: complete lattice),falls jede Teilmenge X ⊆ D eine kleinste obere Schranke besitzt. Diese kleinsteobere Schranke bezeichnen wir auch mit

⊔X.

Jedes Element ist eine obere Schranke der leeren Menge. Weil in einem voll-ständigen Verband auch die leere Menge eine kleinste obere Schranke besitzt, gibtes in jedem vollständigen Verband ein Element ⊥, das kleiner oder gleich jedemanderen Element des vollständigen Verbands ist. Dieses kleinste Element wird auchBottom-Element genannt. Weil in einem vollständigen Verband auch die Menge allerElemente eine obere Schranke besitzen muss, gibt es in jedem vollständigen Verbandauch ein ein größtes Element �, das Top-Element. Betrachten wir unsere Beispiel-Halbordnungen. Dann gilt:

1. Die Menge D = 2{a,b,c} aller Teilmengen der Grundmenge {a, b, c} und allge-mein jeder Grundmenge zusammen mit der Teilmengenrelation ist ein vollstän-diger Verband.

2. Die Menge Z aller ganzen Zahlen ist weder mit der Halbordnung = noch mitder Halbordnung ≤ ein vollständiger Verband.

3. Die Hinzufügung eines kleinsten Elements ⊥ reicht ebenfalls nicht, um aus Z

mit = einen vollständigen Verband zu erhalten. Vielmehr müssen wir außer ei-nem kleinsten Element ⊥ auch noch ein größtes Element, das Top-Element, �,hinzufügen. Das Ergebnis ist der flache Verband Z�

⊥ = Z∪ {⊥,�} :

210-1-2

Analog zu oberen Schranken und kleinsten oberen Schranken kann man auch untereSchranken und größte untere Schranken für eine Teilmenge X einer Halbordnungdefinieren. Zum Aufwärmen beweisen wir den folgenden Satz:

Satz 1.5.1 In jedem vollständigen Verband D besitzt jede Teilmenge X ⊆ D einegrößte untere Schranke ⊔X.

Beweis. Sei U = {u ∈ D | ∀ x ∈ X : u � x} die Menge sämtlicher untererSchranken der Menge X. Da D ein vollständiger Verband ist, besitzt die Menge Ueine kleinste obere Schranke g :=

⊔U. Wir behaupten, dass g die gesuchte größte

untere Schranke der Menge X ist.Um diese Behauptung zu beweisen, zeigen wir zuerst einmal, dass g ebenfalls

eine untere Schranke der Menge X ist. Dazu betrachten wir ein beliebiges Element

Page 29: œbersetzerbau: Band 3: Analyse und Transformation

1.5 Exkurs: Vollständige Verbände 19

x ∈ X. Dann gilt u � x für jedes u ∈ U, da jedes u ∈ U sogar eine untereSchranke für ganz X ist. Folglich ist x eine obere Schranke der Menge U und damitinsbesondere größer oder gleich der kleinsten oberen Schranke von U, d.h. g � x.Da x beliebig war, folgt unsere Behauptung.

Es bleibt zu zeigen, dass g auch die größte untere Schranke von X ist. Dies istaber einfach: weil g eine obere Schranke für U ist, ist g insbesondere größer odergleich jedem Element in U, d.h. u � g für alle u ∈ U. ��Die Verhältnisse in einem vollständigen Verband veranschaulicht Abb. 1.5. Dass es

Abb. 1.5. Die obere und untere Schranke für eine Teilmenge X.

zu einer Teilmenge X stets eine kleinste obere Schranke gibt, folgt aus der Definitioneines vollständigen Verbands. Dass die Teilmenge X aber ebenfalls über eine größteuntere Schranke verfügt, folgt aus Satz 1.5.1.

Wir suchen Lösungen für Ungleichungssysteme der Form:

xi � fi(x1, . . . , xn) i = 1, . . . , n

Bei der Bestimmung der verfügbaren Zuweisungen entsprechen dabei die Unbe-kannten xi in den Ungleichungen den A[u] (u Programmpunkt). Der vollständigeVerband D, in dem wir Werte für die Unbekannten suchen, ist der Teilmengenver-band 2Ass, wobei die Halbordnung durch die Obermengenrelation ⊇ gegeben ist. DieFunktionen fi : Dn → D schließlich beschreiben, wie die Unbekannten xi von denanderen Unbekannten abhängen. Die Ungleichungen haben damit die Form:

A[start] ⊆ ∅ Kante}A[v] ⊆ ⋂{[[k]]� (A[u]) | k = (u, lab, v) Kante} für v �= start

Page 30: œbersetzerbau: Band 3: Analyse und Transformation

20 1 Grundlagen und intraprozedurale Optimierung

Zur Vereinfachung haben wir sämtliche Ungleichungen für dieselbe Unbekannte zueiner Ungleichung zusammen gefasst, indem wir die kleinste obere Schranke überdie Beiträge der rechten Seiten der einzelnen Ungleichungen bilden. Diese Formu-lierung ändert die Lösungsmenge der entsprechenden Ungleichungen nicht, da gilt:

x � d1 ∧ . . . ∧ x � dk gdw. x � ⊔{d1, . . . , dk}Eine wesentliche Eigenschaft der Funktionen fi, die die rechten Seiten unserer Un-gleichungen definieren, ist, dass sie monoton sind. Eine Funktion f : D1 → D2zwischen den beiden Halbordnungen D1, D2 heißt monoton, falls f (a) � f (b) gilt,sofern a � b gilt. Der Einfachkeit halber haben wir hier die Ordnungsrelationen inD1 und in D2 mit dem gleichen Symbol � bezeichnet.

Beispiel 1.5.1 Für eine Menge U D1 = D2 = 2U sei der Teilmengenverband mitder Ordnungsrelation ⊆. Dann ist jede Funktion f mit f x = (x∩ a)∪ b für a, b ⊆ Umonoton. Eine Funktion g mit g x = a \ x für a �= ∅ ist dagegen nicht monoton.

Für D1 = D2 = Z mit der Ordnungsrelation “≤” sind die Funktionen inc unddec mit inc x = x + 1 bzw. dec x = x − 1 monoton. Die durch inv x = −x defi-nierte Funktion inv ist dagegen nicht monoton. ��Sind die Funktionen f1 : D1 → D2 und f2 : D2 → D3 monoton, dann ist auch ihreKomposition f2 ◦ f1 : D1 → D3 monoton.

Ist D2 ein vollständiger Verband, dann bildet auch die Menge [D1 → D2] dermonotonen Funktionen f : D1 → D2 einen vollständigen Verband, wobei

f � g gdw. f x � g x für alle x ∈ D1

gilt. Insbesondere ist für F ⊆ [D1 → D2] die Funktion f mit f x =⊔{g x | g ∈

F} selbst wieder monoton und die kleinste obere Schranke der Menge F.Im Falle von D1 = D2 = 2U können wir für Funktionen fi x = ai ∩ x ∪ bi mit

ai , bi ⊆ U, die Operationen “◦”, “�” und “�” explizit durch Operationen auf denMengen ai , bi ausdrücken;

( f2 ◦ f1) x = a1 ∩ a2 ∩ x ∪ a2 ∩ b1 ∪ b2 Komposition

( f1 � f2) x = (a1 ∪ a2) ∩ x ∪ b1 ∪ b2 Vereinigung

( f1 � f2) x = (a1 ∪ b1) ∩ (a2 ∪ b2) ∩ x ∪ b1 ∩ b2 Durchschnitt

Unser Ziel ist, für das Ungleichungssystem;

xi � fi(x1, . . . , xn), i = 1, . . . , n (∗)eine möglichst kleine Lösung in einem vollständigen Verband D zu konstruieren, wo-bei die fi : Dn → D, welche die rechten Seiten definieren, jeweils monoton sein sol-len. Hier benutzen wir, dass mit D auch Dn ein vollständiger Verband ist. Um das zuGrunde liegende Problem weiter zu vereinfachen, fassen wir die n Funktionen fi zueiner einzigen Funktion f : Dn → Dn zusammen mit f (x1, . . . , xn) = (y1, . . . , yn),

Page 31: œbersetzerbau: Band 3: Analyse und Transformation

1.5 Exkurs: Vollständige Verbände 21

wobei yi = fi(x1, . . . , xn). Es zeigt sich, dass mit den Komponenten-Funktionen fi

auch f monoton ist. Unser Problem hat sich darauf reduziert, eine möglichst kleineLösung einer einzigen Ungleichung x � f x in dem allerdings nun etwas kompli-zierteren vollständigen Verband Dn zu finden.

Dabei gehen wir so vor: Wir beginnen mit einem möglichst kleinen Element d,also etwa mit d = ⊥ = (⊥, . . . ,⊥), dem kleinsten Element von Dn. Falls d � f dgilt, haben wir eine Lösung gefunden. Andernfalls ersetzen wir d durch f d undwiederholen die Ersetzung.

Beispiel 1.5.2 Betrachten wir den vollständigen Verband D = 2{a,b,c} mit der Ord-nungsrelation � = ⊆ zusammen mit dem Ungleichungssystem:

x1 ⊇ {a} ∪ x3

x2 ⊇ x3 ∩ {a, b}x3 ⊇ x1 ∪ {c}

Dann ergibt die Iteration: Die Ergebnisse der einzelnen Iterationen sind in der fol-genden Tabelle zusammen gefasst:

0 1 2 3 4

x1 ∅ {a} {a, c} {a, c} dito

x2 ∅ ∅ ∅ {a}x3 ∅ {c} {a, c} {a, c}

Wir beobachten, dass mindestens ein Wert für die Unbekannten mit jeder Iterationgrößer wird, bis am Ende eine Lösung gefunden ist. ��Tatsächlich können wir uns davon überzeugen, dass dies für jeden vollständigen Ver-band der Fall ist. Genauer gesagt, zeigen wir:

Satz 1.5.2 Sei D ein vollständiger Verband und f : D → D eine monotone Funktion.Dann gilt:

1. Die Folge ⊥, f ⊥, f 2 ⊥, . . . ist eine aufsteigende Kette, d.h. es gilt f i−1 ⊥ �f i ⊥ für alle i ≥ 1.

2. Ist d = f n−1 ⊥ = f n ⊥, dann ist d das kleinste Element d′ mit d′ � f (d′).

Beweis. Zum Beweis der ersten Aussage wenden wir vollständige Induktion an.Für i = 1 gilt die erste Aussage, weil f 1−1 ⊥ = f 0 ⊥ = ⊥ das kleinste Elementdes vollständigen Verbands und damit kleiner oder gleich f 1 ⊥ = f ⊥ ist. Nehmenwir an, die Aussage gelte für i − 1 ≥ 1, d.h. es gilt f i−2 ⊥ � f i−1 ⊥. Wegen derMonotonie der Funktion f gilt:

f i−1 ⊥ = f ( f i−2 ⊥) � f ( f i−1 ⊥) = f i ⊥Wir schließen, dass damit die Aussage auch für i gilt. Folglich gilt die Aussage füralle i ≥ 1.

Page 32: œbersetzerbau: Band 3: Analyse und Transformation

22 1 Grundlagen und intraprozedurale Optimierung

Betrachten wir nun die zweite Aussage. Nehmen wir an, dass

d = f n−1 ⊥ � f n ⊥gilt. Dann ist d is eine Lösung der Ungleichung x � f x. Nehmen wir weiter an, wirhätten irgendeine andere Lösung d′ der Ungleichung, d.h., es gelte auch d′ � f d′.Dann genügt es zu zeigen, dass f i ⊥ � d′ für alle i ≥ 0 gilt. Dies zeigen wirerneut mittels vollständiger Induktion. Für i = 0 ist dies der Fall. Sei nun i > 0 undf i−1 ⊥ � d′. Wegen der Monotonie von f gilt dann,

f i ⊥ = f ( f i−1 ⊥) � f d′ � d′

da d′ eine Lösung ist. Damit gilt unsere Behauptung für alle i. ��Satz 1.5.2 gibt uns ein Verfahren an die Hand, nicht nur irgendeine Lösung, sondernsogar die kleinste Lösung einer Ungleichung zu berechnen – unter der Vorausset-zung, dass die aufsteigende Kette der f i ⊥ irgendwann stabil wird, d.h. ab einem ikonstant ist. Für die Terminierung unseres Verfahrens ist es damit hinreichend, wennsämtliche aufsteigenden Ketten in D irgendwann stabil werden. Das ist sicherlich derFall, sofern wir mit endlichen Verbänden rechnen. Die durch unser Iterationsverfah-ren gefundene kleinste Lösung ist tatsächlich eine Lösung nicht nur der Ungleichungx � f x, sondern sogar eine Lösung der Gleichung: x = f x, d.h. ein Fixpunkt vonf .

Was passiert, wenn nicht sämtliche aufsteigende Ketten in unserem vollständi-gen Verband irgendwann stabil werden? Dann wird unser Iterationsverfahren mögli-cherweise nie terminieren. Nichtsdestoweniger gibt es auch in diesem Fall stets einekleinste Lösung.

Satz 1.5.3 (Knaster – Tarski) In einem vollständigen Verband D hat jede monotoneFunktion f : D → D einen kleinsten Fixpunkt d0, welcher auch die kleinste Lösungder Ungleichung x � f x ist.

Beweis. Eine Lösung der Ungleichung x � f x nennen wir auch Postfixpunkt vonf . Sei P = {d ∈ D | d � f d} die Menge der Postfixpunkte von f . Wir behaupten,dass die größte untere Schranke d0 der Menge P gerade der kleinste Fixpunkt von fist.Dazu beweisen wir zuerst einmal, dass d0 selbst in P enthalten ist, d.h. ein Postfix-punkt von f ist. Offenbar gilt f d0 � f d � d für jeden Postfixpunkt d ∈ P. Folglichist f d0 eine untere Schranke von P und damit kleiner oder gleich der größten unterenSchranke, d.h. f d0 � d0.

Als untere Schranke von P, die in P enthalten ist, ist d0 der kleinste Postfixpunktvon f . Es bleibt zu zeigen, dass f auch ein Fixpunkt von f und damit der kleinsteFixpunkt von f ist.

Wir wissen bereits, dass f d0 � d0 gilt. Betrachten wir die umgekehrte Richtung.Wegen der Monotonie von f , folgern wir, dass auch f ( f d0) � f d0 gilt. Folglich istf d0 ein Postfixpunkt von f , d.h. f d0 ∈ P. Weil aber d0 eine untere Schranke von Pist, muss dann auch d0 � f d0 gelten. ��

Page 33: œbersetzerbau: Band 3: Analyse und Transformation

1.5 Exkurs: Vollständige Verbände 23

Satz 1.5.3 garantiert uns, dass jede monotone Funktion f in einem vollständigenVerband einen kleinsten Fixpunkt besitzt, welcher mit der kleinsten Lösung der Un-gleichung x � f x übereinstimmt.

Beispiel 1.5.3 Sei der vollständige Verband die Menge der natürlichen Zahlen, er-weitert um ∞, d.h. D = N ∪ {∞} mit der Halbordnung ≤. Die Funktion inc mitinc x = x + 1 ist monoton. Es gilt:

inci ⊥ = inci 0 = i � i + 1 = inci+1 ⊥Damit besitzt diese Funktion einen kleinsten Fixpunkt. Dieser wird aber nicht nachendlich vielen Iterationen erreicht. ��Indem wir Satz 1.5.3 auf den vollständigen Verband mit der dualen Ordnungsrelation� (anstelle von �) betrachten, folgern wir, dass jede monotone Funktion nicht nureinen kleinsten, sondern auch einen größten Fixpunkt besitzt.

Beispiel 1.5.4 Betrachten wir erneut den Teilmengenverband D = 2U für eineGrundmenge U und eine Funktion f mit f x = x ∩ a ∪ b. Diese Funktion ist mo-noton. Deshalb hat sie sowohl einen kleinsten wie einen größten Fixpunkt. UnserIterationsverfahren liefert für f :

f f k ⊥ f k �0 ∅ U1 b a ∪ b2 b a ∪ b

��

Wenden wir uns mit diesem Hintergrundwissen wieder unserer Anwendung zu, d.h.dem Lösen eines Ungleichungssystems

xi � fi(x1, . . . , xn), i = 1, . . . , n (∗)über einem vollständigen Verband D für monotone Funktionen fi : Dn → D. Wirwissen nun, dass ein solches Ungleichungssystem stets eine kleinste Lösung besitzt,die mit der kleinsten Lösung des zugehörigen Gleichungssystems

xi = fi(x1, . . . , xn), i = 1, . . . , n

übereinstimmt. In unseren Anwendungen bei der Programmanalyse treffen wir sehroft vollständige Verbände an, in denen es alle aufsteigenden Ketten stabil werden.In diesen Fällen kann die kleinste Lösung des Ungleichungssystems durch unserIterationsverfahren, d.h. wiederholtes Einsetzen explizit berechnet werden. Jedochist die naive Fixpunktiteration gemäß Satz 1.5.2 oft ziemlich ineffizient.

Beispiel 1.5.5 Betrachten wir erneut die Implementierung des Fakultätsprogrammsaus Beispiel 1.4.3. Die Fixpunktiteration zur Berechnung der kleinsten Lösung desUngleichungssystems für verfügbare Zuweisungen zeigt Abb. 1.6. Erst nach fünfRunden stabilisieren sich sämtliche Werte für die Unbekannten. ��

Page 34: œbersetzerbau: Band 3: Analyse und Transformation

24 1 Grundlagen und intraprozedurale Optimierung

1 2 3 4 50 ∅ ∅ ∅ ∅ ∅1 {y ← 1} {y ← 1} ∅ ∅ ∅2 {y ← 1} {y ← 1} ∅ ∅ ∅3 ∅ ∅ ∅ ∅ ∅4 {y ← 1} ∅ ∅ ∅ ∅5 {y ← 1} {y ← 1} ∅ ∅ ∅

Abb. 1.6. Die naive Fixpunktiteration für das Programm aus Beispiel 1.4.3.

Wie könnte man die naive Fixpunktiteration verbessern? Eine erhebliche praktischeVerbesserung lässt sich durch die sogenannte Round-Robin-Iteration erzielen. Al-gorithmisch ist diese sogar leichter als die naive Iteration zu realisieren: man greiftbei der Neuberechnung der Werte für die Unbekannten nicht auf die Werte der Un-bekannten der letzten Runde zurück, sondern benutzt deren in der aktuellen Rundebereits berechneten Wert:

for (i ← 1; i ≤ n; i++) xi ← ⊥;do {

finished ← true;for (i ← 1; i ≤ n; i++) {

new ← fi(x1, . . . , xn);if (¬(xi � new)) {

finished ← false;xi ← xi � new;

}}

} while (¬finished);

Beispiel 1.5.6 Betrachten wir erneut das Ungleichungssystem zur Berechnung derverfügbaren Ausdrücke für das Fakultätsprogramm aus Beispiel 1.4.3. Die zugehö-rige Round-Robin-Iteration zeigt Abbildung 1.7. Offensichtlich reichen nun bereitsdrei Iterationen aus! ��Betrachten wir die Round-Robin-Iteration näher. Die Zuweisung xi ← xi � new;in unserer Implementierung überschreibt nicht einfach den alten Wert für xi, son-dern ersetzt ihn durch die kleinste obere Schranke mit dem neuen Wert. Wir sagen,dass der Algorithmus während seiner Iteration die Lösung für xi akkumuliert. ImFalle monotoner Funktionen fi ist die kleinste obere Schranke des alten Werts für xi

mit dem neuen Wert gerade gleich dem neuen Wert. Im Falle einer nicht-monotonenFunktion fi ist dies allerdings nicht immer der Fall. Dann ist der Algorithmus je-doch robust genug, eine aufsteigende Folge von Werten für jede Unbekannte xi zuberechnen und damit – im Falle der Terminierung – zumindest irgendeine Lösungdes Ungleichungssystems zu liefern.

Page 35: œbersetzerbau: Band 3: Analyse und Transformation

1.5 Exkurs: Vollständige Verbände 25

1 2 30 ∅ ∅1 {y ← 1} ∅2 {y ← 1} ∅3 ∅ ∅ dito4 ∅ ∅5 ∅ ∅

Abb. 1.7. Die Round-Robin-Iteration für das Programm aus Beispiel 1.4.3.

Die Laufzeit des Verfahrens hängt davon ab, wie oft die do-while-Schleife durch-laufen wird. Sei h die maximale Länge einer echt aufsteigenden Kette:

⊥ � d1 � d2 � . . . � dh

in dem vollständigen Verband D. Diese Zahl nennen wir auch die Höhe des voll-ständigen Verbands D. Sei weiterhin n die Anzahl der Unbekannten des Unglei-chungssystems. Dann benötigt die Round-Robin-Iteration maximal h · n Runden derdo-while-Schleife, bis die Werte der kleinsten Lösung für sämtliche Unbekanntenermittelt sind — zusammen gegebenenfalls mit einer weiteren Runde, um die Termi-nierung festzustellen.

Die Abschätzung h · n kann auf n verbessert werden, wenn der vollständige Ver-band von der Form 2U ist für eine Grundmenge U ist, und wenn jede Funktion fi auskonstanten Mengen und Variablen alleine mithilfe der Operationen ∪ und ∩ aufge-baut ist. Dies liegt daran, dass in diesem Fall Enthaltensein eines Elements u ∈ Uin den Ergebnismengen für die Unbekannten xi unabhängig ist vom Enthaltenseinjedes anderen Elements u′ in diesen Mengen. Für welche Variablen xi das Elementu in dem Ergebnis für xi enthalten ist, kann deshalb über dem vollständigen Verband2{u} der Höhe 1 ausgerechnet werden und benötigt dort gerade n Iterationen. In-dem wir anstelle des vollständigen Verbands 2{u} bei der Round-Robin-Iteration denVerband 2U verwenden, führen wir gewissermaßen die Round-Robin-Iterationen fürjedes Element u ∈ U parallel aus.

Diese Abschätzungen betreffen nur den schlimmsten Fall. Bei geeigneter Anord-nung der Variablen wird die kleinste Lösung oft bereits mit weit weniger Iterationenerreicht.

Wir fragen uns, ob die neue Iterationsstrategie ebenfalls die kleinste Lösung lie-fert, wenn die naive Fixpunktiteration die kleinste Lösung geliefert hätte. Nehmenwir dazu an, die Funktionen fi seien sämtlich monoton. Sei y(d)

i die i-te Komponente

von Fd ⊥ und x(d)i der Wert von xi nach der d-ten Ausführung der do-while-Schleife

der Round-Robin-Iteration. Für alle i = 1, . . . , n und d ≥ 0 zeigen wir die folgendenAussagen:

Page 36: œbersetzerbau: Band 3: Analyse und Transformation

26 1 Grundlagen und intraprozedurale Optimierung

1. y(d)i � x(d)

i � zi für jede Lösung (z1, . . . , zn) des Ungleichungssystems;2. terminiert die Round-Robin-Iteration, dann enthalten nach der Terminierung die

Variablen x1, . . . , xn die kleinste Lösung des Ungleichungssystems;3. y(d)

i � x(d)i .

Die Aussage (1) zeigt man mithilfe vollständiger Induktion. Wegen der ersten Aus-sage liegen alle Approximationen x(d)

i unterhalb des Werts der kleinsten Lösung fürdie Unbekannte xi. Terminiert die Round-Robin-Iteration nach der Runde d, dannerfüllen die Werte x(d)

i das Gleichungssystem und sind damit eine Lösung. Wegen(1) bilden sie sogar die kleinste Lösung. Damit folgt die Behauptung (2).

Aus der Aussage (1) können wir zusätzlich folgern, dass die Round-Robin-Iteration nach der d-ten Runde mindestens so größe Werte liefert wie die naiveFixpunktiteration. Terminiert deshalb die naive Fixpunktiteration nach der Runded, würde auch die Round-Robin-Iteration spätestens nach d Runden terminieren.

Wir schließen, dass die Round-Robin-Iteration niemals schlechter als die naiveFixpunktiteration ist. Nichtsdesoweniger kann auch die Round-Robin-Iteration mehroder weniger geschickt durchgeführt werden: tatsächlich hängt ihre Effizienz we-sentlich von der Anordnung ab, in der die Variablen durchlaufen werden.

Günstig ist es, wenn eine Variable xi, von der eine andere Variable xj abhängt,vor dieser neu ausgewertet wird. Im Falle eines Ungleichungssystems ohne zykli-sche Variablenabhängigkeiten können wir so sogar erreichen, dass bereits nach ei-nem Durchlauf der do-while-Schleife die kleinste Lösung erreicht wird.

Beispiel 1.5.7 Betrachten wir erneut das Ungleichungssystem zur Berechnung derverfügbaren Ausdrücke für das Fakultätsprogramm aus Beispiel 1.4.3. Abbildung1.8 zeigt eine günstige und eine ungünstige Anordnung der Unbekannten. Im ungün-

Günstig:

3

2

4

5

x ← x − 1

y ← x ∗ y

0

1

y ← 1

NonZero(x > 1)Zero(x > 1)

Ungünstig:

0

5

4

3

2

1

x ← x − 1

y ← x ∗ y

y ← 1

NonZero(x > 1)Zero(x > 1)

Abb. 1.8. Eine günstige und eine ungünstige Anordnung der Unbekannten.

stigen Fall benötigen wir für dieses Programm immerhin vier Iterationen (Abb. 1.9).��

Page 37: œbersetzerbau: Band 3: Analyse und Transformation

1.6 Kleinste Lösung oder MOP–Lösung? 27

1 2 3 4 50 {y ← 1} {y ← 1} ∅ ∅1 {y ← 1} {y ← 1} {y ← 1} ∅2 ∅ ∅ ∅ ∅ dito3 {y ← 1} {y ← 1} ∅ ∅4 {y ← 1} ∅ ∅ ∅5 ∅ ∅ ∅ ∅

Abb. 1.9. Die Round-Robin-Iteration für die ungünstige Anordnung aus Abb. 1.8.

1.6 Kleinste Lösung oder MOP–Lösung?

Im letzten Abschnitt haben wir Verfahren kennen gelernt, um kleinste Lösungen vonUngleichungssystemen zu ermitteln. Was, müssen wir uns aber jetzt fragen, helfenuns diese kleinsten Lösungen wirklich?

Betrachten wir erneut einen vollständigen Verband D, wie er bei der Programm-analyse auftreten könnte, sowie ein Ungleichungssystem der Form

I [start] � d0

I [v] � [[k]]� (I [u]) k = (u, lab, v) Kante

wobei d0 ∈ D der Wert für den Startpunkt des Programms darstellt und alle ab-strakten Kanteneffekte [[k]]� : D → D monoton sind. Der generische Ansatz, für einAnalyseproblem einen vollständigen Verband auszuwählen, einen Startwert festzule-gen sowie die abstrakten Kanteneffekte durch monotone Funktionen zu beschreiben,heißt auch monotoner Analyserahmen. Mit weiteren Instanzen dieses allgemeinenAnsatzes werden wir uns in den nächsten Abschnitten ausführlich beschäftigen.

Für jeden Programmpunkt v wird der Wert:

I∗[v] =⊔{[[π ]]� d0 | π : start →∗ v}

gesucht. Die Abbildung I∗ heißt auch die Merge-Over-All-Paths-Lösung (kurz:MOP–Lösung) der Analyseaufgabe. Das Verhältnis zwischen der kleinsten Lösungdes Ungleichungssystems und der MOP–Lösung klärt der folgende Satz.

Satz 1.6.1 (Kam, Ullman 1975) Sei I die kleinste Lösung des Ungleichungssy-stems. Für jeden Programmpunkt v gilt

I [v] � I∗[v] .

Das heißt, für jeden Pfad π vom Programmstart nach v gilt:

I [v] � [[π ]]� d0 . (∗)Beweis. Wir beweisen die Aussage (∗) durch Induktion über die Länge von π . Istπ der leere Pfad, d.h. π = ε, dann gilt:

Page 38: œbersetzerbau: Band 3: Analyse und Transformation

28 1 Grundlagen und intraprozedurale Optimierung

[[π ]]� d0 = [[ε]]� d0 = d0 � I [start]

Andernfalls ist π von der Form π = π ′k für eine Kante k = (u, lab, v). NachInduktionsannahme gilt die Behauptung bereits für den kürzeren Pfad π ′, d.h.[[π ′]]� d0 � I [u]. Damit folgern wir:

[[π ]]� d0 = [[k]]� ([[π ′]]� d0)� [[k]]� (I [u]) da [[k]]� monoton ist

� I [v] da I eine Lösung ist

Damit ist die Behauptungi bewiesen. ��In gewisser Weise ist Satz 1.6.1 eine Enttäuschung: eigentlich hatten wir gehofft, diekleinste Lösung wäre identisch mit der MOP–Lösung. Stattdessen müssen wir zurKenntnis nehmen, dass die kleinste Lösung im Allgemeinen nur eine obere Schran-ke für die MOP–Lösung liefert. In vielen praktischen Fällen stimmen jedoch dieFixpunktlösung und die MOP–Lösung überein. Dies ist insbesondere der Fall, wennalle Funktionen [[k]]� distributiv sind. Eine Funktion f : D1 → D2 heißt

• distributiv, falls f (⊔

X) =⊔{ f x | x ∈ X} für alle nichtleeren Teilmengen

X ⊆ D;• strikt, falls f ⊥ = ⊥;• total distributiv, falls f distributiv und strikt ist.

Beispiel 1.6.1 Betrachten wir den vollständigen Verband D = N ∪ {∞} mit dernatürlichen Ordnung ≤. Die Funktion inc mit inc x = x + 1 ist distributiv, erhältaber nicht das kleinste Element.

Als weiteres Beispiel betrachten wir die Funktion

add : (N∪ {∞})2 → (N∪ {∞})mit add (x1, x2) = x1 + x2, wobei der vollständige Verband (N ∪ {∞})2 kompo-nentenweise angeordnet ist. Dann haben wir:

add⊥ = add (0, 0) = 0 + 0 = 0

Deshalb ist die Funktion strikt. Sie ist aber nicht distributiv, wie das folgende Gegen-beispiel belegt:

add ((1, 4) � (4, 1)) = add (4, 4) = 8�= 5 = add (1, 4) � add (4, 1)

��Beispiel 1.6.2 Betrachten wir erneut den Teilmengenverband D = 2U mit der Hal-bordnung ⊆. Für alle a, b ⊆ U ist die Funktion f mit f x = x ∩ a ∪ b distributiv,da

Page 39: œbersetzerbau: Band 3: Analyse und Transformation

1.6 Kleinste Lösung oder MOP–Lösung? 29

(⋃

X) ∩ a ∪ b =⋃{x ∩ a | x ∈ X} ∪ b

=⋃{x ∩ a ∪ b | x ∈ X}

=⋃{ f x | x ∈ X}

für jede nicht-leere Teilmenge X ⊆ D. Die Funktion f ist jedoch nur strikt, sofernb = ∅ gilt.

Ein analoges Resultat erhalten wir für den Teilmengenverband D = 2U mit derungekehrten Ordnung ⊇ und Funktionen f der Form f x = (x ∪ a) ∩ b. Für dieseHalbordnung bedeutet Distributivität nun, dass f (

⋂X) =

⋂{ f x | x ∈ X} git fürjede nicht-leere Teilmenge X ⊆ 2U . ��Tatsächlich gibt es eine genaue Charakterisierung aller distributiven Funktionen, so-fern ihr Definitionsbereich ein atomarer Verband ist. Sei A ein vollständiger Ver-band. Ein Element a ∈ A heißt atomar, falls a �= ⊥ ist und die einzigen Elementea′ ∈ A mit a′ � a die Elemente a′ = ⊥ und a′ = a sind. Der vollständige VerbandA heißt atomar, falls jedes Element d ∈ A die kleinste obere Schranke aller atomarenElemente a � d in A ist.

In dem vollständigen Verband N∪ {∞} aus Beispiel 1.6.1 ist 1 das einzige ato-mare Element. Deshalb ist dieser vollständige Verband nicht atomar. In dem Teil-mengenverband 2U , geordnet durch die Teilmengenrelation ⊆, sind die atomarenElemente gerade die einelementigen Teilmengen {u}, u ∈ U. In dem entsprechen-den Teilmengenverband mit der umgedrehten Ordnung ⊇ sind die atomaren Elemen-te durch die Mengen (U\{u}), u ∈ U, gegeben. Der folgende Satz sagt, dass füratomare Verbände distributive Funktionen eindeutig bestimmt sind durch ihre Wertefür das kleinste Element ⊥ und die atomaren Elemente.

Satz 1.6.2 Seien A und D vollständige Verbände, wobei A atomar ist. Sei A ⊆ A

die Menge der atomaren Elemente in A. Dann gilt:

1. Zwei distributive Funktionen f , g : A → D sind genau dann gleich, wennf (⊥) = g(⊥) und f (a) = g(a) für alle a ∈ A gelten.

2. Jedes Paar (d, h) mit d ∈ D und h : A → D definiert eine distributive Abbil-dung fd,h : A → D durch:

fd,h(x) = d � ⊔{h(a) | a ∈ A, a � x}, x ∈ A

Beweis. Wir zeigen nur die erste Behauptung. Sind die Funktionen f und g gleich,dann stimmen sie auch auf ⊥ und den atomaren Elementen von A überein. Für dieumgekehrte Richtung betrachten wir ein beliebiges Element x ∈ A. Für x = ⊥ giltf (x) = g(x) nach Voraussetzung. Für x �= ⊥ ist die Menge Ax = {a ∈ A | a � x}nicht leer. Deshalb schließen wir:

f (x) = f (⊔

Ax)=

⊔{ f (a) | a ∈ A, a � x}=

⊔{g(a) | a ∈ A, a � x} = g(x)

was zu beweisen war. ��

Page 40: œbersetzerbau: Band 3: Analyse und Transformation

30 1 Grundlagen und intraprozedurale Optimierung

Beachten Sie, dass jede distributive Funktion f : D1 → D2 automatisch bereitsmonoton ist. Es gilt nämlich a � b genau dann, wenn a � b = b gilt. Falls a � bgilt, dann gilt

f b = f (a � b) = f a � f b

Folglich gilt f a � f b, was zu zeigen war. ��Für Programmanalysen mit distributiven Kanteneffekten finden wir:

Satz 1.6.3 (Kildall 1972) Sei jeder Programmpunkt v vom Startpunkt des Programmsaus erreichbar. Seien weiterhin sämtliche Kanteneffekte [[k]]� : D → D distributiv.Dann stimmt die kleinste Lösung I des Ungleichungssystems mit der MOP–LösungI∗ überein, d.h.

I∗[v] = I [v]

für alle Programmpunkte v.

Beweis. Wegen Satz 1.6.1 genügt es zu zeigen, dass I [v] � I∗[v] gilt für allev. Da I die kleinste Lösung des Ungleichungssystems ist, reicht es nachzuweisen,dass unter den gegebenen Voraussetzungen I∗ ebenfalls eine Lösung ist, d.h. alleUngleichungen erfüllt. Für den Startpunkt start des Programms gilt:

I∗[start] =⊔{[[π ]]� d0 | π : start →∗ start} � [[ε]]� d0 � d0

Für jede Kante k = (u, lab, v) überprüfen wir:

I∗[v] =⊔{[[π ]]� d0 | π : start →∗ v}

� ⊔{[[π ′k]]� d0 | π ′ : start →∗ u}=

⊔{[[k]]� ([[π ′]]� d0) | π ′ : start →∗ u}= [[k]]� (

⊔{[[π ′]]� d0 | π ′ : start →∗ u})= [[k]]� (I∗[u])

Dabei gilt die vorletzte Gleichung, weil die Menge {π ′ | π ′ : start →∗ u} allerPfade vom Startpunkt start nach u nicht-leer und der abstrakte Kanteneffekt [[k]]�distributiv ist. Wir folgern, dass I∗ sämtliche Ungleichungen erfüllt. Damit ist dieBehauptung bewiesen. ��Das folgende Beispiel zeigt, dass in Satz 1.6.3 nicht auf die Voraussetzung, dass alleProgrammpunkte auch wirklich erreichbar sind, verzichtet werden kann.

Beispiel 1.6.3 Betrachten wir den Kontrollflussgraphen aus Abbildung 1.10. Als

0 1 2inc7

Abb. 1.10. Ein Kontrollflussgraph zur Erreichbarkeit.

Page 41: œbersetzerbau: Band 3: Analyse und Transformation

1.6 Kleinste Lösung oder MOP–Lösung? 31

vollständigen Verband wählen wir D = N∪ {∞} mit der natürlichen Ordnung ≤.Als einzigen Kanteneffekt nehmen wir die distributive Funktion inc. Dann gilt z.B.für einen beliebigen Anfangswert am Startpunkt des Programms:

I [2] = inc (I [1])= inc 0= 1

Auf der anderen Seite haben wir:

I∗[2] =⊔ ∅ = 0

da es keinen Pfad vom Startpunkt 0 nach Programmpunkt 2 gibt. Die MOP–Lösungist hier folglich verschieden von der kleinsten Lösung. ��Die Voraussetzung, dass alle Programmpunkte erreichbar sind, ist jedoch unkritisch:unerreichbare Programmpunkte können wir stets leicht identifizieren und dann weg-lassen, ohne die Semantik des Programms zu verändern.

Fazit 1.6.1 Wir fassen noch einmal unsere bisherigen Ergebnisse zusammen undwenden sie auf die Analyse der Verfügbarkeit von Zuweisungen an.

• Sind alle Kanteneffekte distributiv, ist die MOP–Lösung mit der kleinsten Lö-sung des zugehörigen Ungleichungssystems identisch.

• Sind die Kanteneffekte nur monoton, liefert jede Lösung des Ungleichungssy-stems zumindest eine obere Schranke für die MOP–Lösung.

• Sind alle aufsteigenden Ketten in dem vollständigen Verband, in dem wir rech-nen, endlich, können wir Round-Robin-Iteration anwenden, um die kleinste Lö-sung des Ungleichungssystems zu berechnen.

Bei unserer bisherigen Anwendung, der Analyse der Verfügbarkeit von Zuweisungenist der vollständige Verband D = 2Ass ein endlicher Teilmengenverband mit derOrdnung ⊇, und die abstrakten Kanteneffekte [[k]]� sind Funktionen f der Form

f x = x\b ∪ a = (x ∪ a) ∩ (b ∪ a)

für b = Ass\b. In Beispiel 1.6.2 haben wir gesehen, dass alle diese Funktionen distri-butiv sind. Wir schließen, dass Round-Robin-Iteration für unser Ungleichungssystemdie MOP-Lösung für unser Analyseproblem berechnet – sofern alle Programmpunk-te vom Startpunkt des Programms erreichbar sind. ��Damit könnten wir den Abschnitt über die Vermeidung von Mehrfachberechnungenabschließen. Unsere Transformation hat nur einen Schönheitsfehler. Zwar konntenwir einige Mehrfachberechnungen des selben Ausdrucks beseitigen, die Beseitigungersetzte diese Berechnungen jedoch durch Umspeicherungen zwischen Variablen. Invielen Fällen ist diese Umspeicherung jedoch überflüssig. Im weiteren Verlauf diesesBuchs werden wir Techniken kennen lernen, um solche möglicherweise eingeführtenIneffizienzen ebenfalls zu beseitigen.

Page 42: œbersetzerbau: Band 3: Analyse und Transformation

32 1 Grundlagen und intraprozedurale Optimierung

1.7 Beseitigung von Zuweisungen an tote Variablen

Bisher haben wir erst eine einzige optimierende Transformation kennen gelernt. Sieersetzt die Neuberechnung von Ausdrücken durch das Nachschlagen ihrer Werte,sofern ihr Wert in einer Variable sicher verfügbar ist. Dies führte zu einem genaue-ren Studium der operationellen Semantik von Programmen und der vollständigenVerbände. Nun wollen wir diese Kenntnisse zur Konstruktion anderer optimierenderTransformationen und Analysen anwenden.

Beispiel 1.7.1 Betrachten wir das folgende Beispiel:

0 : x ← y + 2;1 : y ← 5;2 : x ← y + 3;

Der Wert der Programmvariablen x an den Programmpunkten 0 und 1 ist nicht vonBedeutung. Er wird überschrieben, bevor er benutzt werden kann. Die Variable xnennen wir deshalb an diesen Programmpunkten tot. Weil es auf den Wert der Varia-blen x vor der zweiten Zuweisung an x nicht ankommt, kann die erste Zuweisung anx wegfallen. Diese Zuweisung nennen wir deshalb auch tot. Diese Idee wollen wirim Folgenden präzisieren. ��Nehmen wir an, nach der Programmausführung würden noch die Werte der Variablenaus einer gegebenen Menge X ⊆ Vars benötigt. Diese Menge X könnte leer sein,wenn alle Variablen nur innerhalb des von uns gegenwrtig analysierten Programmsverwendet werden. Die Methoden wird man aber auch auf einzelne Prozedurrümpfeanwenden. Am Endpunkt eines Prozedurrumpfs wird jedoch nicht notwendigerweisedas gesamte Programm verlassen, weshalb auf die Werte global sichtbarer Variablenmöglicherweise auch später noch zugegriffen werden kann. In diesem Fall sollte dieMenge X als die Menge der globalen Variablen definiert werden.

Wir nennen die Variable x lebendig (relativ zu X) entlang des Pfads π zum Pro-grammende, falls x ∈ X und π keine Definition von x enthält, oder es mindestenseine Benutzung von x in π gibt und die erste Benutzung von x nicht hinter der erstenDefinition von x liegt, d.h. π lässt sich zerlegen in π = π1 k π2 so dass die Kante keine Benutzung der Variablen x ist und das Anfangstück π1 keine Definition von xenthält.

Die Mengen der an einer Kante mit Beschriftung lab benutzten bzw. definiertenVariablen sind dabei gegeben durch:

lab benutzt definiert

; ∅ ∅NonZero(e) Vars(e) ∅Zero(e) Vars(e) ∅x ← e Vars(e) {x}x ← M[e] Vars(e) {x}M[e1] ← e2 Vars(e1) ∪ Vars(e2) ∅

Page 43: œbersetzerbau: Band 3: Analyse und Transformation

1.7 Beseitigung von Zuweisungen an tote Variablen 33

Hier bezeichnet Vars(e) die Menge der Programmvariablen, die in dem Ausdruck evorkommen.

Eine Variable x, die nicht lebendig entlang π relativ zu X ist, nennen wir auch totentlang π relativ zu X. Die Variable x nennen wir (möglicherweise) lebendig an demProgrammpunkt v relativ zu einer Menge X, falls x lebendig ist relativ zu X entlangzumindest eines Pfads, der an v startet und an stop endet. Andernfalls nennen wir xam Programmpunkt v tot relativ zu X.

Ob eine Variable möglicherweise lebendig oder bestimmt tot ist, hängt somit vonden möglichen Fortsetzungen der Programmausführung ab. Im Gegensatz dazu hängtdie Verfügbarkeit von Zuweisungen von den bisher durchgeführten Programmschrit-ten ab.

Beispiel 1.7.2 Betrachten wir das Programm aus Abbildung 1.11. In dem Beispiel

10 2 3

x ← y + 2 y ← 5 x ← y + 3

Abb. 1.11. Kleines Beispiel zur Lebendigkeit von Variablen.

haben wir angenommen, dass am Programmende alle Variablen tot sind. Weil es vonjedem Programmpunkt aus nur einen Pfad zum Programmende gibt, lassen sich fürjeden Programmpunkt die Menge der dort lebendigen bzw. toten Variablen leichtbestimmen. Für die einzelnen Programmpunkte ergibt sich:

lebendig tot

0 {y} {x}1 ∅ {x, y}2 {y} {x}3 ∅ {x, y}

��Wie berechnet man für jeden Programmpunkt die Menge der dort möglicherweiselebendigen Variablen? Im Prinzip gehen wir genauso vor, wie bei der Berechnungder verfügbaren Zuweisungen. Wir definieren für jede Kante k = (u, lab, v) ihrenabstrakten Effekt als eine Funktion [[k]]�, die aus der Menge der hinter der Kante,also an v lebendigen Variablen die Menge der vor der Kante also an u lebendigenVariablen konstruiert.

Als Menge der möglichen Werte betrachten wir L = 2Vars. Für eine Kante k =(u, lab, v) hängt dieser abstrakte Effekt wieder nur von ihrer Beschriftung lab ab,d.h. [[k]]� = [[lab]]�, wobei

Page 44: œbersetzerbau: Band 3: Analyse und Transformation

34 1 Grundlagen und intraprozedurale Optimierung

[[;]]� L = L[[NonZero(e)]]� L = [[Zero(e)]]� L = L ∪ Vars(e)[[x ← e]]� L = (L\{x}) ∪ Vars(e)[[x ← M[e]]]� L = (L\{x}) ∪ Vars(e)[[M[e1] ← e2]]� L = L ∪ Vars(e1) ∪ Vars(e2)

Die abstrakten Kanteneffekte [[k]]� können wir wieder zu den Effekten [[π ]]� von Pfa-den π = k1 . . . kr zusammensetzen. Dazu definieren wir:

[[π ]]� = [[k1]]� ◦ . . . ◦ [[kr]]�

Die Reihenfolge der Kanten bleibt hier in der Funktionskomposition erhalten. Diesliegt daran, dass die Funktion [[π ]]� beschreibt, wie man aus einer Menge L vonlebendigen Variablen nach π die Menge der lebendigen Variablen vor π ermittelt.

Sei X die Menge der Variablen, die nach der Programmausführung lebendig sind.Die Menge der an einem Programmpunkt v lebendigen Variablen ergibt sich als dieVereinigung der Mengen von Variablen, die entlang irgendeines Pfads π von v zumProgrammende relativ zu X lebendig sind, d.h. als eine Vereinigung der Mengen[[π ]]� X. Entsprechend definieren wir:

L∗[v] =⋃{[[π ]]� X | π : v →∗ stop}

wobei v →∗ stop die Menge aller Pfade von v zum Programmende stop bezeichnet.Zweckmäßigerweise wählen wir darum für die Menge L als Ordnungsrelation dieTeilmengenbeziehung ⊆. Die Abbildung L∗ repräsentiert erneut die MOP-Lösungunseres Analyseproblems. Programmanalysen, bei denen der Wert an einem Pro-grammpunkt von den Pfaden abhängt, die den Programmpunkt vom Startpunkt auserreichen, nennt man Vorwärtsanalysen. Programmanalysen wie die Lebendigkeitvon Variablen, bei denen der interessierende Wert an einem Programmpunkt dage-gen davon abhängt, wie man von ihm aus das Programmende erreichen kann, nenntman dagegen Rückwärtsanalysen.

Transformation DE:

Nehmen wir an, wir hätten die Mengen L∗ gegeben. Für jeden Programmpunkt venthält das Komplement der Menge L∗[v] die an v sicher, d.h. entlang jedes Pfadstoten Variablen. Zuweisungen an diese Variablen sind überflüssig und können mitden folgenden Transformationsregeln beseitigt werden:

;

v v

x �∈ L∗[v]x ← e

Analog zu Zuweisungen könnte man ebenfalls Speicherzugriffe wegoptimieren,wenn ihr Ergebnis nicht benötigt wird. In vielen praktischen Programmiersprachen

Page 45: œbersetzerbau: Band 3: Analyse und Transformation

1.7 Beseitigung von Zuweisungen an tote Variablen 35

ist der Zugriff auf einige Adressen jedoch nicht zulässig. Tritt ein solcher Speicher-fehler trotzdem auf, bewirkt er Seiteneffekte wie das Auslösen einer Ausnahme odergar einen Programmabbruch. Um die Semantik des Programms zu erhalten, darf derOptimierer diese Effekte nicht einfach beseitigen.Die Transformation DE heißt auch Beseitigung toten Codes oder Dead Code Elimi-nation. Die Korrektheit dieser Transformation zeigt man wieder in zwei Schritten:

1. Man zeigt, dass die abstrakten Effekte für die Kanten die Definition der Leben-digkeit korrekt implementieren;

2. Man zeigt, dass die Anwendung der Transformationsregel semantik-erhaltendist.

Wir beschäftigen uns hier wieder nur mit dem zweiten Punkt. Wir überlegen uns, dasses nicht darauf ankommt, dass die Werte jeder Variablen an jedem Programmpunktvor und nach der Transformation identisch sind. Vielmehr reicht es, wenn das beob-achtbare Verhalten der beiden Programme identisch ist. Als potentiell beobachtbarbetrachten wir einerseits die Folge der durchlaufenen Programmpunkte, andererseitsdie Werte der Variablen aus der Menge X am Ende der Programmausführung sowiealles, was im Speicher steht. Unsere Behauptung zu Punkt (2) lautet deshalb, dassder Wert einer toten Variablen das weitere beobachtbare Verhalten nicht beeinflusst.Dazu betrachten wir einen Zustand s und eine Berechnung des Programms π , die imZustand s startet und zum Programmende führt. Mit Induktion über die Länge derBerechnung π zeigen wir:

(L) Sei s′ ein Zustand, der sich von s nur in den Werten toter Variablen unter-scheidet. Dann ist die Transformation [[π ]] auch für s′ definiert, und für allePräfixe π ′ von π stimmen die Zustände [[π ′]] s and [[π ′]] s′ bis auf die Wertetoter Variablen überein.

Aus der Invariante (L) schließen wir, dass zwei Zustände an einem Programmpunktv sicher zu gleichem Verhalten führen, wenn sie sich nur in den Werten am Pro-grammpunkt v toter Variablen unterscheiden. Zur Korrektheit der Transformationgenügt es es damit zu zeigen, dass sich die Zustände des ursprünglichen und destransformierten Programms jeweils nur in den Werten toter Variablen unterscheiden.

Zur Berechnung der Menge L∗[u] der am Programmpunkt u möglicherweiselebendigen Variablen gehen wir analog vor wie bei der Bestimmung der sicher ver-fügbaren Ausdrücke; wir stellen ein geeignetes Ungleichungssystem auf. Nun habenwir jedoch einen Startwert nicht für den Programmpunkt start, sondern für das Pro-grammende stop. Wir nehmen an, dass nach Ende der Programmausführung nur Va-riablen aus der Menge X möglicherweise lebendig sind. Jede Kante k = (u, lab, v)liefert eine Ungleichung, die nicht den Beitrag entlang dieser Kante für den Pro-grammpunkt v beschreibt wie bei einer Vorwärtsanalyse – sondern den Beitrag fürden Programmpunkt u. Wir erhalten das folgende Ungleichungssystem:

L[stop] ⊇ XL[u] ⊇ [[k]]� (L[v]) k = (u, lab, v) Kante

Page 46: œbersetzerbau: Band 3: Analyse und Transformation

36 1 Grundlagen und intraprozedurale Optimierung

Die Vertauschung der Rollen von start und stop sowie die Vertauschung von Start-und Zielpunkt für jede Kante machen den Unterschied in den Ungleichungssystemenzwischen Vorwärts- und Rückwärtsanalysen aus.

Der vollständige Verband, über dem wir dieses Ungleichungssystem lösen wol-len, ist endlich. Insbesondere werden damit alle aufsteigenden Ketten irgendwannstabil. Da die Kanteneffekte des Ungleichungssystems monoton sind, lässt sich diekleinste Lösung L durch Round-Robin-Iteration bestimmen. Da die Kanteneffektetatsächlich sogar distributiv sind, stimmt diese kleinste Lösung mit der MOP-LösungL∗ überein – sofern das Programmende stop von jedem Programmpunkt aus erreich-bar ist (vgl. Satz 1.6.3).

Beispiel 1.7.3 Wir betrachten erneut das Programm für die Fakultät, wobei wir jetztannehmen, dass Ein- und Ausgabe über Speicherzellen vermittelt wird, und zwar dieZellen M[I] bzw. M[R]. Nach Programmende sollen keine Variablen möglicherwei-se lebendig sein. Den Kontrollflussgraphen zusammen mit den für dieses Programmerzeugten Ungleichungen finden Sie in Abb. 1.12. Dieses Ungleichungssystem ent-

7

36

4

5

2

1

0

x ← M[I]

y ← 1

y ← x ∗ y

x ← x − 1

M[R] ← y

NonZero(x > 1)Zero(x > 1)

L[0] ⊇ (L[1]\{x}) ∪ {I}L[1] ⊇ L[2]\{y}L[2] ⊇ (L[6] ∪ {x}) ∪ (L[3] ∪ {x})L[3] ⊇ (L[4]\{y}) ∪ {x, y}L[4] ⊇ (L[5]\{x}) ∪ {x}L[5] ⊇ L[2]L[6] ⊇ L[7] ∪ {y, R}L[7] ⊇ ∅

Abb. 1.12. Das Ungleichungssystem zur Lebendigkeit von Variablen für das Fakultätspro-gramm.

spricht im wesentlichen dem Kontrollflussgraphen. In der Praxis wird man das Un-gleichungssystem daher nicht unbedingt aufbauen. Man merkt sich für jede Kantedie zugehörigen Kanteneffekte. Nur durch den Austausch dieser Funktionen kannso ein generischer Fixpunktiterierer für ein Programm unterschiedliche Analysendurchführen.

Die Round-Robin-Iteration für das Ungleichungssystem liefert (bei geeigneterReihenfolge der Unbekannten) bereits nach der ersten Runde die kleinste Lösung:

Page 47: œbersetzerbau: Band 3: Analyse und Transformation

1.7 Beseitigung von Zuweisungen an tote Variablen 37

1 27 ∅6 {y, R}2 {x, y, R} dito5 {x, y, R}4 {x, y, R}3 {x, y, R}1 {x, R}0 {I, R}

Wir stellen fest, dass bei keiner Zuweisung des Fakultätsprogramms die linke Seitetot ist. Folglich modifiziert die Transformation DE dieses Programm nicht. ��Die Beseitigung von Zuweisungen an tote Variablen kann möglicherweise weitereVariablen als tot erscheinen lassen. Dies zeigt das Beispiel in Abb. 1.13.

2

1

4

3

2

3

1

4

2

3

1

4

{y, R}

{y, R}

{y, R}

;

;

;

M[R] ← yM[R] ← y

x ← y + 1

M[R] ← y

z ← 2 ∗ x

x ← y + 1

{y, R}

{x, y, R}

{y, R}

Abb. 1.13. Wiederholte Anwendung der Transformation DE.

In diesem Beispiel wird die Schwäche der Lebendigkeitsanalyse sichtbar: Sieklassifiziert eventuell Variablen als lebendig wegen Benutzungen, die in Zuweisun-gen an tote Variablen auftreten. Eine Entfernung einer solchen Zuweisung und einenachfolgende Neuanalyse fände heraus, dass weitere Variablen tot sind. Ein Pro-gramm mehrmals zu analysieren in der Hoffnung, nach und nach weitere Möglich-keiten zur Optimierung zu entdecken, ist aber ineffizient und deshalb unbefriedi-gend. Dies kann man umgehen, wenn man restriktivere Bedingungen für die Leben-digkeit von Variablen einzuführt. Der neue Begriff, echte Lebendigkeit, verwendetden Hilfsbegriff der echten Benutzung einer Variablen relativ zu einem an dem Pro-grammpunkt beginnenden Pfad. Mit diesem Begriff versucht man, Benutzungen inZuweisungen an tote Variable loszuwerden. Allerdings wird die Definition der echtenLebendigkeit dadurch rekursiv. Echte Lebendigkeit hängt ab von echter Benutzung,die wiederum durch echte Lebendigkeit definiert ist.

Nehmen wir wieder an, am Programmende würden die Werte der Variablen ausder Menge X noch benötigt. Dann nennen wir eine Variable x echt lebendig entlang

Page 48: œbersetzerbau: Band 3: Analyse und Transformation

38 1 Grundlagen und intraprozedurale Optimierung

eines Pfads π zum Programmende, falls x ∈ X und π keine Definition von x enthältoder falls π eine echte Benutzung von x enthält, vor der keine Definition von x liegt,d.h. π lässt sich in π = π1 k π2 zerlegen, so dass π1 keine Definition von x enthältund k eine echte Benutzung von x ist relativ zu π2. Die Menge der an einer Kantek = (u, lab, v) echt benutzten Variablen relativ zu einem Pfad π ′ ist dabei gegebendurch:

lab y echt benutzt

; ∅NonZero(e) y ∈ Vars(e)Zero(e) y ∈ Vars(e)x ← e y ∈ Vars(e) ∧ x ist echt lebendig

x ← M[e] y ∈ Vars(e) ∧ x ist echt lebendig

M[e1] ← e2 y ∈ Vars(e1) ∨ y ∈ Vars(e2)

Die Zusatzbedingung bei Zuweisungen und Ladeoperationen ist, dass die linke Seiteselbst echt lebendig sein muss. Diese Zusatzbedingung macht den einzigen Unter-schied zur normalen Lebendigkeit aus.

Beispiel 1.7.4 Betrachten Sie das Programm aus Abb. 1.14. Die Variable z ist am

2

3

1

4

2

3

1

4

{y, R}

{y, R}

{y, R};

;

M[R] ← yM[R] ← y

z ← 2 ∗ x

x ← y + 1

Abb. 1.14. Echt lebendige Variablen.

Programmpunkt 2 nicht lebendig (auch nicht echt lebendig). Somit sind die Variablenauf der rechten Seite der zugehörigen Zuweisung (hier: x) auch nicht echt benutzt.Weil x nicht echt benutzt wird, ist daher x auch am Programmpunkt 1 nicht echtlebendig. ��Die abstrakten Kanteneffekte für echte Lebendigkeit sehen so aus:

Page 49: œbersetzerbau: Band 3: Analyse und Transformation

1.7 Beseitigung von Zuweisungen an tote Variablen 39

[[;]]� L = L[[NonZero(e)]]� L = [[Zero(e)]]� L = L ∪ Vars(e)[[x ← e]]� L = (L\{x}) ∪ ((x ∈ L) ? Vars(e) : ∅)[[x ← M[e]]]� L = (L\{x}) ∪ ((x ∈ L) ? Vars(e) : ∅)[[M[e1] ← e2]]� L = L ∪ Vars(e1) ∪ Vars(e2)

Für ein Element x und Mengen a, b, c bezeichnet dabei der bedingte Ausdruck (x ∈a) ? b : c die Menge:

(x ∈ a) ? b : c =

{b falls x ∈ ac falls x �∈ a

Die abstrakten Kanteneffekte für echte Lebendigkeit sind komplizierter als für leben-dige Variablen. Interessanterweise sind sie aber nichtsdestoweniger distributiv. Diesliegt daran, dass auch der neue Bedingungsoperator distributiv ist, sofern c ⊆ b gilt.Um uns davon zu überzeugen, betrachten wir für einen beliebigen MengenverbandD = 2U mit der Teilmengenbeziehung ⊆ als Ordnungsrelation die Funktion:

f y = (x ∈ y) ? b : c

Dann rechnen wir für eine beliebige nicht-leere Menge Y ⊆ 2U nach:

f (⋃

Y) = (x ∈ ⋃Y) ? b : c

= (∨{x ∈ y | y ∈ Y}) ? b : c

= c ∪ ⋃{(x ∈ y) ? b : c | y ∈ Y}= c ∪ ⋃{ f y | y ∈ Y}

Aus Satz 1.6.2 können wir allgemeiner folgern:

Satz 1.7.1 Sei U eine endliche Menge und f : 2U → 2U eine Funktion.

1. Genau dann gilt f (x1 ∪ x2) = f (x1) ∪ f (x2) für alle x1, x2 ⊆ U, wenn sich fin der folgenden Form darstellen lässt:

f (x) = b0 ∪ ((u1 ∈ x) ? b1 : ∅) ∪ · · · ∪ ((ur ∈ x) ? br : ∅)für geeignete ui ∈ U und bi ⊆ U.

2. Ganau dann gilt f (x1 ∩ x2) = f (x1) ∩ f (x2) für alle x1, x2 ⊆ U, wenn sich fdarstellen lässt in der Form:

f (x) = b0 ∩ ((u1 ∈ x) ? U : b1) ∩ · · · ∩ ((ur ∈ x) ? U : br)

für geeignete ui ∈ U und bi ⊆ U. ��Beachten Sie, dass die Funktionen aus Satz 1.7.1 insbesondere unter Kompositi-on, kleinsten oberen Schranken und kleinsten unteren Schranken abgeschlossen sind(Aufg. 11).

Page 50: œbersetzerbau: Band 3: Analyse und Transformation

40 1 Grundlagen und intraprozedurale Optimierung

Weil die abstrakten Kanteneffekte für echte Lebendigkeit distributiv sind, stimmtdie kleinste Lösung des betreffenden Ungleichungssystems mit der zugehörigenMOP-Lösung überein – sofern das Programmende stop von jedem Programmpunktaus erreichbar ist.

Interessanterweise findet die Analyse der echten Lebendigkeit mehr überflüssigeZuweisungen als wiederholte Analyse der Lebendigkeit allein.

Beispiel 1.7.5 Abbildung 1.15 zeigt eine Schleife, in der eine Variable modifiziert

Lebendigkeit:

;

{x} x ← x − 1

Echte Lebendigkeit:

;

∅ x ← x − 1

Abb. 1.15. Echte Lebendigkeit in Schleifen.

wird, die nur in der Schleife selbst benutzt wird. Die Analyse der Lebendigkeit istnicht in der Lage, die Nutzlosigkeit der Variable zu identifizieren, die Analyse derechten Lebendigkeit dagegen schon. ��

1.8 Beseitigung von Zuweisungen zwischen Variablen

Programme enthalten oft Kopierinstruktionen, die den Inhalt einer Variable in eineandere kopieren. Oft sind diese Kopierinstruktionen die Überreste anderer Optimie-rungen oder Phasen des Übersetzungsprozesses.

Beispiel 1.8.1 Betrachten Sie das Programm aus Abb. 1.16. Im gegebenen Fall istdie Zwischenspeicherung in der Variablen T nutzlos, da der Wert des Ausdrucksnur genau einmal benutzt wird. Statt der Variablen y könnten wir allerdings direktdie Variable T verwenden, da diese den gleichen Wert enthält. Dann ist die Variabley am Programmpunkt 2 tot, und wir können die Zuweisung an y eliminieren. ImErgebnisprogramm gibt es dann zwar immer noch die Variable T; dafür haben wirdie Variable y beseitigt. ��Für eine solche Transformation müssen wir wissen, wie der Wert eines Ausdrucksdurch Kopien von Variablen verbreitet wird. Eine solche Analyse heißt deshalb auchPropagation von Kopien oder copy propagation. Betrachten wir eine Variable x. Anjedem Programmpunkt wird eine Menge von Variablen verwaltet, die mit Sicherheitebenfalls den aktuellen Wert der Variablen x enthalten. Wir sagen auch, Kopien vonx werden fortgeschaltet oder propagiert. Dann kann die Benutzung einer solchenVariable durch eine Benutzung der Variablen x ersetzt werden. Sei V = {V ⊆

Page 51: œbersetzerbau: Band 3: Analyse und Transformation

1.8 Beseitigung von Zuweisungen zwischen Variablen 41

2

3

1

4

2

3

1

4

2

3

1

4

;

M[R] ← y

y ← T

T ← x + 1 T ← x + 1

y ← T

M[R] ← T

T ← x + 1

M[R] ← T

Abb. 1.16. Ein Programm mit Umspeicherungen.

Vars | x ∈ V} der vollständige Verband aller Mengen von Programmvariablen, die xenthalten, geordnet durch die Obermengenrelation ⊇. Am Startpunkt des Programmsenthält nur die Variable x selbst mit Sicherheit ihren eigenen Wert. Deshalb liegt dortdie Menge V0 = {x} vor. Die abstrakten Kanteneffekte hängen wieder nur von derKantenbeschriftung ab. Wir definieren:

[[x ← e]]� V = {x}[[x ← M[e]]]� V = {x}[[z ← y]]� V = V ∩ ((y ∈ V) ? Vars : (Vars\{z})) falls x �≡ z, y ∈ Vars[[z ← r]]� V = V\{z} falls x �≡ z, r �∈ Vars

Hinter der Zuweisung x ← e oder dem Lesen im aus dem Speicher x ← M[e]enthält neben x keine andere Variable mit Sicherheit den Wert aktuellen Wert von x.Die anderen beiden Fälle behandeln Zuweisungen an Variablen z, die von x verschie-den sind. Für alle anderen Kantenbeschriftungen ändert der abstrakte Kanteneffektdie abstrakte Information nicht. Das Ergebnis der Analyse für das Programm ausBeispiel 1.8.1 und die Variable T zeigt Abb. 1.17. Beachten Sie, dass die Informati-

2

3

1

4

T ← x + 1

y ← T

M[R] ← y

{y, T}

{y, T}

{T}

Abb. 1.17. Die Variablen in Beispiel 1.8.1, die den gleichen Wert wie T haben.

Page 52: œbersetzerbau: Band 3: Analyse und Transformation

42 1 Grundlagen und intraprozedurale Optimierung

on vorwärts durch den Kontrollflussgraphen propagiert wird. Wegen Satz 1.7.1 sindalle Kanteneffekte distributiv. Wir schließen, dass auch für diese Analyse das Lö-sen des entsprechenden Ungleichungssystems die MOP–Lösung liefert. Sei Vx dieseLösung. Gemäß unserer Konstruktion folgt aus z ∈ Vx[u], dass z den gleichen Wertenthält wie die Variable x. Deshalb können wir die Zugriffe auf z durch Zugriffe aufx ersetzen. Wir definieren dazu die Substitution V [u]−:

V [u]− z =

{x falls z ∈ Vx[u]z sonst

Damit erhalten wir die nächste Transformation.

Transformation CE:

u u

NonZero(e) NonZero(V [u]−(e))

... analog für Kanten mit Zero (e)

u u

z ← V [u]−(e)z ← e

u u

x ← M[e] x ← M[V [u]−(e)]

u u

M[e1] ← e2 M[V [u]−(e1)] ←V [u]−(e2)

Dabei bezeichnet V−(e) die Anwendung der Substitution V− auf den Ausdruck e.

Page 53: œbersetzerbau: Band 3: Analyse und Transformation

1.9 Konstantenfaltung 43

Beispiel 1.8.2 Es wird Zeit, dass wir uns ein geringfügig größeres Beispiel ansehen,um das Zusammenwirken der verschiedenen Transformationen zu beobachten. InBeispiel 1.4.2 betrachteten wir die Implementierung der Anweisung a[7]−−; in un-serer Beispielsprache und zeigten, wie die zweite Berechnung des Ausdrucks A + 7durch einen Zugriff auf die Variable A1 ersetzt werden konnte. Das Ergebnis der Op-timierung RE zeigt Abb. 1.18 links. Die Anwendung der Optimierung CE ersetzt die

A1 ← A + 7

B1 ← M[A1 ]

B2 ← B1 − 1

A2 ← A1

M[A1 ] ← B2

B1 ← M[A1 ]

A1 ← A + 7

B2 ← B1 − 1

M[A2 ] ← B2

A2 ← A1

A1 ← A + 7

B1 ← M[A1 ]

B2 ← B1 − 1

M[A1 ] ← B2

;

Abb. 1.18. Die Transformationen CE und DE für die Implementierung von a[7]−−;.

Benutzung der Variable A2 durch eine Benutzung der Variable A1. Das Ergebnis derTransformation ist der Kontrollfluss-Graph in der Mitte. Weil durch die Anwendungder Transformation CE nun die Variable A2 tot ist, wird die Zuweisung an A2 imletzten Schritt durch Anwendung der Optimierung DE eliminiert. Zum Aufräumenmuss am Ende nur die eingefügte leere Anweisung aus dem Kontrollfluss-Graphenentfernt werden. ��

1.9 Konstantenfaltung

Das Ziel der Konstantenfaltung (constant folding) ist, möglichst große Teile der Be-rechnung aus der Laufzeit in die Übersetzungszeit zu verlagern.

Beispiel 1.9.1 Betrachten Sie das kleine Programm aus Abb. 1.19. Die Variable x hatam Programmpunkt 2 stets den Wert 7. Deshalb wertet sich die Bedingung x > 0 anden ausgehenden Kanten des Programmpunkts 2 stets zu 1 aus, so dass der Speicher-zugriff immer durchgeführt wird. Folglich kann die Abfrage am Programmpunkt 2gestrichen werden (Abb. 1.20). Mit dem Streichen der Bedingung wird der else-Teilunerreichbar. In unserem Beispiel wird dadurch nichts gewonnen; möglicherweisekönnen so jedoch große Teile eines Programms beseitigt werden. ��Wir fragen uns, ob Ineffizienzen wie in Beispiel 1.9.1 in der Praxis vorkommen.Tatsächlich ergeben sich solche Programme etwa, wenn die Programmiererin be-

Page 54: œbersetzerbau: Band 3: Analyse und Transformation

44 1 Grundlagen und intraprozedurale Optimierung

x ← 7;if (x > 0)

M[A] ← B;

2

1

3

4

5;

x ← 7

M[A] ← B

NonZero (x > 0)Zero (x > 0)

Abb. 1.19. Ein Beispielprogramm zur Konstantenfaltung.

2

1

3

4

5

2

1

3

4

5

;

M[A] ← B

;

;x ← 7

NonZero (x > 0)

M[A] ← B

Zero (x > 0)

;

Abb. 1.20. Eine Optimierung des Beispielprogramms aus Abb. 1.19.

nannte Konstanten einführte, um ihr Programm leserlicher zu gestalten oder um dieKonstanten möglicherweise später leichter konsistent ändern zu können. Auch kannman sich vorstellen, dass ein und dasselbe Programm für mehrere (ähnliche) Anwen-dungen entwickelt wurde. Welche Variante des Programms jeweils zur Ausführungkommt, wird mittels Konstanten gesteuert, deren ständige Abfrage zur Laufzeit manjedoch gerne mit Hilfe des Übersetzers entfernen möchte. Weiterhin tendieren gene-rierte Programme, also Programme, die von anderen Programmen erzeugt werden,dazu, solchen scheinbar ineffizienten Code zu enthalten. Die wichtigste Beobachtungjedoch ist, dass solcher Code während des Übersetzungsprozesses entstehen kann –etwa durch die Implementierung bestimmter Sprachkonstrukte, oder als Überbleibselanderer Programmtransformationen.

Verallgemeinerungen der Konstantenfaltung bieten die verschiedenen Technikenzur partiellen Auswertung von Programmen. Partielle Auswertung führt Berechnun-gen auf statisch bekannten Teilen von Zuständen schon zur Übersetzungszeit aus.

Page 55: œbersetzerbau: Band 3: Analyse und Transformation

1.9 Konstantenfaltung 45

Hier befassen wir uns nur mit der Konstantenfaltung selbst. Unser Ziel ist eine Ana-lyse, die für jeden Programmpunkt v zwei Informationen berechnet:

1. Ist v möglicherweise erreichbar?2. Welche Werte haben die Programmvariablen bei Erreichen von v?

Diese Analyse nennen wir Konstantenpropagation (constant propagation). Den voll-ständigen Verband für diese Analyse konstruieren wir in zwei Schritten. Zuerst ent-werfen wir eine Halbordnung für die möglichen Werte von Variablen. Dazu erwei-tern wir die ganzen Zahlen um ein weiteres Element �, das einen unbekannten Wertdarstellt:

Z� = Z∪ {�} mit x � y gdw. y = � oder x = y

Diese Halbordnung zeigt Abb. 1.21. Die Halbordnung Z� selbst ist noch kein voll-

210-1-2

Abb. 1.21. Die Halbordnung Z� für Werte von Variablen.

ständiger Verband: dazu fehlt z.B. ein kleinstes Element. In einem zweiten Schrittkonstruieren wir den vollständigen Verband der abstrakten Variablenbelegungendurch

D = (Vars → Z�)⊥ = (Vars → Z

�) ∪ {⊥},

d.h. D ist die Menge aller Abbildungen von Variablen auf abstrakte Werte, erweitertum ein neues Element ⊥. Dieses neue Element markiert einen Programmpunkt, andem durch die Fixpunktiteration noch keine Belegung für die Variablen angekom-men ist. Wenn auch die berechnete Lösung an einem Programmpunkt den Wert ⊥annimmt, heißt das, dass der Punkt von keiner Programmausführung erreicht werdenkann.

Auf dieser Menge der abstrakten Zustände definieren wir eine Ordnungsrelationdurch:

D1 � D2 gdw. ⊥ = D1 oder D1 x � D2 x für alle x ∈ Vars

Die abstrakte Variablenbelegung D1 �= ⊥ ist höchstens so groß bzgl. der Halbord-nung � wie die abstrakte Variablenbelegung D2, wenn D1 alle Variablen mit höch-stens so großen Werten bzgl. der Halbordnung � auf Z� belegt. Das heißt aber, dassD1 alle Variablen, die D2 mit Werten aus Z belegt, mit den gleichen Werten belegt.D1 kennt also die genauen Werte für mindestens so viele Variablen wie D2.

Wir rechnen nach, dass D mit dieser Ordnungsrelation ein vollständiger Verbandist. Betrachten Sie dazu eine Teilmenge X ⊆ D. O.E. können wir annehmen, dass⊥ �∈ X. Dann ist X ⊆ (Vars → Z�).

Page 56: œbersetzerbau: Band 3: Analyse und Transformation

46 1 Grundlagen und intraprozedurale Optimierung

Ist X = ∅, dann ist⊔

X = ⊥ ∈ D. Folglich enthält D eine kleinste obereSchranke für X. Ist dagegen X �= ∅, dann ist die kleinste obere Schranke

⊔X = D

gegeben durch:

D x =⊔{ f x | f ∈ X} =

{z falls f x = z für alle f ∈ X� sonst

Weil damit jede Teilmenge X von D eine kleinste obere Schranke besitzt, ist D einvollständiger Verband. Zu jeder Kante k = (u, lab, v) konstruieren wir einen ab-strakten Kanteneffekt [[k]]� = [[lab]]� : D → D, welcher die konkrete Berechnungsimuliert. Dabei soll [[lab]]� ⊥ = ⊥ für alle Beschriftungen lab gelten.

Sei D �= ⊥ eine abstrakte Variablenbelegung. Zur Konstruktion der abstrak-ten Kanteneffekte benötigen wir eine abstrakte Auswertungsfunktion, die den Werteines Ausdrucks ermittelt – soweit es die Informationen in D erlauben. Bei der ab-strakten Auswertung müssen wir damit rechnen, dass für manche D und mancheAusdrücke nur der Wert � (unbekannter Wert) ermittelt werden kann. Die abstrakteAusdrucksauswertung lehnt sich an die konkrete Ausdrucksauswertung an: wir tau-schen nur die konkreten Operatoren � durch die zugehörigen abstrakten Operatoren�� aus, die mit abstrakten Werten, insbesondere mit dem Wert � umgehen können.Für binäre Operatoren � definieren wir etwa:

a �� b =

{� falls a = � oder b = �a � b sonst

Wenn eines der beiden Argumente unbekannt, also gleich � ist, soll auch das Er-gebnis der Operatoranwendung unbekannt sein. Für zwei bekannte Argumente, alsoWerte ungleich � verhält sich dagegen der abstrakte Operator wie der konkrete.

Diese Definition der abstrakten Operatoren ist die naheliegendste. Für mancheOperatoren und bestimmte Argumente kann man algebraische Gesetze ausnutzen,um bessere Informationen über eine Operatoranwendung zu bekommen. So lieferteine Multiplikation stets 0, wenn nur einer der beiden Operanden 0 ist; auf den an-deren Operanden kommt es in diesem Fall gar nicht an! Solche algebraischen Iden-titäten gibt es auch für andere Operatoren und sollten vom Übersetzer berücksichtigtwerden.

Nehmen wir an, wir hätten für jeden konkreten Operator � einen zugehörigenabstrakten Operator �� auf abstrakten Werten zur Verfügung. Dann definieren wirdie abstrakte Ausdrucksauswertung

[[e]]� : (Vars → Z�) → Z

durch:

[[c]]� D = c[[� e]]� D = �� [[e]]� D für einstellige Operatoren �

[[e1 � e2]]� D = [[e1]]� D �� [[e2]]� D für binäre Operatoren �

Page 57: œbersetzerbau: Band 3: Analyse und Transformation

1.9 Konstantenfaltung 47

Beispiel 1.9.2 Betrachten wir die abstrakte Variablenbelegung

D = {x → 2, y → �}Dann ergibt sich:

[[x + 7]]� D = [[x]]� D +� [[7]]� D= 2 +� 7= 9

[[x − y]]� D = 2 −� �= �

��Als Nächstes definieren wir die abstrakten Kanteneffekte [[k]]� = [[lab]]�. Wir setzen[[lab]]� ⊥ = ⊥, und für D �= ⊥ definieren wir:

[[;]]� D = D

[[NonZero (e)]]� D =

{⊥ falls 0 = [[e]]� DD sonst

[[Zero (e)]]� D =

{⊥ falls 0 �� [[e]]� DD falls 0 � [[e]]� D

[[x ← e]]� D = D ⊕ {x → [[e]]� D}[[x ← M[e]]]� D = D ⊕ {x → �}

[[M[e1] ← e2]]� D = D

Dabei bezeichnet der Operator ⊕ die Abänderung einer Funktion an einem Argu-ment zu einem angegebenen Wert. Weiterhin nehmen wir an, dass vor der Program-mausführung nichts über die Werte der Variablen bekannt ist. Deshalb haben wir fürden Programmpunkt start die abstrakte Variablenbelegung D� = {x → � | x ∈Vars}.

Wie wir es gewohnt sind, setzen wir die abstrakten Kanteneffekte [[k]]� zu denEffekten von Pfaden π = k1 . . . kr zusammen durch:

[[π ]]� = [[kr]]� ◦ . . . ◦ [[k1]]� : D → D

Beispiel 1.9.3 Die kleinste Lösung des Ungleichungssystems zur Analyse in unse-rem einleitenden Beispiel zeigt Abb. 1.22. ��Wie zeigen wir die Korrektheit der berechneten Information? Hier hilft die Theorieder abstrakten Interpretation weiter, wie sie von Patrick und Radhia Cousot 1977vorgeschlagen wurde. Wir präsentieren diese Theorie in einer leicht vereinfachtenForm. Die Idee besteht darin, konkrete Werte durch abstrakte Werte zu beschreiben.Zur Beschreibung wählen wir Werte aus einer Halbordnung D. Die Beziehung zwi-schen konkreten Werten und ihren Beschreibungen wird durch eine Beschreibungs-relation Δ dargestellt. Die Beschreibungsrelation Δ sollte die folgende Eigenschafthaben:

Page 58: œbersetzerbau: Band 3: Analyse und Transformation

48 1 Grundlagen und intraprozedurale Optimierung

2

1

3

4

5;

x ← 7

M[A] ← B

NonZero (x > 0)Zero (x > 0)1 {x → �}2 {x → 7}3 {x → 7}4 {x → 7}5 ⊥� {x → 7} = {x → 7}

Abb. 1.22. Die Lösung des Ungleichungssystems für Abb. 1.19.

x Δ a1 ∧ a1 � a2 ==⇒ x Δ a2

Wenn also a1 eine Beschreibung eines konkreten Werts x ist und a1 � a2 gilt, dannist auch a2 eine Beschreibung von x. Für eine solche Beschreibungsrelation könnenwir eine Konkretisierung γ definieren, die jedem abstrakten Wert a ∈ D die Mengeder von a beschriebenen konkreten Werte zuordnet:

γ a = {x | x Δ a}Ein größerer abstrakter Wert beschreibt eine Obermenge der konkreten Werte, die einkleinerer abstrakter Wert beschreibt, und stellt somit eine ungenauere Informationdar:

a1 � a2 ==⇒ γ(a1) ⊆ γ(a2)

Für die Konstantenpropagation bauen wir die Beschreibungsrelation schrittweise auf.Wir beginnen mit einer Beschreibungsrelation Δ ⊆ Z × Z� auf den Werten fürProgrammvariable. Wir definieren:

z Δ a gdw. z = a ∨ a = �Zu dieser Beschreibungsrelation gehört die Konkretisierung:

γ a =

{{a} falls a � �Z falls a = �

Die Beschreibungsrelation für Werte von Programmvariablen setzen wir fort zu einerBeschreibungsrelation zwischen konkreten und abstrakten Variablenbelegungen, diewir der Einfachheit halber wieder mit Δ bezeichnen. Diese BeschreibungsrelationΔ ⊆ (Vars → Z) × (Vars → Z�)⊥ definieren wir durch

ρ Δ D gdw. D �= ⊥ ∧ ρ x � D x (x ∈ Vars)

Page 59: œbersetzerbau: Band 3: Analyse und Transformation

1.9 Konstantenfaltung 49

Gemäß dieser Definition gibt es keine konkrete Variablenbelegung ρ mit ρ Δ ⊥.Folglich liefert auch die Konkretisierung γ für ⊥ die leere Menge. Für eine abstrakteVariablenbelegung D �= ⊥ liefert die Kontretisierung γ die Menge aller konkretenVariablenbelegungen, die für jede Variable x den Wert D x zurück liefern, soferndieser in Z liegt, und andernfalls einen beliebigen Wert:

γ D = {ρ | ∀ x : (ρ x) Δ (D x)}Damit gilt etwa:

{x → 1, y → −7} Δ {x → �, y → −7}Die einfache Konstantenpropagation, die wir hier betrachten, ignoriert die Werte in-nerhalb des Speichers. Deshalb beschreiben wir die Programmzustände (ρ, μ) allei-ne durch abstrakten Variablenbelegungen, die ρ beschreiben. Die Beschreibungsre-lation ist damit definiert durch:

(ρ, μ) Δ D gdw. ρ Δ D

Hier liefert die Konkretisierung:

γ D =

{∅ falls D = ⊥{(ρ, μ) | ρ ∈ γ D} sonst

Wir wollen zeigen, dass jeder Pfad π im Kontrollflussgraphen die Beschreibungsre-lation Δ zwischen konkreten und abstrakten Zuständen erhält. Wir behaupten:

(K) Gilt s Δ D und ist [[π ]] s definiert, dann gilt auch ([[π ]] s) Δ ([[π ]]� D) .

Diese Behauptung visualisiert das folgende Diagramm:

s

D D1

s1

Δ Δ

[[π ]]

[[π ]]�

Aus der Behauptung (K) folgt insbesondere, dass

[[π ]] s ∈ γ ([[π ]]� D),

wenn immer s ∈ γ(D) gilt.Zum Beweis der Eigenschaft (K) für beliebige Pfade π genügt es, sie für eine

einzelne Kante k zu zeigen. Da die Behauptung für Pfade der Länge 0 gilt, folgt dann

Page 60: œbersetzerbau: Band 3: Analyse und Transformation

50 1 Grundlagen und intraprozedurale Optimierung

die Behauptung (K) für alle Pfade durch vollständige Induktion. Für jede Kante kund s Δ D bleibt zu zeigen, dass ([[k]] s) Δ ([[k]]� D) gilt, sofern [[k]] s definiert ist.

Der wesentliche Schritt zum Beweis der Eigenschaft (K) für eine Kante bestehtdarin, für jeden Ausdruck e nachzuweisen, dass gilt:

([[e]]ρ) Δ ([[e]]� D), sofern ρ Δ D. (∗∗)Zum Beweis der Aussage (∗∗) zeigen wir für jeden Operator �:

(x � y) Δ (x� �� y�), sofern x Δ x� ∧ y Δ y�

Dann folgt die Behauptung (∗∗) mittels struktureller Induktion über den Aufbau desAusdrucks e. Die Aussage über das Verhältnis zwischen konkreten und abstraktenOperatoren muss für jeden Operator gesondert nachgewiesen werden. Für die Kon-stantenpropagation ist dies sicherlich erfüllt.

Kehren wir zum Beweis der Verträglichkeit der konkreten und abstrakten Effekteeiner Kante k = (u, lab, v) mit der Beschreibungsrelation Δ auf Zuständen zurück.Wir führen eine Fallunterscheidung nach der Beschriftung lab der Kante durch.

Es gelte s = (ρ, μ) Δ D. Dann ist insbesondere D �= ⊥.

Zuweisung x ← e: Es gelten:

[[x ← e]] s = (ρ1, μ) mit ρ1 = ρ ⊕ {x → [[e]]ρ}

[[x ← e]]� D = D1 mit D1 = D ⊕ {x → [[e]]� D}Die Behauptung (ρ1, μ) Δ D1 folgt darum aus der Verträglichkeit der konkretenund abstrakten Ausdrucksauswertung mit der Beschreibungsrelation Δ .

Lesen x ← M[e]: Es gelten:

[[x ← M[e]]] s = (ρ1, μ) mit ρ1 = ρ ⊕ {x → μ ([[e]]ρ)}

[[x ← M[e]]]� D = D1 mit D1 = D ⊕ {x → �}Die Behauptung (ρ1, μ) Δ D1 folgt, da ρ1 x Δ � gilt.

Speichern M[e1] ← e2:

Hier gilt die Behauptung, da weder der konkrete noch der abstrakte Kanteneffektdie Variablenbelegung modifiziert.

Bedingung Zero(e):

Sei [[Zero(e)]] s definiert. Dann gilt 0 = ([[e]]ρ) Δ ([[e]]� D).Folglich ist [[Zero(e)]]� D = D �= ⊥, und die Behauptung ist erfüllt.

Bedingung NonZero(e):

Sei nun [[NonZero(e)]] s definiert. Dann gilt 0 �= ([[e]]ρ) Δ ([[e]]� D).Folglich gilt auch [[e]]� D �= 0. Damit haben wir: [[NonZero(e)]]� D = D, unddie Behauptung folgt.

Page 61: œbersetzerbau: Band 3: Analyse und Transformation

1.9 Konstantenfaltung 51

Zusammenfassend schließen wir, dass die Invariante (K) gilt.

Die MOP-Lösung für die Konstantenpropagation ist die kleinste obere Schrankeüber alle möglichen Beiträge, die Pfade vom Startpunkt zu einem Programmpunkt vfür die Anfangsbelegung D� liefern können:

D∗[v] =⊔{[[π ]]� D� | π : start →∗ v} ,

wobei D� x = � für alle x ∈ Vars gilt. Wegen der Invariante (K) gilt dann für alleAnfangszustände s und alle Berechnungen π , die den Programmpunkt v erreichen:

([[π ]] s) Δ (D∗[v])

Zur Approximation des MOP lösen wir das zugehörige Ungleichungssystem.

Beispiel 1.9.4 Betrachten wir unser Fakultätsprogramm, diesmal mit festem An-fangswert für die Variable x. Das Ergebnis der Analyse zeigt Abb. 1.23. Obwohl wir

7

36

4

5

2

1

0

x ← 10

y ← 1

M[R] ← y y ← x ∗ y

x ← x − 1

NonZero(x > 1)Zero(x > 1)

1 2 3x y x y x y

0 � � � �1 10 � 10 �2 10 1 � �3 10 1 � �4 10 10 � � dito5 9 10 � �6 ⊥ � �7 ⊥ � �

Abb. 1.23. Konstantenpropagation für das Fakultätsprogramm.

die Fakultätsberechnung komplett in die Übersetzungszeit verlegen könnten, liefertdie Konstantenpropagation nicht dieses Ergebnis: der Grund ist, dass die Konstanten-propagation für jeden Programmpunkt die Variablenwerte ermittelt, die während dergesamten Programmausführung konstant sind. In der Schleifen ändern sich jedochdie Werte von x und y. ��Als Fazit halten wir fest, dass Konstantenpropagation zwar mit konkreten Wertenrechnet, aber nur einen „Ausschnitt“ aus der konkreten Variablenbelegung bestim-men kann. Ausdrücke, die nur Variablen aus diesem Ausschnitt enthalten, können

Page 62: œbersetzerbau: Band 3: Analyse und Transformation

52 1 Grundlagen und intraprozedurale Optimierung

statisch, d.h. durch den Übersetzer ausgewertet werden. Die Fixpunktiteration zurBerechnung der kleinsten Lösung des Ungleichungssystems terminiert garantiert: bein Programmpunkten und m Variablen benötigt sie maximal O(m · n) Runden. WieSie in Beispiel 1.9.4 gesehen haben, terminiert das Verfahren allerdings oft schnel-ler. Aber Achtung: die Kanteneffekte für die Konstantenpropagation sind nicht alledistributiv!

Ein einfaches Gegenbeispiel zur Distributivität liefert der abstrakte Kanteneffektfür die Zuweisung x ← x + y;. Betrachten wir die beiden Variablenbelegungen:

D1 = {x → 2, y → 3} und D2 = {x → 3, y → 2}Einerseits gilt:

[[x ← x + y]]�D1 � [[x ← x + y]]�D2 = {x → 5, y → 3} � {x → 5, y → 2}= {x → 5, y → �}

Andererseits gilt aber:

[[x ← x + y]]� (D1 � D2) = [[x ← x + y]]� {x → �, y → �}= {x → �, y → �}

Damit ist

[[x ← x + y]]�D1 � [[x ← x + y]]�D2 �= [[x ← x + y]]� (D1 � D2)

und die Distributivität ist verletzt. Folglich liefert die kleinste Lösung D des Unglei-chungssystems i.A. nur eine obere Approximation der MOP-Lösung, d.h. es gilt:

D∗[v] � D[v]

für jeden Programmpunkt v. Als obere Approximation beschreibt D[v] trotzdem dasErgebnis jeder Berechnung π , die in v endet:

([[π ]] (ρ, μ)) Δ D[v] ,

wann immer [[π ]] (ρ, μ) definiert ist. Damit liefert die kleinste Lösung immerhinsichere Information, die wir in einer Programmtransformation verwenden können.

Transformation CF:

Die erste Verwendung der Information D besteht darin, Programmpunkte, die alssicher unerreichbar identifiziert wurden, zu beseitigen. Diese Beseitigung von totemCode leistet die Transformationsregel:

Page 63: œbersetzerbau: Band 3: Analyse und Transformation

1.9 Konstantenfaltung 53

D[u] = ⊥u

Weiterhin entfernen wir sämtliche (Bedingungs-)Kanten, die zwar zu einem mögli-cherweise erreichbaren Programmpunkt führen, selbst aber stets den Wert ⊥ liefern:

u

lab

[[lab]]�(D[u]) = ⊥ u

v v

Die nächsten beiden Regeln vereinfachen Bedingungskanten, deren Bedingungsaus-druck einen definitiven Wert liefert. Dieser zeigt an, dass diese Kanten sicher beijeder Berechnung ausgewählt werden:

u u

;

[[e]]� D = 0⊥ �= D[u] = D

Zero (e)

u u

;

[[e]]� D �∈ {0,�}⊥ �= D[u] = D

NonZero(e)

Schließlich verwenden wir die Information D, um im Programm vorkommende Aus-drücke gegebenenfalls bereits zur Übersetzungszeit auszuwerten. Für Zuweisungenerhalten wir etwa:

x ← e′u u

x ← e

⊥ �= D[u] = D

wobei sich der Ausdruck e′ ergibt, indem man den Ausdruck e bzgl. der abstraktenVariablenbelegung D auszuwerten versucht, d.h. man definiert:

Page 64: œbersetzerbau: Band 3: Analyse und Transformation

54 1 Grundlagen und intraprozedurale Optimierung

e′ =

{c falls [[e]]� D = c �= �e falls [[e]]� D = �

Bei der Vereinfachung von Ausdrücken an anderen Kanten verfahren wir analog.

Die Konstantenfaltung, so wie wir sie bisher definierten, bezieht sich immer aufdie maximalen Ausdrücke aus den Anweisungen. Man kann sie aber auch einsetzen,um Teilausdrücke zu vereinfachen:

x + (3 · y){x →�,y →5}

=========⇒ x + 15

y · (x + 3){x →�,y →5}

=========⇒ 5 · (x + 3)

Unsere Analyse kann weiterhin dahingegehend verbessert werden, dass die in Be-dingungen enthaltene Information besser ausgenutzt wird.

Beispiel 1.9.5 Betrachten Sie das folgende Beispiel-Programm:

if (x = 7)y ← x + 3;

Selbst wenn die Analyse den Wert von x vor der if-Abfrage nicht kennt, könnte sieableiten, dass bei Betreten des then-Teils x stets den Wert 7 hat. ��Gut ausgenützt werden können Bedingungen, welche die Gleichheit von Variablenmit Werten oder anderen Variablen testen:

[[NonZero (x = e)]]� D =

{⊥ falls [[x = e]]� D = 0D1 sonst

wobei wir setzen:D1 = D ⊕ {x → (D x � [[e]]� D)}

Einen analogen abstrakten Kanteneffekt wählen wir auch für Zero (x �= e).Die Optimierung, die wir für unser Programm aus Beispiel 1.9.5 erhalten, zeigt

die Abb. 1.24.

1.10 Intervallanalyse

Oft ist die exakte Menge von Werten, die eine Variable an einem Programmpunkt beiirgendeiner Programmausführung annehmen kann, nicht bekannt. Für viele Zweckereicht es jedoch, ein (möglichst kleines) Intervall zu kennen, in dem sicher alle Wertedieser Variablen liegen. Solche Intervalle berechnet die Intervallanalyse.

Page 65: œbersetzerbau: Band 3: Analyse und Transformation

1.10 Intervallanalyse 55

0

1

2

3

0

1

2

3;

NonZero (x = 7)

y ← x + 3

Zero (x = 7)

;

NonZero (x = 7)

y ← 10

Zero (x = 7)

Abb. 1.24. Die Ausnutzung der Information an Bedingungen.

Beispiel 1.10.1 Betrachten wir das folgende Programm:

for (i ← 0; i < 42; i++) a[i] = i;

In Programmiersprachen wie JAVA wird verlangt, dass die Indizes bei Feldzugriffenstets innerhalb der deklarierten Grenzen des Felds liegen. Beginnt das Feld a z.B. abder Adresse A und soll es int-Werte mit den Indizes 0, . . . , 41 enthalten, dann könnteder erzeugte Zwischencode etwa so aussehen:

i ← 0;B : if (i < 42) {

if (0 ≤ i ∧ i < 42) {A1 ← A + i;M[A1] ← i;i ← i + 1;

} else goto error;goto B;

}Die Bedingung der äußeren Schleife macht die innere Bereichsüberprüfung überflüs-sig. Der Programmpunkt error wird nie erreicht. Die innere Bereichsüberprüfungkann deshalb eliminiert werden. ��Die Intervallanalyse verallgemeinert die Konstantenpropagation, indem sie den Wer-tebereich Z� für die Variablen durch einen Bereich von Intervallen ersetzt. Die Men-ge aller Intervalle ist gegeben durch:

I = {[l, u] | l ∈ Z∪ {−∞}, u ∈ Z∪ {+∞}, l ≤ u}Hier stehen l für lower und u für upper. Gemäß dieser Definition repräsentiert jedesIntervall eine nicht-leere Menge von ganzen Zahlen. Zwischen Intervallen definieren

Page 66: œbersetzerbau: Band 3: Analyse und Transformation

56 1 Grundlagen und intraprozedurale Optimierung

wir die natürliche Ordnungsrelation �:

[l1, u1] � [l2, u2] gdw. l2 ≤ l1 ∧ u1 ≤ u2

Die entsprechende geometrische Anschauung illustriert Abb. 1.25.

l1 u1

l2 u2

Abb. 1.25. Die Ordnungsrelation für Intervalle [l1, u1] � [l2, u2].

Die kleinste obere und die größte untere Schranke zweier Intervalle sind dannwie folgt definiert:

[l1, u1] � [l2, u2] = [min{l1, l2}, max{u1, u2}][l1, u1] � [l2, u2] = [max{l1, l2}, min{u1, u2}], sofern max{l1, l2} ≤ min{u1, u2}

Die geometrische Anschauung dieser Operationen illustriert Abb. 1.26. Über denbeiden Beispielintervallen haben wir dabei ihre kleinste obere Schranke vermerktund ihre größte untere Schranke darunter. Wie Z� ist die Menge I mit der Halbord-

l1 u1

l2 u2

[l1, u1] � [l2, u2]

[l1, u1] � [l2, u2]

Abb. 1.26. Kleinste obere (oben) und größte untere Schranke (unten) der zwei Intervalle inder Mitte.

nung � zwar eine Halbordnung, aber kein vollständiger Verband: da die leere Mengeexplizit ausgeschlossen ist, gibt es kein kleinstes Element. Folglich existieren klein-ste obere Schranken nur für nicht-leere Mengen von Intervallen. Beachten Sie auch,

Page 67: œbersetzerbau: Band 3: Analyse und Transformation

1.10 Intervallanalyse 57

dass es (im Gegensatz zu der Halbordnung Z�) in I aufsteigende Ketten gibt, dieniemals stabil werden, z.B. diese:

[0, 0] � [0, 1] � [−1, 1] � [−1, 2] � . . .

Zwischen konkreten Werten und Intervallen stellen wir die folgende Beschreibungs-relation Δ auf:

z Δ [l, u] gdw. l ≤ z ≤ u

Diese Beschreibungsrelation führt zu folgender Konkretisierung:

γ [l, u] = {z ∈ Z | l ≤ z ≤ u}Beispiel 1.10.2 Damit haben wir etwa:

γ [0, 7] = {0, . . . , 7}γ [0, ∞] = {0, 1, 2, . . .}

��Wir wollen mit Intervallen rechnen. Die Summe zweier Intervalle sollte alle Werteenthalten, die sich ergeben, wenn wir beliebige Werte aus den beiden Argument-intervallen addieren. Deshalb definieren wir:

[l1, u1] +� [l2, u2] = [l1 + l2, u1 + u2] wobei−∞ + _ = −∞

+∞ + _ = +∞

Beachten Sie, dass niemals der Wert von −∞ + ∞ ermittelt werden muss.Für die Negation definieren wir:

−� [l, u] = [−u,−l]

Schwieriger ist es, das kleinste Intervall zu finden, das sämtliche Produkte der Wertezweier Intervalle enthält. Eine einfache Beschreibung ohne umständliche Fallunter-scheidungen bietet die folgende Definition:

[l1, u1] ·� [l2, u2] = [a, b] wobeia = min{l1l2, l1u2, u1l2, u1u2}b = max{l1l2, l1u2, u1l2, u1u2}

Beispiel 1.10.3 Wir überprüfen die Plausibilität unserer Definition der Intervallmul-tiplikation anhand einiger Beispiele:

[0, 2] ·� [3, 4] = [0, 8][−1, 2] ·� [3, 4] = [−4, 8]

[−1, 2] ·� [−3, 4] = [−6, 8][−1, 2] ·� [−4,−3] = [−8, 4]

��

Page 68: œbersetzerbau: Band 3: Analyse und Transformation

58 1 Grundlagen und intraprozedurale Optimierung

Problematischer dagegen ist es, eine geeignete Definition der Division von Interval-len zu finden. Sei [l1, u1] /� [l2, u2] = [a, b] .

• Ist 0 nicht im Nenner-Intervall enthalten, können wir setzen:

a = min{l1/l2, l1/u2, u1/l2, u1/u2}b = max{l1/l2, l1/u2, u1/l2, u1/u2}

• Ist dagegen 0 im Nenner-Intervall enthalten, d.h. gilt: l2 ≤ 0 ≤ u2, ist einLaufzeitfehler nicht auszuschließen. In der Semantik unserer kleinen Program-miersprache haben wir offen gelassen, was beim Eintreten eines solchen Feh-lers geschehen soll. Der Einfachkeit halber nehmen wir hier an, dass in diesemFall jeder Wert ein erlaubtes Ergebnis ist. Darum definieren wir für diesen Fall:

[a, b] = [−∞, +∞]

Neben arithmetischen Operationen benötigen wir abstrakte Versionen der Vergleichs-operatoren. Die abstrakte Gleichheitsoperation ist dabei wesentlich verschieden vonder „natürlichen“ Gleichheit auf Intervallen. Insbesondere sollte das Ergebnis wiederein Intervall sein. Ist es einelementig, heißt das, dass das Ergebnis der Wertvergleichefür alle Auswahlen der Elemente der beiden Argumentintervalle stets das Gleiche ist.Wir haben:

[l1, u1] =� [l2, u2] =

⎧⎪⎨⎪⎩

[1, 1] falls l1 = u1 = l2 = u2

[0, 0] falls u1 < l2 ∨ u2 < l1

[0, 1] sonst

Um diese Definition zu verstehen, erinnern wir uns, dass wir hier entsprechend derProgrammiersprache C die Booleschen Werte false und true als 0 bzw. nicht-0 re-präsentieren. Die konkrete Wertegleichheit liefert deshalb einen Wert aus der Menge{0, 1} zurück. Wir erläutern nun die drei obigen Fälle.

Der erste Fall erfasst den Vergleich zweier identischer einelementiger Interval-le, der zweite den Fall disjunkter Intervalle und der dritte den Fall überlappenderIntervalle. Im ersten Fall ergibt sich bei Vergleich aller jeweils durch die Intervallerepräsentierten Werte true. Die Menge der möglichen Ergebnisse ist damit in dem In-tervall [1, 1] enthalten. Im zweiten Fall dagegen ergibt sich bei Vergleich aller jeweilsdurch die Intervalle repräsentierten Werte false. Die Menge der möglichen Ergebnis-se ist damit in dem Intervall [0, 0] enthalten. Im letzten Fall dagegen kann sich beiVergleich der durch die Intervalle repräsentierten Werte sowohl true wie false erge-ben. Die Menge der möglichen Ergebnisse ist darum in der Menge {0, 1} enthalten.Diese Menge wird durch das Intervall [0, 1] beschrieben.

Beispiel 1.10.4 Wieder überzeugen wir uns anhand kleiner Beispiele von der Ver-nünftigkeit dieser Definition:

[42, 42]=�[42, 42] = [1, 1][1, 2] =� [3, 4] = [0, 0][0, 7] =� [0, 7] = [0, 1]

Page 69: œbersetzerbau: Band 3: Analyse und Transformation

1.10 Intervallanalyse 59

��Von den weiteren Vergleichsoperationen betrachten wir nur noch die Operation <.Hier haben wir:

[l1, u1] <� [l2, u2] =

⎧⎪⎨⎪⎩

[1, 1] falls u1 < l2

[0, 0] falls u2 ≤ l1

[0, 1] sonst

Beispiel 1.10.5[1, 2] <� [9, 42] = [1, 1][0, 7] <� [0, 7] = [0, 1][3, 4] <� [1, 3] = [0, 0]

��Mithilfe der Halbordnung (I,�) konstruieren wir einen vollständigen Verband fürabstrakte Variablenbelegungen analog zu unserem vollständigen Verband für dieKonstantenpropagation:

DI = (Vars → I)⊥ = Vars → I∪ {⊥}für ein neues Element ⊥, das wieder das kleinste Element bezeichnet. Die Beschrei-bungsrelation zwischen konkreten und abstrakten Variablenbelegungen definierenwir auf natürliche Weise durch

ρ Δ D gdw. D �= ⊥ ∧ ∀ x ∈ Vars : (ρ x) Δ (D x).

Dies führt zu einer entsprechenden Beschreibungsrelation Δ zwischen konkreten Zu-ständen (ρ, μ) und abstrakten Variablenbelegungen:

(ρ, μ) Δ D gdw. ρ Δ D

Auch die abstrakte Ausdrucksauswertung definieren wir analog zur abstrakten Aus-drucksauswertung bei der Konstantenpropagation. Auch über Intervallen gilt dannfür alle Ausdrücke:

([[e]] ρ) Δ ([[e]]� D) sofern ρ Δ D

Schauen wir uns als nächstes die Kanteneffekte an, die wir zur Intervallanalyse brau-chen. Abgesehen davon, dass wir nun mit Intervallen rechnen, sehen die entsprechen-den Kanteneffekte ganz genauso wie bei der Konstantenpropagation aus:

Page 70: œbersetzerbau: Band 3: Analyse und Transformation

60 1 Grundlagen und intraprozedurale Optimierung

[[;]]� D = D[[x ← e]]� D = D ⊕ {x → [[e]]� D}

[[x ← M[e]]]� D = D ⊕ {x → �}[[M[e1] ← e2]]� D = D

[[NonZero (e)]]� D =

{⊥ falls [0, 0] = [[e]]� DD sonst

[[Zero (e)]]� D =

{⊥ falls [0, 0] �� [[e]]� DD falls [0, 0] � [[e]]� D

sofern D �= ⊥. Dabei bezeichnet � das Intervall [−∞, ∞].Wir nehmen an, dass wie bei der Konstantenpropagation vor der Programmaus-

führung nichts über die Werte der Variablen bekannt ist. Dort können wir darum nurdas größte Element � = {x → [−∞, ∞] | x ∈ Vars} des Verbandes annehmen.Zum Beweis der Korrektheit der Intervallanalyse stellen wir eine Invariante auf, diesich von der Invariante (K) für die Konstantenpropagation nur dadurch unterschei-det, dass wir mit Intervallen rechnen anstatt mit Z�. Auch der Beweis der neuenInvariante benutzt die gleiche Argumentation, so dass wir darauf verzichten, den Be-weis auszuführen.

Eine wesentliche Quelle der Information sind auch bei der Intervallanalyse dieBedingungen. Noch viel mehr als bei der Konstantenpropagation können wir unshier Vergleiche von Variablen mit Werten zunutze machen. Nehmen wir an, e ist vonder Form x�e1 für Vergleichsoperatoren � ∈ {=, <, >}. Dann definieren wir:

[[NonZero (e)]]� D =

{⊥ falls [0, 0] = [[e]]� DD1 sonst

wobei

D1 =

⎧⎪⎨⎪⎩

D ⊕ {x → (D x) � ([[e1]]� D)} falls e ≡ (x = e1)D ⊕ {x → (D x) � [−∞, u − 1]} falls e ≡ (x < e1), [[e1]]� D = [_, u]D ⊕ {x → (D x) � [l + 1, ∞]} falls e ≡ (x ≥ e1), [[e1]]� D = [l, _]

Eine Bedingung NonZero(x < e1) erlaubt es, vom Intervall von x das Intervall[u, ∞] wegzuschneiden, wobei u der größtmögliche Wert im Intervall für e1 ist. Ent-sprechend definieren wir:

[[Zero (e)]]� D =

{⊥ falls [0, 0] �� [[e]]� DD1 sonst

wobei

D1 =

⎧⎪⎨⎪⎩

D ⊕ {x → (D x) � [−∞, u]} falls e ≡ (x > e1), [[e1]]� D = [_, u]D ⊕ {x → (D x) � [l, ∞]} falls e ≡ (x < e1), [[e1]]� D = [l, _]D falls e ≡ (x = e1)

Page 71: œbersetzerbau: Band 3: Analyse und Transformation

1.10 Intervallanalyse 61

Beachten Sie, dass hier grösste untere Schranken von Intervallen verwendet werden.Diese Durchschnitte sind im gegebenen Kontext jedoch immer definiert, da sich an-dernfalls bereits vorher die Alternative mit Ergebnis ⊥ ergeben hätte.

Betrachten wir das Programm aus Beispiel 1.10.1. Seinen Kontrollflussgraphenund die kleinste Lösung des Ungleichungssystems für die Intervallanalyse der Varia-blen i zeigt Abb. 1.27.

0

1

8

6

5

4

2

37

Zero(i < 42)

Zero(0 ≤ i < 42)

NonZero(i < 42)

NonZero(0 ≤ i < 42)

A1 ← A + i

M[A1] ← i

i ← i + 1

i ← 0i

l u0 −∞ +∞

1 0 422 0 413 0 414 0 415 0 416 1 427 ⊥8 42 42

Abb. 1.27. Die kleinste Intervall-Lösung für Beispiel 1.10.1.

Da die Halbordnung I aufsteigende Ketten besitzt, die niemals stabil werden, istzunächst unklar, wie man die kleinste Lösung der Ungleichungssysteme der Inter-vallanalyse berechnen könnte. In unserem Beispiel terminiert die Round-Robin-Ite-ration zwar, aber erst nach 42 Runden. Man kann jedoch leicht Programme konstru-ieren, bei denen die Round-Robin-Iteration nie terminiert.

Wir benötigen deshalb Techniken, die es uns erlauben, auch vollständige Ver-bände mit aufsteigende Ketten zu verwenden, die niemals stabil werden. Wie derallgemeine Ansatz der abstrakten Interpretation gehen die generellen Verfahren desWidening und Narrowing auf Patrick und Radhia Cousot zurück. Bereits die ersteeinschlägige Arbeit behandelte als Beispiel die Intervallanalyse.

Die Idee des Widening besteht darin, die Fixpunktiteration zu beschleunigen –eventuell durch die Aufgabe von Präzision. Die Beschleunigung soll so organisiertwerden, dass für jeden abstrakten Wert einer Unbekannten des Ungleichungssystemsnur endlich viele Veränderungen zugelassen werden.

Für die Intervallanalyse bedeutet das, dass wir nicht beliebige Vergrößerungenvon Intervallen erlauben. Wir gestatten keine Vergrößerungen von endlichen zu end-lichen Intervallgrenzen. Eine zulässige aufsteigende Kette könnte dann etwa so aus-sehen:

Page 72: œbersetzerbau: Band 3: Analyse und Transformation

62 1 Grundlagen und intraprozedurale Optimierung

[3, 17] � [3, +∞] � [−∞, +∞]

Im Folgenden wollen wir diese Idee formalisieren. Sei wieder

xi � fi (x1, . . . , xn) , i = 1, . . . , n

ein Ungleichungssystem über einem vollständigen Verband D, wobei die fi nunnicht notwendigerweise monoton sein müssen. Auch für solche Ungleichungssyste-me können wir eine akkumulierende Iteration definieren, die dann zwar nicht unbe-dingt die kleinste Lösung, aber, sofern die Iteration terminiert, zumindest irgendeineLösung liefert. Wir betrachten das zu dem Ungleichungssystem zugehörige akkumu-lierende Gleichungssystem:

xi = xi � fi (x1, . . . , xn) , i = 1, . . . , n

Ein Tupel x = (x1, . . . , xn) ∈ Dn ist genau dann eine Lösung des Ungleichungssy-stems, wenn es eine Lösung des akkumulierenden Gleichungssystems ist.

Mit dem akkumulierenden Gleichungssystem selbst ist noch nicht viel gewon-nen. Auch eine Fixpunktiteration für dieses Gleichungssystem, etwa mithilfe vonRound-Robin-Iteration terminiert nicht notwendigerweise. Um Terminierung zu er-zwingen, ersetzen wir den Operator � des akkumulierenden Gleichungssystemsdurch einen Widening-Operator �– . Damit erhalten wir das Gleichungssystem:

xi = xi �– fi(x1, . . . , xn) , i = 1, . . . , n

Für die neue Operation �– sollte dabei gelten:

v1 � v2 � v1 �– v2

Die während einer Fixpunktiteration für eine Unbekannte xi akkumulierten Wer-te werden gegebenenfalls schneller größer. Insbesondere berechnet Round-Robin-Iteration für das modifizierte System, wenn sie denn terminiert, immer noch eine Lö-sung des akkumulierenden Gleichungssystems und damit des Ausgangsungleichungs-systems.

Wir wenden dieses Vorgehen nun auf die Intervallanalyse und den vollständigenVerband DI = (Vars → I)⊥ an. Einen Widening-Operator �– für diesen vollstän-digen Verband definieren wir durch:

⊥�– D = D �– ⊥ = D

und für D1 �= ⊥ �= D2

(D1 �– D2) x = (D1 x)�– (D2 x) wobei

[l1, u1]�– [l2, u2] = [l, u] mit

l =

{l1 falls l1 ≤ l2

−∞ sonst

u =

{u1 falls u1 ≥ u2

+∞ sonst

Page 73: œbersetzerbau: Band 3: Analyse und Transformation

1.10 Intervallanalyse 63

Der Widening-Operator für Variablenbelegungen basiert auf einem Widening-Operatorfür Intervalle. Auch dieser Operator behandelt seine beiden Argumente nicht gleichund ist folglich nicht kommutativ. Das sieht man ein, wenn Die Intuition dahinter er-kennt man, wenn man die Anwendung des Widening-Operators betrachtet. Währendder Fixpunktiteration ist der linke Operand immer der alte Wert, während der rechteder neue Wert ist.

Beispiel 1.10.6 Wir haben etwa:

[0, 2]�– [1, 2] = [0, 2][1, 2]�– [0, 2] = [−∞, 2][1, 5]�– [3, 7] = [1, +∞]

��Im Allgemeinen liefert ein Widening-Operator anstelle der kleinsten oberen Schran-ke irgendeine obere Schranke. Damit werden bei einer Fixpunktiteration die Wer-te für die Unbekannten schneller größer. Der Widening-Operator sollte so gewähltwerden, dass die dabei entstehenden aufsteigenden Ketten irgendwann stabil werdenund damit die Fixpunktiteration terminiert. Der hier vorgestellte Widening-Operatorfür Intervalle garantiert zum Beispiel, dass jedes Intervall höchstens zweimal grö-ßer werden kann. Damit begrenzt er die Anzahl der notwendigen Round-Robin-Iterationen für Programme mit n Programmpunkten auf O(n · #Vars).

Fassen wir diese Idee noch einmal zusammen. Um eine Lösung eines Unglei-chungssystems über einem vollständigen Verband mit unendlichen aufsteigendenKetten zu bestimmen, definieren wir einen geeigneten Widening-Operator. DiesenWidening-Operator setzen wir ein, um die Berechnung einer Lösung des zugehöri-gen akkumulierenden Gleichungssystems zu beschleunigen und Konvergenz zu er-zwingen. Ändert der Widening-Operator Werte nur endlich oft, können wir die Ter-minierung der akkumulierenden Iteration garantieren.

Die Konstruktion eines geeigneten Widening-Operators ist eine Art schwarzeKunst: Einerseits muss er ziemlich radikal Information verwerfen, um Terminierungzu garantieren. Andererseits sollte er genügend relevante Informationen bewahren.Abb. 1.28 zeigt die Round-Robin-Iteration für das Programm aus Beispiel 1.10.1.Wie erhofft, terminiert die Iteration sehr schnell – jedoch mit enttäuschendem Ergeb-nis: im Beispiel sind sämtliche obere Schranken verloren gegangen. Eine Einsparungder Feldgrenzenüberprüfung ist nicht möglich.

Offenbar wurde Information zu schnell verworfen. Bei einigem Nachdenkenbemerken wir, dass es, um Terminierung zu garantieren, nicht notwendig ist, denWidening-Operator für jede Unbekannte, d.h. an jedem Programmpunkt, einzuset-zen. Tatsächlich würde es genügen, wenn man Widening genügend oft anwendet,das heißt, dass jeder Kreis im Kontrollflussgraphen zumindest eine Anwendung desWidening-Operators enthält. Eine Menge I von Knoten in einem gerichteten Gra-phen G, mit der Eigenschaft, dass jeder Kreis in G mindestens einen Knoten aus Ienthält, nennen wir auch Kreistrenner (loop separator). Wenden wir Widening nicht

Page 74: œbersetzerbau: Band 3: Analyse und Transformation

64 1 Grundlagen und intraprozedurale Optimierung

0

1

8

6

5

4

2

37

Zero(i < 42)

Zero(0 ≤ i < 42)

NonZero(i < 42)

NonZero(0 ≤ i < 42)

A1 ← A + i

M[A1] ← i

i ← i + 1

i ← 01 2 3

l u l u l u0 −∞ +∞ −∞ +∞

1 0 0 0 +∞

2 0 0 0 +∞

3 0 0 0 +∞

4 0 0 0 +∞ dito5 0 0 0 +∞

6 1 1 1 +∞

7 ⊥ 42 +∞

8 ⊥ 42 +∞

Abb. 1.28. Beschleunigte Round-Robin-Iteration für Beispiel 1.10.1.

an allen Programmpunkten, sondern nur an den Punkten aus einer solchen Menge Ides Kontrollflussgraphen an, terminiert die Round-Robin-Iteration immer noch.

Beispiel 1.10.7 Diese Idee probieren wir an unserem Testprogramm aus Beispiel1.10.1 aus. Abbildung 1.29 zeigt Beispiel-Mengen I1 und I2 von Knoten, an denenwir die Schleife des Programms auftrennen könnten. Für die Menge I1 = {1} ergibt

0

1

8

6

5

4

2

37

Zero(i < 42)

Zero(0 ≤ i < 42)

NonZero(i < 42)

NonZero(0 ≤ i < 42)

A1 ← A + i

M[A1] ← i

i ← i + 1

i ← 0

I1 = {1} oder

I2 = {2}

Abb. 1.29. Kreistrenner für das Beispiel 1.10.1.

die Round-Robin-Iteration:

Page 75: œbersetzerbau: Band 3: Analyse und Transformation

1.10 Intervallanalyse 65

1 2 3

l u l u l u0 −∞ +∞ −∞ +∞

1 0 0 0 +∞

2 0 0 0 413 0 0 0 414 0 0 0 41 dito5 0 0 0 416 1 1 1 427 ⊥ ⊥8 ⊥ 42 +∞

Tatsächlich erhalten wir fast die kleinste Lösung. Die einzige Information, die wirverlieren, ist die obere Schranke für die Schleifenvariable i an den Programmpunkten1 und 8. Für die Menge I2 = {2} ergibt sich dagegen:

1 2 3

l u l u l u0 −∞ +∞ −∞ +∞

1 0 0 0 422 0 0 0 +∞

3 0 0 0 414 0 0 0 41 dito5 0 0 0 416 1 1 1 427 ⊥ 42 +∞

8 ⊥ 42 42

Nun erhält man genaue Aussagen über die Variable i an den Programmpunkten 1und 8, verliert jedoch soviel Information am Programmpunkt 2, dass man die Uner-reichbarkeit des Programmpunkts 7 nicht mehr herleiten kann. ��Dieses Beispiel zeigt, dass die Beschränkung des Widening auf wenige wichtigePunkte die Genauigkeit der Analyse erheblich verbessern kann. Das Beispiel zeigtaber auch, dass es in der Anwendung nicht immer offensichtlich ist, diejenigen Stel-len zu finden, an denen die größte Genauigkeit erzielt wird. Als Ergänzung betrach-ten wir deshalb eine weitere Technik: Narrowing.

Das Narrowing ist eine Technik, um eine möglicherweise zu große und damitzu ungenaue Lösung sukzessive zu verbessern. Wie beim Widening entwickeln wirzuerst die allgemeine Herangehensweise für beliebige Ungleichungssysteme undschauen uns dann an, wie Narrowing für die Intervallanalyse funktionieren könnte.

Page 76: œbersetzerbau: Band 3: Analyse und Transformation

66 1 Grundlagen und intraprozedurale Optimierung

Sei x irgendeine Lösung des Ungleichungssystems

xi � fi (x1, . . . , xn) , i = 1, . . . , n

Nehmen wir weiter an, dass die rechten Seiten fi sämtlich monoton sind und F diezugehörige Funktion Dn → Dn ist. Aus der Monotonie von F folgt:

x � F x � F2 x � . . . � Fk x � . . .

Diese Iteration nennen wir Narrowing. Narrowing hat die Eigenschaft, dass alle Tu-pel Fi x, die nach einigen Iterationen erreicht werden, selbst wieder Lösungen desUngleichungssystems sind. Dies gilt ebenfalls für Narrowing mittels Round-Robin-Iteration. Terminierung ist darum kein Problem mehr; man stoppt, wenn die erreich-ten Werte zufriedenstellend sind.

Beispiel 1.10.8 Betrachten wir erneut das Programm aus Beispiel 1.10.1. Wir star-ten die Narrowing-Iteration mit dem Ergebnis, welches das naive Widening lieferte.Dann erhalten wir:

0 1 2

l u l u l u0 −∞ +∞ −∞ +∞ −∞ +∞

1 0 +∞ 0 +∞ 0 422 0 +∞ 0 41 0 413 0 +∞ 0 41 0 414 0 +∞ 0 41 0 415 0 +∞ 0 41 0 416 1 +∞ 1 42 1 427 42 +∞ ⊥ ⊥8 42 +∞ 42 +∞ 42 42

Die optimale Lösung wird hier tatsächlich erreicht! ��In unserem Beispiel kompensiert das anschließende Narrowing die Informationsver-luste durch Widening vollständig. Das ist nicht immer zu erwarten. Auch ist nichtausgeschlossen, dass die Narrowing-Iteration möglicherweise sehr lange läuft; mög-licherweise terminiert Narrowing nicht einmal: dann nämlich, wenn es in dem voll-ständigen Verband absteigende Ketten

d1 � d2 � . . .

gibt, die niemals stabil werden. Dies ist zum Beispiel bei den Intervallen der Fall.Um Terminierung zu garantieren, kann man beschleunigtes Narrowing einsetzen.

Nehmen wir an, wir hätten irgendeine Lösung unseres Ungleichungssystems:

xi � fi (x1, . . . , xn) , i = 1, . . . , n

Page 77: œbersetzerbau: Band 3: Analyse und Transformation

1.10 Intervallanalyse 67

Dann betrachten wir das Gleichungssystem:

xi = xi � fi (x1, . . . , xn) , i = 1, . . . , n

Weil wir mit einer möglicherweise zu großen Lösung starten, benutzen wir die Bei-träge der rechten Seiten, um die bisherigen Werte für die Unbekannten zu verbessern,d.h. zu verkleinern.

Sei H : Dn → Dn die Funktion mit H (x1, . . . , xn) = (y1, . . . , yn) mit yi =xi � fi (x1, . . . , xn). Sind alle fi monoton, dann gilt:

Hi x = Fi x für alle i ≥ 0 .

In dem Gleichungssystem ersetzen wir nun den Operator � durch einen neuen Ope-rator �– mit der folgenden Eigenschaft:

a1 � a2 � a1 �– a2 � a1

Den neuen Operator nennen wir auch Narrowing-Operator. Der neue Operator ver-kleinert möglicherweise Werte nicht so schnell wie die größte untere Schranke, istaber zumindest nicht vergrößernd.

Im Falle der Intervallanalyse könnte man einen Narrowing-Operator so definie-ren, dass er Intervallgrenzen nur modifiziert, um unendliche Intervall-Grenzen durchendliche zu ersetzen. Unter diesen Umständen kann jedes Intervall höchstens zwei-mal modifiziert werden. Für Variablenbelegungen D definieren wir

⊥�– D = D �– ⊥ = ⊥und für D1 �= ⊥ �= D2

(D1 �– D2) x = (D1 x)�– (D2 x) wobei

[l1, u1]�– [l2, u2] = [l, u] mit

l =

{l2 falls l1 = −∞

l1 sonst

u =

{u2 falls u1 = ∞

u1 sonst

Wieder bemerken wir, dass der Narrowing-Operator seine Argumente nicht gleich-berechtigt behandelt und damit nicht kommutativ ist. In der Anwendung des Opera-tors ist der linke Operand der Wert aus dem letzten Iterationsschritt und der rechteOperand der neu berechnete Wert.

Beispiel 1.10.9 Auch das beschleunigte Narrowing mit Round-Robin-Iteration pro-bieren wir auf dem Programm aus Beispiel 1.10.1 aus. Wir erhalten:

Page 78: œbersetzerbau: Band 3: Analyse und Transformation

68 1 Grundlagen und intraprozedurale Optimierung

0 1 2

l u l u l u0 −∞ +∞ −∞ +∞ −∞ +∞

1 0 +∞ 0 +∞ 0 422 0 +∞ 0 41 0 413 0 +∞ 0 41 0 414 0 +∞ 0 41 0 415 0 +∞ 0 41 0 416 1 +∞ 1 42 1 427 42 +∞ ⊥ ⊥8 42 +∞ 42 +∞ 42 42

Tatsächlich geht trotz der Beschleunigung zumindest in diesem Beispiel keine Infor-mation verloren. ��Anders als bei Widening mussten wir bei Narrowing voraussetzen, dass die rechtenSeiten des Ungleichungssystems monoton sind. Wenn unser Narrowing-Operator nurendlich lange echt absteigende Ketten gestattet, terminiert das beschleunigte Nar-rowing garantiert. Im Falle der Intervallanalyse hatten wir unseren Operator �–so definiert, dass jedes Intervall höchstens zweimal modifiziert wird. Folglich er-fordert auch die Round-Robin-Iteration mit diesem Narrowing-Operator maximalO(n · #Vars) Runden (n die Anzahl der Programmpunkte).

1.11 Aliasanalyse

Bisher haben wir uns bei unseren Analysen und Optimierungen im wesentlichen nurum Variablen gekümmert. Den Speicher M unserer Programmiersprache haben wirdabei wie ein großes statisch allokiertes Feld betrachtet. Diese Auffassung ist fürmanche Fragestellungen ausreichend. Moderne Programmiersprachen bieten jedochKonzepte an, um nicht nur explizit über Namen oder Adressen, sondern auch an-onym über Zeiger auf Datenobjekte zuzugreifen. In diesem Abschnitt behandeln wirGrundkonzepte und einfache Ansätze, um mit Zeigern auf dynamisch allokierte Da-tenobjekte umzugehen. Dazu erweitern wir unsere Programmiersprache um Zeiger,die auf den Anfang dynamisch allokierter Blöcke zeigen. Zur Unterscheidung ver-wenden wir kleine Buchstaben für int-Variablen und große für Zeigervariablen. Dengenerischen Variablennamen z verwenden wir, wenn wir sowohl int-Variablen wieZeigervariablen meinen. Zusätzlich gestatten wir als besondere Zeigerkonstante null.Als weitere Konzepte betrachten wir:

• Eine Anweisung z ← new(e) für einen Ausdruck e und eine Zeigervariable z.Der Operator new() stellt einen neuen Block im Speicher zur Verfügung undliefert einen Zeiger auf den Anfang des Blocks zurück, der in z abgelegt wird.Die Größe dieses Blocks ist durch den Wert des Ausdrucks e gegeben.

Page 79: œbersetzerbau: Band 3: Analyse und Transformation

1.11 Aliasanalyse 69

• Eine Anweisung z ← R[e] für eine Zeigervariable R, einen Ausdruck e undeine Variable z, die den Wert aufnehmen soll. Angelehnt an die Adressierung imstatisch allokierten Feld M in unserer bisherigen Sprache, liefert die Indizierungdes Zeigers R mit dem Wert des Ausdrucks e den Inhalt der entsprechendenStelle innerhalb des Blocks, auf den R zeigt.

• Eine Anweisung R[e1] ← e2 für eine Zeigervariable R, einen Ausdruck e1 undeinen Ausdruck e2, der den neuen Inhalt für die entsprechende Speicherzellebereitstellt.

Was wir nicht gestatten, ist Zeigerarithmetik, d.h. arithmetische Operationen mit Zei-gervariablen. Der Einfachheit halber verzichten wir auch auf die Einführung einesTypsystems, welches sicherstellt, dass Zeigerwerte und int-Werte unterschieden wer-den, dass eine Variable entweder nur int-Werte oder nur Zeigerwerte enthält und zurIndizierung wie für arithmetische Operationen nur int-Werte verwendet werden.

Ist der Wert der Zeigervariable R1 gleich dem Wert der Zeigervariable R2, d.h.zeigen sie auf den gleichen Speicherbereich, nennen wir R1 einen Alias von R2. Einewichtige Frage in Anwesenheit von dynamischer Speicherverwaltung ist, ob zweiZeigervariablen an einem Programmpunkt möglicherweise den gleichen Wert haben.Dieses Problem heißt May-Alias-Problem. Ebenfalls wichtig ist, herauszufinden, obzwei Zeigervariablen bei Erreichen eines Programmpunkts immer gleich sind. DiesesProblem heißt entsprechend Must-Alias-Problem.

Aliasinformation ist notwendig, wenn wir unsere bisherigen Analysen nicht mehrauf int-Variablen und int-Ausdrücke beschränken, sondern auch Werte im Speichereinbeziehen wollen. Betrachten wir z.B. unsere bisherige Analyse der verfügbarenZuweisungen. Eine unmittelbare Verallgemeinerung würde so vorgehen:

• Wir erweitern die Menge Ass der Zuweisungen um die im Programm vorkom-menden Ladeanweisungen z ← R[e]. Sei Def die sich ergebende Menge vonAnweisungen.

• Wir erweitern die Kanteneffekte:

[[R ← new(e)]]� A = A\Occ(R)

[[z ← e]]� A =

{A\Occ(z) ∪ {z ← e} falls z �∈ Vars(e)A\Occ(z) falls z ∈ Vars(e)

[[z ← R[e]]]� A =

{A\Occ(z) ∪ {z ← R[e]} falls z �∈ Vars(e) ∪ {R}A\Occ(z) falls z ∈ Vars(e) ∪ {R}

[[R[e1] ← e2]]� A = A\Loads

Dabei bezeichnet Occ(z) die Menge der Definitionen, welche die Variable z enthal-ten, und die Menge Loads besteht aus allen Speicherzugriffen der Form z ← R[e].Alle anderen Kanten haben keinen Effekt auf die Menge A der verfügbaren Defini-tionen. Wir bemerken, dass sämtliche Informationen über aus dem Speicher geladen-de Werte verloren gehen, wenn nur irgendwelche Werte in den Speicher geschriebenwerden.

Beispiel 1.11.1 Ein erstes Beispiel wird in Abb. 1.30 dargestellt. Das Programm

Page 80: œbersetzerbau: Band 3: Analyse und Transformation

70 1 Grundlagen und intraprozedurale Optimierung

X ← new(2);Y ← new(2);X[0] ← Y;Y[1] ← 7;

Y[1] ← 7

X[0] ← Y

1

Y ← new(2)2

3

4

0

X ← new(2)

Abb. 1.30. Ein einfaches Programm mit Kontrollflussgraph.

allokiert zwei Blöcke. An der Adresse 0 des ersten Blocks wird ein Verweis aufden zweiten Block abgespeichert, während an die Adresse 1 des zweiten Blocks derWert 7 abgespeichert wird. Abb. 1.31 zeigt den Programmzustand nach Ausführungdieses Programms. ��

YX

01

01 7

Abb. 1.31. Der Programmzustand nach Ausführung des Programms aus Abb. 1.30.

Beispiel 1.11.2 Als etwas schwierigeres Beispiel betrachten wir ein Progammstück,das eine gegebene Liste umdreht (Abb. 1.32). Dieses Programmstück ist sehr kurz.Genau zu verstehen, wieso der Algorithmus funktioniert, erfordert dennoch Nach-denken. Dieses Beispiel dokumentiert damit recht gut, dass Programme mit nichttri-vialer Benutzung von Zeigern schwer verständlich sind und damit anfällig für subtileFehler. ��Bevor wir eine Analyse von May-Aliasen entwickeln, wollen wir zuerst eine kon-krete Semantik für unsere Programmiersprache bereitstellen. Den Speicher stellenwir uns jetzt nicht mehr als eine (potentiell unendliche) Folge von Speicherzellenvor, sondern als eine (potentiell unendliche) Folge von Blöcken, von denen wir je-weils wieder annehmen wollen, dass sie aus einer Folge von Speicherzellen beste-hen. Jede Operation new() stellt einen weiteren solchen Block zur Verfügung. Vorder Programmausführung können wir nicht wissen, wie groß die Blöcke sind, die die

Page 81: œbersetzerbau: Band 3: Analyse und Transformation

1.11 Aliasanalyse 71

R ← null;A : if (T �= null) {

H ← T;T ← T[0];H[0] ← R;R ← H;goto A;

}

3

4

5

6

2

R ← H

H[0] ← R

T ← T[0]

H ← T7

1

0

R ← null;

NonZero(T �= null)Zero(T �= null)

Abb. 1.32. Ein Programm zur Listenumkehr.

Operation new() zur Verfügung stellt. Deshalb nehmen wir bei der Formalisierungder Semantik an, dass jeder allokierte Block (potentiell) unendlich viele Speicherzel-len enthält – von denen jedoch bei jeder Programmausführung nur maximal so vielebenutzt werden, wie bei Anlegen des Blocks angegeben.

Addrh = {null} ∪ {ref a | a ∈ {0, . . . , h}} Adressen

Valh = Addrh ∪Z Werte

Storeh = (Addrh ×N0}) → Valh Speicher

Stateh = (Vars → Valh) × {h} × Storeh

State =⋃

h≥0 Stateh Zustände

Die Menge der Werte enthält nun neben ganzen Zahlen Adressen von Blöcken.Adressen stellen die Werte von Zeigervariablen dar. Beachten Sie, dass wir hier Zei-ger nur an den Anfang und nicht auch ins Innere von Blöcken erlauben. Ein Pro-grammzustand besteht aus einer Belegung von Variablen und einem Speicher. DerSpeicher ordnet jeder Zelle in jedem bereits allokierten Block einen Wert zu. Zu-sätzlich vermerken wir im Programmzustand, wie oft die Operation new() bereitsaufgerufen wurde, d.h. wie viele Blöcke bereits allokiert wurden.

Sei (ρ, h, μ) ∈ State ein Programmzustand. Dann erhalten wir für die neuenOperationen die folgenden (konkreten) Kantenefekte:

[[R ← new(e)]] (ρ, h, μ) = (ρ ⊕ {R → ref h}, h + 1,μ ⊕ {(ref h, i) → null | i ∈ N0})

[[z ← R[e]]] (ρ, h, μ) = (ρ ⊕ {z → μ (ρ R, [[e]]ρ)}, h, μ)[[R[e1] ← e2]] (ρ, h, μ) = (ρ, h, μ ⊕ {(ρ R, [[e1]]ρ) → [[e2]]ρ})

Page 82: œbersetzerbau: Band 3: Analyse und Transformation

72 1 Grundlagen und intraprozedurale Optimierung

Die komplizierteste Operation ist die Operation new(). Gemäß unserer Semantikführt die Operation new() die folgenden Schritte aus:

1. sie berechnet die Größe des neuen Blocks;2. sie stellt den neuen Block bereit;3. sie initialisiert alle Speicherzellen innerhalb des Blocks mit null (wir hätten hier

auch irgendeinen anderen Wert wählen können);4. sie liefert einen Zeiger auf den Anfang des neuen Blocks zurück.

Tatsächlich ist unsere Semantik zu detailliert, weil sie mit absoluten Adressen rech-net. Die beiden Programme:

X ← new(4);Y ← new(4);

Y ← new(4);X ← new(4);

werden von ihr nicht als äquivalent betrachtet. Ein Ausweg besteht darin, dass wirÄquivalenz von Programmzuständen definieren nur bis auf Permutation der in denZuständen vorkommenden Adressen.

Unser erstes Ziel ist, für jede Zeigervariable eine (Beschreibung der) Obermengealler ihrer Werte zu ermitteln, d.h. aller Adressen, die in der Variablen enthalten seinkönnen. Eine solche Analyse nennen wir Points-to-Analyse. Ausgehend von der kon-kreten Semantik, definieren wir für diese Analyse eine abstrakte Semantik. Anstellevon potentiell unendlich vielen konkreten Adressen wollen wir dabei nur endlich vie-le abstrakte Adressen zu unterscheiden. Auch wollen wir nicht mehr zwischen denverschiedenen Positionen innerhalb eines durch eine (abstrakte) Adresse identifizier-ten Speicherblocks unterscheiden, sondern für jeden Block nur eine Menge der darinmöglicherweise enthaltenen Adressen verwalten.

Verschiedene Points-to-Analysen unterscheiden sich darin, welche Mengen ab-strakter Adressen verwendet werden. Hier beschränken wir uns auf den Ansatz, allean einer Kante (u, R ← new(e), v) erzeugten Adressen durch eine abstrakte Adres-se zu beschreiben, die wir mit dem Anfangspunkt u der Kante identifizieren. Wirdefinieren:

Addr� = Nodes Erzeugungsstellen

Val� = 2Addr�

Abstrakte Werte

Store� = Addr� → Val� abstrakter Speicher

State� = (Pointer → Val�) × Store� Zustände

Dabei ist Pointer ⊆ Vars die Menge der Zeigervariablen. Unsere abstrakten Zustän-de ignorieren damit sämtliche int-Werte und auch die spezielle Zeigerkonstante null.Auf abstrakten Zuständen gibt es eine natürliche Ordnungsrelation, die sich von derMengeninklusion herleitet:

(ρ�1, μ�

1) � (ρ�2, μ�

2) falls (∀ R ∈ Pointer. ρ1(R) ⊆ ρ2(R)) ∧(∀ u ∈ Addr�. μ

�1(u) ⊆ μ

�2(u))

Page 83: œbersetzerbau: Band 3: Analyse und Transformation

1.11 Aliasanalyse 73

Y[1] ← 7

X[0] ← Y

1

Y ← new(2)2

3

4

0

X ← new(2) X Y 0 1

0 ∅ ∅ ∅ ∅1 {0} ∅ ∅ ∅2 {0} {1} ∅ ∅3 {0} {1} {1} ∅4 {0} {1} {1} ∅

Abb. 1.33. Die abstrakten Zustände für das Programm aus Beispiel 1.11.1.

Beispiel 1.11.3 Betrachten wir erneut das Programm aus Beispiel 1.11.1. Die ab-strakten Zustände für die verschiedenen Programmpunkte zeigt Abb. 1.33. In diesemBeispiel geht keine Information verloren, da einerseits jede Kante, an der ein neu-er Block allokiert wird, nur einmal besucht wird, und andererseits jeder Block nurmaximal einmal eine Adresse aufnimmt. ��Die Kanteneffekte für unsere Points-to-Analyse ergeben sich zu:

[[(_, R1 ← R2, _)]]� (D, M) = (D ⊕ {R1 → D R2}, M)[[(u, R ← new(e), _)]]� (D, M) = (D ⊕ {R → {u}}, M)[[(_, R1 ← R2[e], _)]]� (D, M) = (D ⊕ {R1 → ⋃{M a | a ∈ D R2}}, M)

[[(_, R1[e1] ← R2, _)]]� (D, M) = (D, M ⊕ {a → (M a) ∪ (D R2) | a ∈ D R1})Alle weiteren Kanten verändern den abstrakten Zustand nicht. Die Kanten-Effektehängen jetzt von der ganzen Kante ab. Dies gilt zumindest für Kanten, an denenneue Blöcke allokiert werden. Zuweisungen an eine Variable überschreiben den ent-sprechenden Eintrag in der Variablenbelegung D destruktiv. Beim Lesen aus einemBlock im Speicher muss zusätzlich beachtet werden, dass die Adresse des Blockseventuell nicht genau bekannt ist. Um auf der sicheren Seite zu sein, wird der neueWert für eine Zeigervariable auf der linken Seite darum als Vereinigung der Beiträ-ge sämtlicher Blocks definiert, deren abstrakte Adresse möglicherweise vorkommt.Beim Schreiben in den Speicher muss beachtet werden, dass möglicherweise eineMenge von abstrakten Zieladressen a vorliegt und zusätzlich jeder solchen abstrak-ten Adresse möglicherweise eine Menge von konkreten Adressen entspricht. Die Ab-speicherung kann deshalb nicht destruktiv erfolgen, sondern kann die Menge Adres-sen, die als neuer abstrakter Wert in Frage kommt, nur zu den jeweiligen MengenM a hinzufügen.

Daraus folgt insbesondere, dass ohne Vorinitialisierung jedes neuen Blocks unse-re Analyse für jeden Block annehmen müsste, dass er jeden möglichen Wert enthält.Lieferte die Operation new() nicht vorinitialisierte Blöcke zurück, könnte unsereAnalyse deshalb keine sinnvollen Informationen über den Speicher zurück liefern!

Page 84: œbersetzerbau: Band 3: Analyse und Transformation

74 1 Grundlagen und intraprozedurale Optimierung

Alternativ könnten wir auch annehmen, dass bei einer korrekten Programmausfüh-rung niemals der Inhalt einer uninitialisierten Speicherzelle als Adresse verwendetwird. Unter dieser Annahme verhält sich das Programm exakt genau so wie einProgramm, bei dem jede Zelle eines neu allokierten Blocks vor der Benutzung desBlocks mit null initialisiert wurde.

Basierend auf unserem abstrakten Bereich State� und unseren abstrakten Kan-teneffekten stellen wir für jeden Kontrollflussgraphen ein Ungleichungsystem auf.Vor der Programmausführung ist nichts über die Werte der Zeigervariablen bekannt.Da es noch keine Speicherblocks gibt, nehmen wir darum den abstrakten Zustand(D∅, M∅) an mit

D∅ x = ∅, D∅ R = Addr�, M∅ a = ∅für alle int-Variablen x, alle Zeigervariablen R und alle abstrakten Adressen a.

Sei P [v], v Programmpunkt, die kleinste Lösung unseres Ungleichungssystems.Die kleinste Lösung liefert uns für jeden Programmpunkt v einen abstrakten ZustandP [v] = (D, M), welcher für jede Zeigervariable R eine Obermenge der abstraktenAdressen von Speicherblocks liefert, auf den R bei Erreichen von v zeigt. Insbe-sondere wissen wir, dass R kein Alias einer anderen Zeigervariable R′ ist, wenn(D R) ∩ (D R′) = ∅.

Beachten Sie hier, dass wir den konkreten Wert null in unserem abstrakten Zu-stand nicht mitmodelliert haben. Dereferenzieren von null kann von einer Analysemit unseren abstrakten Zuständen darum nicht erkannt werden. Wir würden gernedie Korrektheit unserer Analyse beweisen können. Zu unserer Enttäuschung müssenwir feststellen, dass dies gegenüber unserer Referenz-Semantik nicht möglich ist.Das liegt daran, dass wir für verschiedene Programmausführungen i.a. nicht garan-tieren können, dass die h-te Allokation eines Blocks stets an der selben Kante imKontrollflussgraphen erfolgt. Andererseits sollte aber die genaue Nummer h keinesemantische Signifikanz haben. Eine Lösung besteht darin, dass wir die Korrektheitnicht relativ zu der Referenz-Semantik beweisen, sondern relativ zu einer konkretenSemantik, die wir für unsere Zwecke mit Zusatzinformationen instrumentiert ha-ben. In unserem Fall verwenden wir als konkrete Adressen nicht einfach die Werteref h, h ∈ N0. Stattdessen verwenden wir:

Addr = {ref (u, h) | u ∈ Nodes, h ∈ N0}In der Adresse wird nun der Ausgangsknoten u der Kante vermerkt, an der ein neu-er Block allokiert wird. Die derart gruppierten Adressen lassen sich leicht unserenabstrakten Adressen zuordnen. Haben wir die Korrektheit unserer Analyse relativzu der instrumentierten konkreten Semantik nachgewiesen, müssen wir als zweitesnachweisen, dass die instrumentierte konkrete Semantik äquivalent zur Referenz-Semantik ist. Aufg. 22 gibt Ihnen Gelegenheit, diese Idee genauer auszuführen.

Die May-Alias-Analyse, so wie wir sie bisher vorgestellt haben, verwaltet fürjeden Programmpunkt einen eigenen abstrakten Speicher. Gibt es viele abstrakteAdressen, kann dessen Repräsentation sehr aufwendig sein. Weil unsere abstrakten

Page 85: œbersetzerbau: Band 3: Analyse und Transformation

1.11 Aliasanalyse 75

Kanteneffekte keine destruktiven Operationen auf dem abstrakten Speicher bereit-stellen, unterscheiden sich die abstrakten Speicher an den Programmpunkten inner-halb einer Schleife nicht!

Um den Preis eines möglicherweise verschmerzbaren Genauigkeitsverlusts, könn-te man überhaupt nur einen abstrakten Zustand (D, M) zu berechnen, der dann diekonkreten Zustände an sämtlichen Programmpunkten beschreibt. Eine solche Ana-lyse nennen wir flussunabhängig.

Beispiel 1.11.4 Betrachten wir erneut unser einfaches Programm aus Beispiel 1.11.1.Das erwartete Ergebnis der Analyse zeigt Abb. 1.34. Da jede Programmvariable und

Y[1] ← 7

X[0] ← Y

1

Y ← new(2)2

3

4

0

X ← new(2)

X Y 0 1

{0} {1} {1} ∅

Abb. 1.34. Das Ergebnis flussunabhängiger Analyse für das Programm aus Beispiel 1.11.1.

jede Speicherzelle maximal einmal einen Wert erhält, beeinträchtigt die Flussabhän-gigkeit in diesem Beispiel das Ergebnis nicht. ��Für die Implementierung der flussunabhängigen Analyse betrachten wir nicht deneinen abstrakten Zustand als ganzes. Vielmehr führen wir für jede Programmvaria-ble R und jede abstrakte Adresse a eine eigene Unbekannte P [R] bzw. P [a] ein.Eine Kante (u, lab, v) des Kontrollflussgraphen gibt jeweils Anlass zu den folgen-den Ungleichungen:

lab Ungleichungen

R1 ← R2 P [R1] ⊇ P [R2]R ← new(e) P [R] ⊇ {u}

R1 ← R2[e] P [R1] ⊇ ⋃{P [a] | a ∈ P [R2]}R1[e] ← R2 P [a] ⊇ (a ∈ P [R1]) ?P [R2] : ∅ für alle a ∈ Addr�

Andere Kanten haben keinen Effekt. In diesem Ungleichungssystem sind nun auchdie Ungleichungen für Zuweisungen an Zeigervariablen oder Leseoperationen nichtmehr destruktiv. Damit wir für Zeigervariablen nicht-triviale Ergebnisse berechnenkönnen, nehmen wir auch für Zeigervariablen an, dass sie bei Programmstart mit null

Page 86: œbersetzerbau: Band 3: Analyse und Transformation

76 1 Grundlagen und intraprozedurale Optimierung

initialisiert werden, oder – alternativ – dass auf ihren Wert erst nach einer Initialisie-rung zugegriffen wird. Weil die rechten Seiten der vorkommenden Ungleichungenmonotone Funktionen über Mengen von Adressen repräsentieren, besitzt das Unglei-chungssystem eine kleinste Lösung P1[R], R ∈ Pointer, P1[a], a ∈ Addr�. Diesekleinste Lösung können wir etwa mit dem Round-Robin-Algorithmus berechnen.

Zur Korrektheit einer Lösung s� ∈ State� des Ungleichungssystems genügt es,für jede Kante k im Kontrollflussgraphen zu zeigen, dass das folgende Diagrammkommutiert:

s s1

s�

[[k]]

Δ Δ

wobei Δ die Beschreibungsrelation zwischen konkreten und abstrakten Werten ist.Das Ungleichungssystem hat die Größe O(k · n), falls k die Anzahl der benötigtenabstrakten Adressen und n die Anzahl der Kanten im Kontrollflussgraphen ist. Weildie Werte, mit denen der Fixpunktalgorithmus rechnet, Mengen der Kardinalität ma-ximal k sind, können sich die Werte für jede der Unbekannten P1[R],P1[u] maxi-mal k-mal ändern. Im Verhältnis zur Genauigkeit der Information ist diese Verfahrendamit immer noch relativ teuer. Oft sind wir auch weniger an den einzelnen Men-gen P1[R],P1[u] selbst interessiert, als vielmehr daran, ob sie einen gemeinsamenDurchschnitt haben!

Die letzte Idee, die wir darum hier diskutieren wollen, verzichtet darauf, für Va-riablen und abstrakte Adressen ihre möglichen Werte zu approximieren. Stattdessenberechnet sie auf der Menge der Variablen R und Ausdrücke R[e] eine Äquivalenz-relation. Da es auf die genaue Form der Ausdrücke e nicht ankommt, repräsentierenwir alle Zeigerausdrücke der Form R[e] durch R[]. Der formale Zeigerausdruck R[]steht für alle Zellen der Blöcke, auf die R möglicherweise zeigt.

Sei Z die Menge Z = {R, R[] | R ∈ Pointer}. Dann suchen wir eine Äquiva-lenzrelation ≡ ⊆ Z × Z, so dass r1 ≡ r2 für zwei formale Zeigerausdrücke aufdann gilt, wenn r1 und r2 gleiche Adressen enthalten.

Beispiel 1.11.5 Betrachten wir wieder einmal unser kleines Programm aus Beispiel1.11.1. Eine entsprechende Äquivalenzrelation zeigt Abb. 1.35. Die Äquivalenzre-lation gibt direkt Auskunft, welche Zeigerausdrücke möglicherweise den gleichenZeigerwert (verschieden von null) enthalten. ��Sei EQ die Menge der Äquivalenzrelationen auf Z. Eine Äquivalenzrelation ≡1betrachten wir als kleiner oder gleich einer anderen Äquivalenzrelation ≡2, falls≡2 mehr Gleichheiten enthält als ≡1, d.h. falls ≡1⊆≡2. Bzgl. dieser Ordnung istEQ ein vollständiger Verband. Wie die vorhergehende Analyse soll auch die neueAnalyse flussunabhängig sein, d.h. es soll eine Äquivalenzrelation für das gesamteProgramm berechnet werden. Jede Äquivalenzrelation ≡ können wir als Partition

Page 87: œbersetzerbau: Band 3: Analyse und Transformation

1.11 Aliasanalyse 77

Y[1] ← 7

X[0] ← Y

1

Y ← new(2)2

3

4

0

X ← new(2)

≡ = {{X}, {Y, X[]}, {Y}, {Y[]}}

Abb. 1.35. Die Äquivalenzklassen der Relation ≡ für das Programm aus Beispiel 1.11.1.

π = {P1, . . . , Pm} von als äquivalent zu betrachtenden Zeigerausdrücken repräsen-tieren. Seien ≡1,≡2 Äquivalenzrelationen und π1, π2 die zugehörigen Partitionen.Dann gilt ≡1⊆≡2 genau dann wenn die Partition π1 eine Verfeinerung der der Par-tition π2 ist, d.h. falls jede Äquivalenzklasse P1 ∈ π1 in einer ÄquivalenzklasseP2 ∈ π2 enthalten ist.

Eine einzelne Äquivalenzklasse P ⊆ Z einer Äquivalenzrelation π identifizierenwir durch einen Repräsentanten p ∈ P. Der Einfachheit halber wählen wir diesen inPointer, wann immer P ∩ Pointer �= ∅. Sei π = {P1, . . . , Pr} eine Partition und pi

der Repräsentant der Äquivalenzklasse Pi. Für unsere Analyseverfahren benötigenwir die folgenden Operationen auf π :

Pointer find (π , p) liefert den Repräsentanten der Klasse Pi mit p ∈ Pi

Partition union (π , pi1 , pi2 ) liefert {Pi1 ∪ Pi2} ∪ {Pj | i1 �= j �= i2}d.h. vereinigt die zwei repräsentierten Klassen

Sind R1, R2 ∈ Pointer äquivalent, müssen auch R1[] und R2[] als äquivalent be-trachtet werden. Deshalb werden wir die Operation union stets rekursiv anwenden:

Partition union∗ (π , q1, q2) {pi1 ← find (π , q1);pi2 ← find (π , q2);if (pi1 = pi2 ) return π ;else {

π ← union (π , pi1 , pi2 );if (pi1 , pi2 ∈ Pointer) return union∗ (π , pi1 [], pi2 []);else return π ;

}}

Page 88: œbersetzerbau: Band 3: Analyse und Transformation

78 1 Grundlagen und intraprozedurale Optimierung

Die Operation union wie die abgeleitete Operation union∗ verhalten sich monotonauf Partitionen. Die Aliasananalyse, die wir mit diesen Operationen konstruieren,iteriert genau einmal über die Kanten des Kontrollflussgraphen. Sobald er auf eineKante trifft, an der Zeiger verändert werden, unifiziert er linke und rechte Seite:

π ← {{R}, {R[]} | R ∈ Pointer};forall ((_, lab, _) Kante) π ← [[lab]]� π ;

Dabei ist:[[R1 ← R2]]� π = union∗ (π , R1, R2)

[[R1 ← R2[e]]]� π = union∗ (π , R1, R2[])[[R1[e] ← R2]]� π = union∗ (π , R1[], R2)

[[lab]]� π = π sonst

Beispiel 1.11.6 Wir betrachten erneut unserer Standardprogramm aus Beispiel 1.11.1.Die einzelnen Schritte unserer Analyse für dieses Programm zeigt Abb. 1.36. ��

Y[1] ← 7

X[0] ← Y

1

Y ← new(2)2

3

4

0

X ← new(2){{X}, {Y}, {X[]}, {Y[]}}

(0, 1) {{X}, {Y}, {X[]}, {Y[]}}(1, 2) {{X}, {Y}, {X[]}, {Y[]}}(2, 3) {{X}, {Y, X[]} , {Y[]}}(3, 4) {{X}, {Y, X[]}, {Y[]}}

Abb. 1.36. Die flussunabhängige Aliasanalyse für das Programm aus 1.11.1.

Beispiel 1.11.7 Schauen wir uns auch noch das Verhalten unserer Aliasanalyse fürdas Programm aus Beispiel 1.11.2 zur Listenumkehr an (Abb. 1.37). Das Ergebnisder Analyse ist allerdings nicht sehr aussagekräftig: jeder der Zeigerausdrücke kannein möglicher Alias jedes anderen Zeigerausdrucks sein. ��Die Aliasanalyse iteriert genau einmal über die Kanten. Das ist kein Zufall. Eine wei-tere Iteration würde die Partition nicht mehr verändern (siehe Aufg. 23). Das Verfah-ren berechnet folglich die kleinste Lösung des Ungleichungssystems über Partitionen:

P2 � [[lab]]� P2 , (_, lab, _) Kante des Kontrollflussgraphen

Wie die zweite Points-to-Analyse ist die Aliasanalyse flussunabhängig. Für ihre Kor-rektheit muss wieder angenommen werden, dass alle Zugriffe stets auf Zellen erfol-gen, die bereits initialisiert sind.

Page 89: œbersetzerbau: Band 3: Analyse und Transformation

1.11 Aliasanalyse 79

3

4

5

6

2

R ← H

H[0] ← R

T ← T[0]

H ← T7

1

0

R ← null;

NonZero(T �= null)Zero(T �= null) {{H}, {R}, {T}, {H[]}, {R[]}, {T[]}}(2, 3) { {H, T} , {R}, {H[], T[]} , {R[]}}(3, 4) { {H, T, H[], T[]} , {R}, {R[]}}(4, 5) { {H, T, R, H[], R[], T[]} }(5, 6) {{H, T, R, H[], R[], T[]}}

Abb. 1.37. .

Wir wollen den Aufwand abschätzen, den die Aliasanalyse benötigt. Sei k dieAnzahl der Zeigervariablen und n die Anzahl der Kanten im Kontrollflussgraphen.Jede Kante wird genau einmal betrachtet. Für jede Kante gibt es maximal einenAufruf der Funktion union∗. Die Funktionsaufrufe union∗ führen jeweils zwei Auf-rufe der Funktion find durch. Nur, wenn diese Aufrufe Repräsentanten zweier un-terschiedlicher Äquivalenzklassen liefern, wird die Operation union aufgerufen undgegebenenfalls rekursiv erneut union∗ aufgerufen. Am Anfang gibt es 2k Äquiva-lenzklassen. Da sich mit jedem Aufruf von union die Anzahl der Äquivalenzklassenverringert, kann es insgesamt maximal 2k − 1 Aufrufe der Operation union gebenund damit nur O(n + k) Aufrufe der Operation find.

Wir benötigen eine effiziente Datenstruktur, die die Operationen union und findunterstützt. Solche Union-Find-Datenstrukturen sind in der Literatur seit langem be-kannt. Der Vollständigkeit halber präsentieren wir hier eine besonders einfache Im-plementierung, die auf Robert E. Tarjan zurück geht. Eine Partition einer endlichenGrundmenge U wird als gerichteter Wald repräsentiert:

• Zu jedem u ∈ U gibt es einen Vaterverweis F[u] .• Ein Element u ist eine Wurzel in dem gerichteten Wald, wenn der Vaterverweis

von u auf u selbst zeigt, d.h. F[u] = u ist.

Alle Knoten, die mittelbar durch ihre Vaterverweise die gleiche Wurzel erreichenkönnen, bilden eine Äquivalenzklasse. deren Repräsentant die Wurzel ist.

Abb. 1.38 zeigt die Partition {{0, 1, 2, 3}, {4}, {5, 6, 7}} der Grundmenge U ={0, . . . , 7}. Der untere Teil zeigt die Repräsentation durch ein Feld F mit Vaterver-weisen, welche darüber graphisch visualisiert ist.

Bzgl. der gewählten Repräsentation, lassen sich die Operationen find und unionsehr leicht implementieren:

Page 90: œbersetzerbau: Band 3: Analyse und Transformation

80 1 Grundlagen und intraprozedurale Optimierung

0 1 2 3 6 74 50 1 2 3 6 74 5

0

1

3

2

4 7

5

6

1 1 3 1 4 7 5 7

Abb. 1.38. Die Partition π = {{0, 1, 2, 3}, {4}, {5, 6, 7}} auf der Menge {0, . . . , 7}, reprä-sentiert durch Vaterverweise.

find : Um den Repräsentanten der Äquivalenzklasse eines Elements u zu finden,genügt es, von u aus den Vaterverweisen so lange zu folgen, bis ein Elementu′ gefunden ist, dessen Vaterverweis wieder auf u′ zeigt.

union : Um die Äquivalenzklassen zu zwei Repräsentanten u1 und u2 zu vereinigen,muss nur der Vaterverweis einer dieser Elemente auf das andere umgesetzt wer-den. Das Ergebnis der union-Operation auf der Beispielpartition aus Abb. 1.38zeigt Abb. 1.39.

0 1 2 3 6 74 50 1 2 3 6 74 51 1 3 1 4 7 5 4

0

1

3

2

4

7

5

6

Abb. 1.39. Das Ergebnis der Operation union(π , 4, 7) für die Partition π aus Abb. 1.38.

Page 91: œbersetzerbau: Band 3: Analyse und Transformation

1.11 Aliasanalyse 81

Die Operation union erfordert nur O(1) viele Schritte. Die Kosten der Operationfind dagegen sind proportional zur Länge des Pfads von dem Element der Anfragebis zur Wurzel des zugehörigen Baums. Dieser Pfad kann im schlimmsten Fall sehrlang sein. Eine Idee, um lange Pfade zu verhindern, besteht darin stets den kleinerenBaum unter den größeren zu hängen. Mit dieser Strategie wird z.B. auf unserer Bei-spielpartition aus Abb. 1.38 die Operation union den Vaterverweis des Elements 4auf 7 setzen und nicht umgekehrt (Abb. 1.40).

0 1 2 3 6 74 50 1 2 3 6 74 5

0

1

3

2

4

7

1 1 3 1 7 7 5 7

5

6

Abb. 1.40. Das Ergebnis der Operation union(π , 4, 7) für die Partition π aus Abb. 1.38 beiBerücksichtigung der Größen der beteiligten Äquivalenzklassen.

Um bei der union-Operation jeweils den Repräsentanten der kleineren Klasseunter den Repräsentanten der größeren Klasse zu hängen, muss für jede Äquivalenz-klasse die Anzahl der darin enthaltenen Elemente mitverwaltet werden. Das verteu-ert die Kosten der union-Operation nur unwesentlich. Sei n die Anzahl der union-Operationen, die auf der Anfangspartition π0 = {{u} | u ∈ U} ausgeführt wurden.Dann ist die Länge der Pfade zu einer Wurzel höchstens O(log(n)). Folglich hatjede find-Operation nun maximal Kosten O(log(n)).

Erstaunlicherweise lässt sich die Datenstruktur weiter verbessern. Dazu werdenwährend einer find-Operation die Vaterverweise sämtlicher besuchter Elemente di-rekt auf die Wurzel des zugehörigen Baums umgelenkt. Das verteuert zwar jedefind-Operation um einen (kleinen) konstanten Faktor, verbilligt aber spätere find-Anfragen. Ein Beispiel, wie sich durch die Umsetzung dieser Idee die Pfade zurWurzel verkürzen, zeigt Abb. 1.41. Im linken Baum haben die Pfade eine Länge biszu 4. Bei einer find-Anfrage für den Knoten 6 werden die Knoten 3, 7, 5 und 6 direk-te Nachfolger der Wurzel 1. Dadurch verkürzen sich die Wege im Beispiel auf eineLänge maximal 2.

Für diese Implementierung einer union-find-Datenstruktur kann man nachwei-sen, dass n union-Operationen und m find-Operationen zusammen nur Zeit O((n +

Page 92: œbersetzerbau: Band 3: Analyse und Transformation

82 1 Grundlagen und intraprozedurale Optimierung

0 1 2 3 6 74 50 1 2 3 6 74 50 1 2 3 6 74 50 1 2 3 6 74 5

3

4

7

5

2

60

1

3

4

7

5

2

60

1

5 1 3 1 7 7 5 3 5 1 3 1 7 1 1 1

Abb. 1.41. Pfadkomprimierung durch die find-Operation für 6.

m) · log∗(n)) benötigen, wobei log∗ die inverse Funktion der iterierten Exponen-

tiation ist: log∗(n) ist die kleinste Zahl k, so dass n ≤ 22···2

für einen Turm vonExponentiationen der Höhe k ist. Die Funktion log∗ ist damit eine unglaublich lang-sam wachsende Funktion, die für alle praktischen Eingaben n einen Wert ≤ 5 liefert.Für einen Beweis der oberen Komplexitätsschranke verweisen wir auf die einschlägi-gen Lehrbücher über Algorithmen und Datenstrukturen, etwa das Buch von Cormen,Leiserson, Rivest und Stein [CLRS09].

Fazit 1.11.1 In diesem Kapitel haben wir Analysen zur Behandlung von Zeigerva-riablen und dynamisch allokiertem Speicher kennengelernt. Wir begannen mit einerPoints-to-Analyse, die für jeden Programmpunkt eine eigene Analyseinformationberechnet. Sie behandelt Zuweisungen an Programmvariablen destruktiv, kann aberbei Zugriffen auf dynamisch allokierte Speicherzellen nur alle dort möglicherwei-se eingetragenen Werte akkumulieren. Um den Preis, nun auch Programmvariablennicht mehr destruktiv zu behandeln, haben wir eine möglicherweise effizientere fluss-unabhängige Variante dieser Points-to-Analyse entwickelt, die nur noch eine Analy-seinformation liefert, welche sämtliche bei der Programmausführung vorkommen-den Programmzustände beschreibt. Unter der Voraussetzung, dass wir nur an Alias-information interessiert sind, haben wir schließlich als letztes eine flussunabhängigeAnalyse entwickelt, Zeigerausdrücke in Äquivalenzklassen möglicher Aliase einteilt.Diese Analyse basiert auf einer Union-Find-Datenstruktur und ist damit blitzschnell– findet aber bei Programmen mit komplexeren Zeigermanipulationen nicht sehr vielheraus. ��

Page 93: œbersetzerbau: Band 3: Analyse und Transformation

1.12 Fixpunktalgorithmen 83

1.12 Fixpunktalgorithmen

Im letzten Kapitel haben wir einigen Aufwand betrieben, um ein möglichst effizien-tes Verfahren zur Aliasanalyse zu entwickeln. Das legt die Frage nahe, wie man imAllgemeinen effizient (nach Möglichkeit kleinste) Lösungen von Ungleichungssy-stemen über vollständigen Verbänden berechnet. Das einzige praktische Verfahren,das wir bisher kennengelernt haben, um Lösungen von Ungleichungssystemen

xi � fi (x1, . . . , xn) , i = 1, . . . , n

zu bestimmen, ist die Round-Robin-Iteration. Sie lässt sich leicht implementierenund manuell nachvollziehen. Dieser Algorithmus hat aber Defizite. Zum einen be-nötigt er eine ganze Runde, um die Terminierung der Iteration festzustellen. Zumandern wertet er sämtliche rechten Seiten fi der Unbekannten xi neu aus, obwohlsich vielleicht in der letzten Runde nur der Wert einer einzigen Variablen geänderthat. Auch hängt die Laufzeit wesentlich von der Anordnung der Variablen ab.

Effizienter ist der Worklist-Algorithmus. Dieses Verfahren verwaltet die Mengeder Variablen xi, deren Werte möglicherweise nicht ihre Ungleichung erfüllen, ineiner Datenstruktur W, der Worklist. Die Variablen werden der Reihe nach aus Wentnommen. Für eine entnommene Variable xi wird der Wert der rechten Seite bzgl.der aktuellen Werte für die Unbekannten berechnet. Wird dieser Wert nicht von demvorherigen Wert für xi subsumiert, wird der Wert von xi durch einen Wert ersetzt,der sowohl den alten wie den neuen Wert für xi subsumiert. Durch die Entnahmeder Variablen xi ist die Worklist kürzer geworden. Falls sich für xi aber ein größererWert ergibt, sind möglicherweise die Ungleichungen nicht mehr erfüllt, deren rechteSeiten vom Wert der Variablen xi direkt abhängen. Diese möglicherweise nicht mehrerfüllten Ungleichungen bzw. ihre linke Seiten müssen deshalb für eine erneute Be-rechnung vorgemerkt, d.h. in die Worklist W eingefügt werden.

Zur Implementierung dieses Verfahrens benötigen wir für jede Unbekannte xi ei-ne Menge I[xi] aller Variablen, deren rechte Seite möglicherweise unmittelbar von xi

abhängt. In unseren bisherigen Anwendungen der Programmanalyse ist eine solcheunmittelbare Variablenabhängigkeit leicht zu identifizieren: bei einer Vorwärtsanaly-se beeinflusst der Wert für den Programmpunkt u den Wert für einen Programmpunktv unmittelbar, wenn es im Kontrollflussgraphen eine Kante von u nach v gibt. Ent-sprechend beeinflusst bei einer Rückwärtsanalyse der Wert für den Programmpunkt vden Wert für einen Programmpunkt u unmittelbar, wenn es im Kontrollflussgrapheneine Kante von u nach v gibt. Sind die rechten Seiten fi durch Funktionen gegeben,über deren Implementierung man nichts weiß, kann eine genaue Bestimmung derAbhängigkeiten schwierig sein.

In der Beschreibung des Algorithmus müssen wir deutlich zwischen den Unbe-kannten xi und ihren Werten unterscheiden. In der Worklist W werden Unbekannteverwaltet. Dazu führen wir ein Feld D ein, das der Einfachkeit halber mit den Unbe-kannten selbst indiziert wird. Der Eintrag D[xi] soll jeweils den aktuellen Wert derUnbekannten xi enthalten. In unserer Formulierung allgemeiner Ungleichungssyste-me hatten wir bisher angenommen, die rechten Seiten fi seien Funktionen des TypsDn → D. Um ausnutzen zu können, dass eine rechte Seite eventuell nur von sehr

Page 94: œbersetzerbau: Band 3: Analyse und Transformation

84 1 Grundlagen und intraprozedurale Optimierung

wenigen Variablen abhängt, ist es sinnvoll, für die rechten Seiten fi die Funktionali-tät:

fi : (X → D) → D

anzunehmen, wobei X = {x1, . . . , xn} die Menge der Unbekannten des Unglei-chungssystems ist. Eine solche Funktion fi erwartet eine Belegung der Unbekanntenxi mit Werten und liefert für diese einen Wert zurück. Greift die Funktion auf denWert einer Variablen xj zu, wird die Belegung für die Variable xj aufgerufen. Dieaktuelle Belegung der Unbekannten mit Werten liefert die Funktion eval:

D eval(xj) { return D[xj]; }Damit sieht die Implementierung des Worklist-Verfahrens so aus:

W ← ∅;forall (xi ∈ X) {

D[xi] ← ⊥; W ← W ∪ {xi};}while (exists xi ∈ W) {

W ← W\{xi};t ← fi eval;if (t �� D[xi]) {

D[xi] ← D[xi] � t;W ← W ∪ I[xi];

}}

Zur Verwaltung der Menge W von Variablen, deren rechte Seiten erneut ausgewertetwerden sollen, können wir eine einfache Listendatenstruktur verwenden. BeachtenSie, dass gemäß der letzten Zeile des Rumpfs der while-Schleife die Elemente ausI[xi] nur dann in die Datenstruktur W eingefügt werden müssen, wenn sie nichtbereits in W enthalten sind. Für diese Überprüfung kann ein weiteres Feld benutztwerden, das für jede Variable anzeigt, ob sie bereits in W enthalten ist oder nicht.

Beispiel 1.12.1 Zur Illustration des Worklist-Verfahrens betrachten wir erneut dasUngleichungssystem aus Beispiel 1.5.2:

x1 ⊇ {a} ∪ x3

x2 ⊇ x3 ∩ {a, b}x3 ⊇ x1 ∪ {c}

Die rechten Seiten dieses Systems sind durch Ausdrücke gegeben, aus denen wir dieVariablenabhängigkeiten direkt ablesen können. Wir finden:

Page 95: œbersetzerbau: Band 3: Analyse und Transformation

1.12 Fixpunktalgorithmen 85

I

x1 x3

x2

x3 x1, x2

Die Berechnungsschritte des Worklist-Verfahrens für dieses Ungleichungssystemzeigt Abb. 1.42. Dabei ist die nächste zu entnehmende Variable xi der in der aktu-ellen Worklist jeweils hervorgehoben. Insgesamt benötigen wir sechs Auswertungenrechter Seiten – darunter könnte es keine Round-Robin-Auswertung tun. ��

D[x1] D[x2] D[x3] W

∅ ∅ ∅ x1 , x2, x3

{a} ∅ ∅ x2 , x3

{a} ∅ ∅ x3

{a} ∅ {a, c} x1 , x2

{a, c} ∅ {a, c} x3 , x2

{a, c} ∅ {a, c} x2

{a, c} {a} {a, c}

Abb. 1.42. Die Worklist-basierte Fixpunktiteration für Beispiel 1.5.2.

Der nächste Satz fasst unsere Beobachtungen zu dem Worklist-Verfahren zusammen.Um die Laufzeit genauer spezifizieren zu können, erinnern wir uns, dass die Höhe heines vollständigen Verbands D die maximale Länge einer echt aufsteigenden Kettevon Elementen in D ist. Die Größe | fi| einer rechten Seite fi definieren wir als dieAnzahl der Variablen, auf die bei der Auswertung von fi möglicherweise zugegriffenwird. Beachten Sie, dass damit die Summe der Größen der rechten Seiten geradegegeben ist als:

∑xi∈X

| fi| = ∑xj∈X

#I[xj]

Das liegt daran, dass jede Variablenabhängigkeit xj → xi genau einmal in der Sum-me links und genau einmal in der Summe rechts gezählt wird. Die Größe eines Un-gleichungssystems mit der Menge von Unbekannten X definieren wir damit als dieSumme: ∑xi∈X(1 + #I[xi]). Mit diesen Definitionen finden wir:

Satz 1.12.1 Sei S ein Ungleichungssystem der Größe N über dem vollständigen Ver-band D der Höhe h > 0. Dann gilt:

1. Der Worklist-Algorithmus terminiert nach maximal h · N Auswertungen rechterSeiten;

Page 96: œbersetzerbau: Band 3: Analyse und Transformation

86 1 Grundlagen und intraprozedurale Optimierung

2. Der Worklist-Algorithmus liefert eine Lösung. Sind alle fi monoton, liefert er diekleinste Lösung.

Beweis. Zum Beweis der ersten Behauptung überlegen wir uns, dass jede Variablexi höchstens h-mal ihren Wert ändern kann. Damit wird auch die Liste I[xi] derVariablen, die von xi abhängen, höchstens h-mal zu der Worklist W hinzugefügt.Folglich ist die Anzahl an Auswertungen beschränkt durch:

n + ∑ni=1 h · # I[xi]

= n + h · ∑ni=1 # I[xi]

≤ h · ∑ni=1(1 + # I[xi])

= h · N

Von der zweiten Behauptung betrachten wir nur die Aussage für monotone rechteSeiten. Bezeichne D0 die kleinste Lösung des Ungleichungssystems. Zunächst zeigtman, dass zu jedem Zeitpunkt gilt:

D0[xi] � D[xi] für alle Unbekannten xi .

Anschließend überzeugt man sich, dass am Ende der Ausführung des Rumpfs derwhile-Schleife alle Variablen xi, die ihre Ungleichung möglicherweise nicht erfül-len, stets in der Worklist enthalten sind. Da bei der Terminierung des Algorithmusdie Worklist leer ist, schließen wir, dass dann alle Ungleichungen erfüllt sind unddas Feld D folglich eine Lösung repräsentiert. Da die kleinste Lösung des Unglei-chungssystems eine obere Schranke dieser Lösung ist, haben wir die kleinste Lösunggefunden. ��Genauso wie die Round-Robin-Iteration funktioniert das Worklist-Verfahren gemäßAussage von Satz 1.12.1 auch für Ungleichungssysteme, bei denen die rechten Sei-ten nicht monoton sind. In diesem Fall wird bei Terminierung allerdings nicht diekleinste, sondern irgendeine Lösung des Ungleichungssystems berechnet.

Sind dagegen sämtliche rechten Seiten fi monoton, kann der Algorithmus ver-einfacht werden, indem die Akkumulation bei der Neuberechnung der Werte für dieUnbekannten durch Überschreiben ersetzt wird:

D[xi] ← D[xi] � t; ==⇒ D[xi] ← t;

Soll stattdessen eine Iteration mit Widening realisiert werden, wird bei der Akku-mulation die kleinste obere Schranke zwischen altem und neuem Wert durch eineAnwendung des Widening-Operators “�– ” ersetzt:

D[xi] ← D[xi] � t; ==⇒ D[xi] ← D[xi]�– t;

Im Falle einer Narrowing-Iteration ersetzt man entsprechend:

D[xi] ← D[xi] � t; ==⇒ D[xi] ← D[xi]�– t;

Page 97: œbersetzerbau: Band 3: Analyse und Transformation

1.12 Fixpunktalgorithmen 87

wobei hier die Iteration in der while-Schleife nicht mit den Werten ⊥ für jede Va-riable startet, sondern für eine bereits berechnete Lösung des Ungleichungssystemsdurchgeführt wird.

In der Praxis hat sich das Worklist-Verfahren als sehr effizient erwiesen. DasWorklist-Verfahren hat nichtsdestoweniger zwei Nachteile:

• Der Algorithmus benötigt die unmittelbaren Abhängigkeiten zwischen den Un-bekannten, d.h. die Mengen I[xi].In unseren bisherigen Anwendungen waren diese Abhängigkeiten offensichtlich.Dies ist jedoch nicht in allen Anwendungen der Fall.

• Wird die rechte Seite für eine Unbekannte ausgewertet, werden die aktuellenWerte der dazu benötigten Unbekannten verwendet – egal, ob für diese Unbe-kannten bereits ein nicht-trivialer Wert vorliegt oder nicht. Besser wäre eine Stra-tegie, die erst versucht, einen möglichst guten Wert für eine Unbekannte xj zuberechnen, bevor ihr Wert verwendet wird.

Zur Verbesserung erweitern wir die Funktion eval um Selbstbeobachtung: bevor dieFunktion eval den Wert einer Variablen xj zurückliefert, protokolliert sie die Varia-ble xi, für deren rechte Seite der Wert von xj benötigt wird, d.h. die Funktion evalfügt xi zu der Menge I[xj] hinzu. Dazu wird der Funktion eval als erstes zusätzlichesArgument die Variable xi übergeben. Die Funktion eval soll aber nicht einfach denaktuellen Wert von xj zurückliefern, sondern einen möglichst guten Wert. Deshalbwird als erstes rekursiv die Berechnung eines möglichst guten Werts für xj angesto-ßen – sogar noch bevor die Variablenabhängigkeit zwischen xj und xi protokolliertwird. Insgesamt erhalten wir damit für die Funktion eval:

D eval (xi) (xj) { solve(xj);I[xj] ← {xi} ∪ I[xj];return D[xj];

}Zusammen mit der Funktion solve ergibt sich eine rekursive Berechnung einer Lö-sung. Damit die Rekursion jedoch nicht unendlich absteigt, verwaltet die Funktionsolve die Mengen called und stable. Die Menge called enthält jeweils die Mengealler Variablen, deren rechte Seite in Auswertung begriffen, aber noch nicht been-det ist. Die Menge stable ist eine Obermenge der Menge called. Zusätzlich enthältsie alle Variablen, für die (relativ zu den aktuellen Werten der Variablen aus called)der Fixpunkt bereits erreicht ist. Für die Variablen aus der Menge stable wird dieFunktion solve deshalb nichts tun.

Zu Beginn der Fixpunktiteration werden die Mengen called und stable jeweilsmit der leeren Menge initialisiert. Das Programm, das die Fixpunktiteration durch-führt, sieht so aus:

called ← ∅; stable = ∅;forall (xi ∈ X) D[xi] ← ⊥;forall (xi ∈ X) solve(xi);

Page 98: œbersetzerbau: Band 3: Analyse und Transformation

88 1 Grundlagen und intraprozedurale Optimierung

wobei die Prozedur solve wie folgt definiert ist:

void solve (xi){if (xi �∈ stable) {

called ← called ∪ {xi};stable ← stable ∪ {xi};t ← fi (eval (xi));called ← called\{xi};if (t �� D[xi]) {

D[xi] ← D[xi] � t;W ← I[xi]\called; I[xi] ← ∅;stable ← stable\W;forall (xi ∈ W) solve(xi);

}}

}Falls die Variable xi bereits stabil ist, terminiert der Aufruf solve(xi) sofort. An-dernfalls wird die Variable xi sowohl zu der Menge called wie der Menge stablehinzugefügt. Dann wird die rechte Seite fi der Variablen ausgewertet. Anstelle deraktuellen Variablenbelegung verwendet solve die partiell auf xi angewendete Funk-tion eval, die vor jedem Variablenzugriff den aktuell bestmöglichen Wert für dieseVariable berechnet und anschließend die Variable xi zu der betreffenden I-Mengehinzu gefügt.

Sei t der Wert, den die Auswertung der rechten Seite für xi liefert. Nach dieserAuswertung kann die Variable xi aus der Menge called entfernt werden. Wird derWert t von dem vorherigen Wert für xi subsumiert, wird der Aufruf von solve sofortverlassen. Andernfalls wird er mit der kleinsten oberen Schranke des alten Werts unddes neuen Werts t überschrieben. Die Änderung des Werts der Variablen xi muss nunzu allen Variablen propagiert werden, deren letzte Auswertung auf einen (kleineren)Wert von xi zugriff. Das heißt, dass deren rechte Seite erneut ausgewertet werdenmuss. Die Menge W dieser Variablen ist gegeben durch die Menge I[xi], aus der alleVariablen aus called entfernt wurden: die rechten Seiten der Variablen aus calledbefinden sich ja bereits in (Neu-)Auswertung. Ihre Neuauswertung wird nicht einweiteres Mal angestoßen.

Nach Berechnung der Menge W wird der alte Wert der Menge I[xi] nicht mehrbenötigt und darum auf die leere Menge zurück gesetzt: die angestoßene Auswertungder Variablen dieser Menge wird die betreffenden Variablenabhängigkeiten ja gege-benenfalls rekonstruieren! Die Variablen der Menge W können nicht mehr als stabilbetrachtet werden. Deshalb werden sie aus der Menge stable entfernt. Anschließendwird die Prozedur solve für alle Variablen der Menge W aufgerufen.

Beispiel 1.12.2 Wir betrachten erneut das Ungleichungssystem aus Bsp. 1.5.2 undBsp. 1.12.1:

Page 99: œbersetzerbau: Band 3: Analyse und Transformation

1.12 Fixpunktalgorithmen 89

x1 ⊇ {a} ∪ x3

x2 ⊇ x3 ∩ {a, b}x3 ⊇ x1 ∪ {c}

Eine Ausführung des rekursiven Fixpunktalgorithmus ist in Abb. 1.43 gezeigt. Nach

solve(x2) eval (x2) (x3) solve(x3) eval (x3) (x1) solve(x1) eval (x1) (x3) solve(x3)stable!

I[x3 ] = {x1}⇒ ∅

D[x1 ] = {a}I[x1 ] = {x3}⇒ {a}

D[x3 ] = {a, c}I[x3 ] = ∅solve(x1) eval (x1) (x3) solve(x3)

stable!I[x3 ] = {x1}⇒ {a, c}

D[x1 ] = {a, c}I[x1 ] = ∅solve(x3) eval (x3) (x1) solve(x1)

stable!I[x1 ] = {x3}⇒ {a, c}

ok

I[x3 ] = {x1 , x2}⇒ {a, c}

D[x2 ] = {a}

Abb. 1.43. Eine Ausführung des rekursiven Fixpunktalgorithmus.

rechts ist jeweils der Abstieg in einen rekursiven Aufruf dargestellt. In der Spalte ei-nes Aufrufs der Prozedur solve finden sich die ermittelten neuen Einträge D[xi] undI[xi] sowie die Aufrufe der Prozedur solve zur Behandlung der Variablen in W. Einok in der Spalte signalisiert, dass die erneute Auswertung der rechten Seite keine

Änderung des aktuellen Werts der Variablen erfordert. Ein stable! dagegen zeigt an,dass die Variable, für die das letzte solve aufgerufen wurde, stabil ist und der Aufrufdamit sofort terminiert. In der Spalte eines Aufrufs der Funktion eval werden eben-falls die vorgenommenen Änderungen der Mengen I[xj] angezeigt sowie der zurückgelieferte Wert.

Obwohl das Beispiel sehr klein ist, wertet dieser Algorithmus noch weniger rech-te Seiten aus als der Worklist-Algorithmus. ��

Page 100: œbersetzerbau: Band 3: Analyse und Transformation

90 1 Grundlagen und intraprozedurale Optimierung

Der rekursive Fixpunktalgorithmus lässt sich elegant mit einer Programmierspra-che wie z.B. OCAML implementieren, die einerseits Zuweisungen, andererseits aberauch partielle Anwendungen höherer Funktionen unterstützt.

Der rekursive Fixpunktalgorithmus ist komplizierter als der Worklist-Algorith-mus, führt i.A. aber zu weniger Auswertungen rechter Seiten und benötigt keine Vor-berechnung der Variablenabhängigkeiten, viel besser; er funktioniert sogar, wenn dieVariablenabhängigkeiten sich während der Fixpunktiteration ändern! Darüber hinaushat der rekursive Fixpunktalgorithmus eine weitere Besonderheit, die wir später beider interprozeduralen Analyse in Kapitel 2.3 ausnutzen werden:

Das Verfahren kann leicht so modifiziert werden, dass nicht die Werte für sämt-liche Unbekannte berechnet werden. Vielmehr können wir die Auswertung mit ei-ner interessierenden Unbekannten xi starten. Dann werden nur solche Unbekanntenausgewertet, deren Wert zur Berechnung des Werts der Unbekannten xi beiträgt.Fixpunktiterierer mit dieser Eigenschaft heißen lokal.

1.13 Beseitigung teilweiser Redundanzen

Wir wenden uns wieder der Frage zu, wie der Übersetzer die Programmausführungdurch die Beseitigung überflüssiger Zuweisungen beschleunigen kann. In Kapitel1.2 haben wir beschrieben, wie die erneute Auswertung eines Ausdrucks e entlangeiner Kante u → v eingespart werden kann, wenn der Wert des Ausdrucks e an demProgrammpunkt u in einer Variable x verfügbar ist. Dies ist dann der Fall, wenn dieZuweisung x ← e auf allen Pfaden vom Startpunkt des Programms nach u ausge-wertet wurde und keine Variable, die in der Zuweisung vorkommt, später verändertwurde. Diese Optimierung ersetzt ein redundantes Vorkommen von e durch die Va-riable x auf der linken Seite der Zuweisung. Bisher konnten wir ein redundantesVorkommen eines Ausdrucks e an einer Kante von u nach v nur ersetzen. wenn esVorkommen von x ← e auf allen Pfaden vom Startpunkt des Programms zu u gab.Nun wollen wir überlegen, ob wir eine Mehrfachberechnung auch dann vermeidenkönnen, wenn x ← e nur auf manchen Pfaden auftritt.

Beispiel 1.13.1 Betrachten Sie das Programm aus Abb. 1.44 links. Die ZuweisungT ← x + 1 wird auf jedem Pfad ausgewertet, auf dem rechten Pfad sogar zweimalund mit identischem Effekt. Auch wenn der Wert von x + 1, der an der Kante von 2nach 3 berechnet wird, in der Variablen T abgespeichert wird, kann das Vorkommenvon x + 1 an der Kante zwischen den Programmpunkten 3 und 4 nicht einfach durchden Zugriff auf den Wert von T ersetzt werden. Der Übersetzer kann die zweite Zu-weisung T ← x + 1 aber so verschieben, dass die doppelte Ausführung vermiedenwird. Diese Programmtransformation führt zu dem Programm in Abb. 1.44 rechts.��Gesucht ist eine Transformation, welche Zuweisungen x ← e mit x �∈ Vars(e) so indas Programm einfügt, dass einerseits die Variable x immer dann, wenn wir erneuteine Zuweisung x ← e ausführen, bereits den richtigen Wert enthält, andererseits

Page 101: œbersetzerbau: Band 3: Analyse und Transformation

1.13 Beseitigung teilweiser Redundanzen 91

0

1

5

6

7

2

3

4

0

1

5

6

7

2

3

4

0

1

5

6

7

2

3

4

M[x] ← T

x ← M[a]

M[T] ← 5

T ← x + 1

T ← x + 1

T ← x + 1

M[x] ← T

x ← M[a]

M[T] ← 5

T ← x + 1

T ← x + 1

T ← x + 1

M[x] ← T

x ← M[a]

M[T] ← 5

T ← x + 1

T ← x + 1

T ← x + 1

Abb. 1.44. Eine Beseitigung teilweiser Redundanz

aber überflüssige Mehrfachauswertungen vermieden werden. Das ist das Ziel der er-sten Phase der Transformation PRE (Einfügen von Abspeicherungen). In der zwei-ten Phase werden dann alle nun in vollständig redundanten Vorkommen von x ← edurch die leere Anweisung ersetzt.

Unsere Transformation fügt Zuweisungen x ← e (an möglichst wenig Stellen)vor Programmpunkten u ein, an denen die Zuweisung x ← e auf allen ausgehendenPfaden berechnet wird, bevor die linke Seite der Zuweisung benutzt wird oder eineder in ihr vorkommenden Variablen einen neuen Wert erhält. Eine solche Zuweisungx ← e nennen wir an dem Programmpunkt u sehr beschäftigt (very busy). ZurIdentifizierung dieser Zuweisungen benötigen wir eine neue Programmanalyse.

Wir nennen die Zuweisung x ← e beschäftigt entlang eines Pfades π zum Pro-grammende, falls π die Form π = π1kπ2 hat, wobei k eine Zuweisung x ← e istund π1 keine Benutzung der linken Seite x sowie keine Überschreibung einer derVariablen der Zuweisung, d.h. aus {x} ∪ Vars(e) enthält.

Am Programmpunkt v ist die Zuweisung x ← e sehr beschäftigt, falls sie entlangjedes Pfades von v zum Programmende beschäftigt ist. Damit ist die Bestimmung dersehr beschäftigten Zuweisungen eine Rückwärtsanalyse. Wie bei der Analyse derverfügbaren Zuweisungen benutzen wir als abstrakte Werte für die ProgrammpunkteMengen von Zuweisungen, d.h.:

B = 2Ass

Am Programmende ist keine Zuweisung sehr beschäftigt. Für eine Kante k =(u, lab, v) ist der abstrakte Kanteneffekt [[k]]� = [[lab]]� in Abhängigkeit von derKantenbeschriftung gegeben durch:

Page 102: œbersetzerbau: Band 3: Analyse und Transformation

92 1 Grundlagen und intraprozedurale Optimierung

[[; ]]� B = B[[NonZero(e)]]� B = [[Zero(e)]]� B = B\Ass(e)

[[x ← e]]� B =

{B\(Occ(x) ∪ Ass(e)) ∪ {x ← e} falls x �∈ Vars(e)B\(Occ(x) ∪ Ass(e)) falls x ∈ Vars(e)

[[x ← M[e]]]� B = B\(Occ(x) ∪ Ass(e))[[M[e1] ← e2]]� B = B\(Ass(e1) ∪ Ass(e2))

Hier bezeichnet Occ(x) die Menge aller Zuweisungen, in denen x vorkommt. DieAbkürzung Ass(e) für einen Ausdruck e bezeichnet die Menge aller Zuweisungen,deren linke Seiten in e vorkommen. Wir wollen die sicher sehr beschäftigten Zuwei-sungen ermitteln, d.h. wir müssen für jeden Programmpunkt u den Durchschnitt derBeiträge berechnen, den die einzelnen Pfade von u zum Programmende liefern. Folg-lich wählen wir auf der Menge 2Ass als Ordnungsrelation die Obermengenbeziehung⊇. Die MOP-Lösung für den Programmpunkt v ergibt sich damit zu:

B∗[u] =⋂{[[π ]]� ∅ | π : u →∗ stop}

wobei wie bei den anderen Analysen [[π ]]� den Effekt des Pfades π bezeichnet. Fürπ = k1 . . . km ist dabei wie bei jeder Rückwärtsanalyse:

[[π ]]� = [[k1]]� ◦ . . . ◦ [[km]]�

Die abstrakten Kanteneffekte [[ki]]� sind sämtlich distributiv. Deshalb liefert diekleinste Lösung B bzgl. der gewählten Anordnung der abstrakten Werte exakt dieMOP-Lösung – sofern von jedem Programmpunkt aus der Endpunkt des Programmsim Kontrollflussgraph erreichbar ist.

Beispiel 1.13.2 Abb. 1.45 zeigt die Mengen der sehr beschäftigten Zuweisungen fürdas Programm aus Beispiel 1.13.1. Da der Kontrollflussgraph azyklisch ist, kannman diese Mengen z.B. mit Round-Robin-Iteration in einer Runde berechnen. ��Es sei hier darauf hingewiesen, dass die (zumindest formale) Erreichbarkeit des Pro-grammendes von großer Wichtigkeit für die beabsichtigte Optimierung ist. Ist voneinem Programmpunkt u aus der Endpunkt des Programms nämlich nicht erreich-bar, würde die Analyse sehr beschäftigter Zuweisungen auch jede Zuweisung x ← ean u als sehr beschäftigt ausweisen, für den von u aus keine Neu-Definition einerVariable in Vars(e) ∪ {x} erreichbar ist.

Beispiel 1.13.3 Betrachten Sie das Programm aus Abb. 1.46. Das Programm gestat-tet nur eine unendliche Berechnung. Von dem Programmpunkt 1 aus ist folglich auchkein Endpunkt erreichbar. Damit ist an diesem Programmpunkt jede Zuweisung sehrbeschäftigt, die nicht die Variable x enthält — im Extremfall also auch Zuweisungen,die gar nicht im Programm selbst vorkommen. ��Das Einfügen einer verschiebbaren Zuweisung y ← e soll so erfolgen, dass dieseZuweisung an allen Programmpunkten, an denen y ← e sehr beschäftigt ist, auch

Page 103: œbersetzerbau: Band 3: Analyse und Transformation

1.13 Beseitigung teilweiser Redundanzen 93

0

1

5

6

7

2

3

4

M[x] ← T

x ← M[a]

M[T] ← 5

T ← x + 1

T ← x + 1

T ← x + 1

7 ∅6 ∅5 {T ← x + 1}4 ∅3 {T ← x + 1}2 {T ← x + 1}1 ∅0 ∅

Abb. 1.45. Die sehr beschäftigten Zuweisungen für Beispiel 1.13.1.

3

0

1

4 2

x ← a + b

a ← M[7]

Abb. 1.46. Ein Programm, bei dem der Endpunkt nicht erreichbar ist.

verfügbar ist. Da unsere Kontrollflussgraphen keine nicht-deterministische Verzwei-gungen enthalten, ist am Programmpunkt vor einer Kante, die mit y ← e beschriftetist, diese Zuweisung stets sehr beschäftigt. Unsere Strategie für die Einfügung be-steht darin, die Einfügungen so weit wie möglich nach vorne zu verschieben; mögli-cherweise sogar vor den ursprünglichen Startpunkt unseres Programms hinaus.

Die Vorverlegung wird jedoch sowohl durch Korrektheits- als auch durch Effizi-enzüberlegungen eingeschränkt: Aus Korrektheitsgründen dürfen wir die Zuweisungy ← e nicht über eine Kante hinweg schieben, an der y oder eine Variable aus eeinen neuen Wert erhält: nach einer solchen Verschiebung würde die Auswertungvon y ← e ja möglicherweise einen falschen Wert ergeben oder vor der Benutzungüberschrieben werden! Aus Effizienzgründen dürfen wir die Zuweisung y ← e nichtin einen Pfad schieben, auf dem y ← e vorher nicht ausgewertet würde: eine solcheVerschiebung führt entlang dieses Pfads zu einer Laufzeitverschlechterung. Hierbeiist zu beachten, dass nicht jede beliebige Zuweisung auf einen Pfad geschoben wer-den kann, der sie vorher nicht enthielt. Je nach Semantik der Programmiersprachekann die Zuweisung Seiteneffekte haben. Zum Beispiel die Auslösung einer Aus-

Page 104: œbersetzerbau: Band 3: Analyse und Transformation

94 1 Grundlagen und intraprozedurale Optimierung

nahme bei einer Division durch Null. Wenn die Semantik der Programmiersprachesolche Seiteneffekte zu den beobachtbaren Zuständen zählt, darf eine solche Anwei-sung nicht nur aus Effizien- sonderen auch aus Korrektheitsgründen nicht in einenPfad verschoben werden, in dem sie so bisher nicht vorkam.

Für die Umsetzung unserer Einfügestrategie betrachten wir zuerst einmal denStartpunkt start des Programms. Damit alle Zuweisungen aus B[start] nach derTransformation an start verfügbar werden, führen wir einen neuen Startpunkt einund berechnen die Zuweisungen aus B[start] vor Erreichen von start. Das bewirktdie erste Teilregel.

Transformation PRE für den Startknoten:

vv

B[v]

Wir haben uns hier die Freiheit genommen, an eine Kante evt. mehrere von einanderunabhängige Zuweisungen zu schreiben, d.h. in beliebiger Reihenfolge ausgeführtwerden dürfen. Dazu vergewissert man sich, dass für zwei Zuweisungen y1 ← e1,y2 ← e2, die gemeinsam in einer Menge B[u] vorkommen, y1 �≡ y2 ist, und wedery1 in e2, noch y2 in e1 vorkommt.

Betrachten wir als nächstes einen Programmpunkt u mit genau einer ausgehen-den Kante nach v mit Beschriftung s. An der Kante müssen wir alle Zuweisungenaus B[v] platzieren, die einerseits nicht über s hinweg vorgezogen werden dürfen,aber andererseits hinter s nicht verfügbar sind. Diese Menge ist gegeben durch

ss = B[v]\([[s]]�B(B[v]) ∪ [[s]]�A(A[u]))

Wie in Kapitel 1.2, bezeichnet A[u] die Menge der am Programmpunkt u verfügba-ren Zuweisungen. Zur Unterscheidung der abstrakten Kanteneffekte für verfügbareZuweisungen bzw. sehr beschäftigte Zuweisungen haben wir die zugehörigen Kan-teneffekte durch die Indizes A bzw. B gekennzeichnet.

Transformation PRE für leere Anweisungen und verschiebbare Zuweisungen:

v

u

;

v

u

vv

u

x ← e

u

; ss

Ist die Kante mit der leeren Anweisung beschriftet, ist die Menge ss der zu plat-zierenden Zuweisungen gegeben durch B[v]\(B[v] ∪A[u]) = ∅. Es muss deshalbkeine zusätzliche Zuweisung an der Kante platziert werden.

Bei einer verschiebbaren Anweisung d.h. einer Zuweisung x ← e mit x �∈Vars(e) wird ausschließlich die Folge ss platziert, da die Zuweisung x ← e an einen

Page 105: œbersetzerbau: Band 3: Analyse und Transformation

1.13 Beseitigung teilweiser Redundanzen 95

anderen Ort verschoben wird. Für die Menge ss der zu platzierenden Zuweisungenerhalten wir in diesem Fall:

ss = B[v]\(B[v]\(Occ(x) ∪ Ass(e)) ∪A[u]\Occ(x) ∪ {x ← e})= (B[v] ∩Occ(x)\{x ← e}) ∪ (B[v] ∩ Ass(e)\A[u])

Ist die Kante mit einer nicht verschiebbaren Anweisung s beschriftet, ersetzen wirdie Beschriftung s durch s, gefolgt mit den Zuweisungen ss.

Transformation PRE für nichtverschiebbare Anweisungen:

v

u

v

u

s s

ss

Ist die Kante von u nach v mit der nicht verschiebbaren Zuweisung x ← e beschriftetmit x ∈ Vars(e), erhalten wir für ss:

ss = B[v] ∩ (Occ(x) ∪ Ass(e))\(A[u]\Occ(x))= (B[v] ∩ (Occ(x)) ∪ (B[v] ∩ Ass(e)\A[u])

Die zu erzeugenden Zuweisungen ss für Leseoperationen x ← M[e] ergeben sichanalog. Für eine Schreiboperation M[e1] ← e2 erhalten wir:

ss = B[v] ∩ (Ass(e1) ∪ Ass(e2))\A[u]

Als letztes betrachten wir den Fall, dass u eine bedingte Verzweigung mit Bedin-gung b ist. Seien v1 bzw. v2 die Nachfolgeknoten, falls die Bedingung einen Wert0 bzw. verschieden von 0 liefert. Zuweisungen aus A[u] brauchen an keiner dervon u ausgehenden Kanten platziert werden, da sie an deren Enden bereits vor derTransformation verfügbar sind. Von den übrigen Zuweisungen in B[v1] müssen alleZuweisungen platziert werden, die nicht über u hinaus verschoben werden dürfen.Das sind diejenigen, die Variablen der Bedingung modifizieren oder die nicht auchin B[v2] enthalten sind. Analog behandeln wir die Kante zu v2.

Transformation PRE für Verzweigungen:

v2v1

u

v1 v2

Zero(b)

ss1 ss2

NonZero(b) NonZero(b)Zero(b)u

Page 106: œbersetzerbau: Band 3: Analyse und Transformation

96 1 Grundlagen und intraprozedurale Optimierung

wobeiss1 = (B[v1] ∩ Ass(b)\A[u]) ∪ (B[v1]\(B[v2] ∪A[u])ss2 = (B[v2] ∩ Ass(b)\A[u]) ∪ (B[v2]\(B[v1] ∪A[u])

Der Effekt der angegebenen Transformationsregeln für PRE ist, dass jede Zuwei-sung x ← e nach der Transformation PRE an allen Programmpunkten verfügbarist, an denen x ← e vor der Anwendung der Regeln sehr beschäftigt war. Damitist eine Zuweisung x ← e insbesondere an sämtlichen Stellen verfügbar, wo sie imursprünglichen Programm berechnet wurde.

Beispiel 1.13.4 Abb. 1.47 zeigt die Analyseinformation für unser Programm ausBsp. 1.13.1 zusammen mit dem Ergebnis der Transformation. Tatsächlich wurde die

2

1

0

3

4

5

7

6

x ← M[a]

T ← x + 1

T ← x + 1

M[x] ← T

M[T] ← 5

A B0 ∅ ∅1 ∅ ∅2 ∅ {T ← x + 1}3 ∅ {T ← x + 1}4 {T ← x + 1} ∅5 {∅} T ← x + 16 {T ← x + 1} ∅7 {T ← x + 1} ∅

Abb. 1.47. Das Ergebnis der Transformation PRE für unser Beispiel 1.13.1 zusammen mitden dafür erforderlichen Analysen.

teilweise Mehrfachberechnung beseitigt. ��Sei ss die Folge der am Programmpunkt v sehr beschäftigten Zuweisungen. Zur Kor-rektheit zeigt man für alle Ausführungspfade π des Programms vom ursprünglichenStartpunkt zu einem Programmpunkt v und alle Programmzustände σ vor der Pro-grammausführung, dass gilt:

[[ss]] ([[π ]] σ) = [[k0π ]]′ σ

wobei k0 die neue Kante zum Startpunkt des ursprünglichen Programms ist und [[π ]]bzw. [[π ]]′ die Semantik des Programmpfads π vor bzw. nach der Anwendung derTransformation ist. Die Gültigkeit der Aussage kann mit vollständiger Induktion be-wiesen werden. Für den leeren Programmausführungspfad π = ε ist sie sicherlich

Page 107: œbersetzerbau: Band 3: Analyse und Transformation

1.14 Anwendung: Schleifeninvarianter Code 97

richtig. Für einen nicht-leeren Pfad π = π ′k folgt sie aus der Induktionsvorausset-zung mit einer Fallunterscheidung nach den möglichen Beschriftungen der letztenKante k des Pfads.

Im Beispiel haben wir gesehen, dass sich die Anzahl der Auswertungen der Zu-weisung x ← e auf keinem Pfad erhöhte, auf einigen sich dagegen möglicherweiseverringert. Diese Eigenschaft der garantierten Nicht-Verschlechterung möchte mangerne für sämtliche Programme nachweisen. Wir werden diesen Beweis hier nichtführen. Intuitiv basiert er jedoch darauf, dass im transformierten Programm die Zu-weisung x ← e an allen Programmpunkten, an denen x ← e vorher sehr beschäftigtwar, nun verfügbar ist und deshalb gestrichen wird. Jeder in eine Programmaus-führung neu eingefügten Zuweisung lässt sich so mindestens eine darauffolgendeZuweisung zuordnen, die vermieden wird.

Die Beseitigung teilweise redundanter Zuweisungen beseitigt als Nebeneffektauch einige ganz redundante d.h. verfügbare Zuweisungen. Durch die Anwendungvon PRE können zusätzlich weitere Zuweisungen teilweise redundant werden! Eskann sich also lohnen, die Transformation PRE mehrmals auszuführen. ÄhnlicheMethoden können auch eingesetzt werden, um Speicherzugriffe einzusparen. Damitsich nicht sämtliche Lese- und Schreibanweisungen gegenseitig blockieren, kann ei-ne Alias-Analyse eingesetzt werden, welche garantiert, dass zwei Adressausdrückesich auf unterschiedliche Speicherzellen beziehen. Techniken dafür haben wir in Ka-pitel 1.11 entwickelt.

Die Transformation PRE erlaubt uns ebenfalls, schleifeninvarianten Code auseiner Schleife heraus zu verlagern. Dies wollen wir im nächsten Kapitel näher be-trachten.

1.14 Anwendung: Schleifeninvarianter Code

Eine überflüssige Mehrfachberechnung liegt vor, wenn eine Schleife eine Zuweisungenthält, die bei allen Schleifenausführungen denselben Wert berechnet.

Beispiel 1.14.1 Betrachten wir das folgende Programm:

for (i ← 0; i < n; i++) {T ← b + 3;a[i] ← T;

}Den Kontrollflussgraphen für dieses Programm zeigt Abb. 1.48. Die Schleife enthältdie Zuweisung T ← b + 3, welche bei jeder Iteration der Schleife denselben Wert inder Variablen T ablegt. Die Zuweisung T ← b + 3 darf jedoch nicht vor die Schleifegezogen werden, wie rechts in Abb. 1.48 angedeutet. Dort würde diese Zuweisungauch in Berechnungen ausgeführt, welche die Schleife gar nicht betreten! ��

Page 108: œbersetzerbau: Band 3: Analyse und Transformation

98 1 Grundlagen und intraprozedurale Optimierung

3

2

4

6

0

1

5

i ← 0

NonZero(i < n)Zero(i < n)

i ← i + 1

M[A + i] ← T

T ← b + 3

0

7

1

M[A + i] ← T

i ← i + 14 5

3

2

Zero(i < n) NonZero(i < n)

T ← b + 3

i ← 0

Abb. 1.48. Eine Schleife mit invariantem Code und eine ungeeignete Transformation.

Das Problem, keinen angemessenen Platz für den schleifeninvarianten Code zu fin-den, stellt sich bei do-while-Schleifen dagegen nicht. Darum erscheint es sinnvoll,vor dem Versuch, schleifeninvarianten Code zu entdecken, zuerst while-Schleifen indo-while-Schleifen umzuwandeln. Die entsprechende Transformation heißt Schleifen-umkehr (loop inversion).

Beispiel 1.14.2 Betrachten Sie erneut die while-Schleife aus Beispiel 1.14.1. Dieumgekehrte Schleife zeigt Abb. 1.49 links. In der umgekehrten Schleife gibt es ei-ne separate Abfrage vor dem ersten Betreten des Rumpfs. Am Ende des Rumpfserscheint dieselbe Abfrage ein weiteres Mal.

Die für die Transformation berechnete Analyseinformation zeigt Abb. 1.50. DieAnwendung der Transformation PRE zur Beseitigung der teilweisen Überflüssigkeitzeigt Abb. 1.49 rechts. Der schleifeninvariante Code wurde vor die do-while-Schleifegeschoben. ��Wir halten fest, dass die Transformation PRE schleifeninvariante Berechnungen ausdo-while-Schleifen herausziehen kann. Um auch while-Schleifen optimieren zu kön-nen, wandeln wir sie zuerst in do-while-Schleifen um. Haben wir den Quellcode fürdie Schleife zur Verfügung, ist das in den meisten imperativen und objektorientier-ten Programmiersprachen sehr einfach. In C oder JAVA zum Beispiel können wir diewhile-Schleife:

while (b) stmt

ersetzen durch:if (b) do stmt while (b);

In unserer Programmdarstellung haben wir Kontrollstrukturen wie while aber zu-gunsten von Kontrolllussgraphen aufgegeben. Darüberhinaus können Optimerungenim Übersetzer den Kontrolllussgraphen so verändern, dass er micht mehr eindeutig

Page 109: œbersetzerbau: Band 3: Analyse und Transformation

1.14 Anwendung: Schleifeninvarianter Code 99

0

1

i ← 0

NonZero(i < n)

Zero(i < n)T ← b + 3

i ← i + 1

Zero(i < n) NonZero(i < n)

M[A + i] ← T

2

3

4

56

0

1

i ← 0

Zero(i < n) NonZero(i < n)

M[A + i] ← T

i ← i + 1

6 5

4

3

2

Zero(i < n)

NonZero(i < n)

T ← b + 3

Abb. 1.49. Die umgekehrte Schleife des Programms aus Beispiel 1.14.1 mit Ergebnis derTransformation.

A B0 ∅ ∅1 ∅ ∅2 ∅ {T ← b + 3}3 {T ← b + 3} ∅4 {T ← b + 3} ∅5 {T ← b + 3} ∅6 ∅ ∅

Abb. 1.50. Die Analyseinformation für die umgekehrte Schleife aus Abb. 1.49.

in Kontrollstrukturen abgebildet werden kann. Gerade durch Verschieben von Codeoder die Eliminierung toten Codes können leicht Kanten entstehen, die keine Be-rechnungen mehr durchführen und entfernt werden können. Daher müssen wir dieSchleifen durch Grapheigenschaften beschreiben. Wir betrachten hier nur den Fall,in dem die Schleife einen eindeutigen Kopf aufweist. Hierzu verwenden wir die Prä-dominator-Relation auf Programmpunkten.

Wir sagen, ein Programmpunkt u prädominiert einen Programmpunkt v, falls je-der Pfad π vom Startpunkt des Programms, der v erreicht, über u führt. Wir schrei-ben: u ⇒ v . Die Relation ⇒ ist reflexiv, transitiv und anti-symmetrisch und definiertdamit eine Halbordnung auf den Programmpunkten. Diese Relation erlaubt es uns,Rücksprungkanten im Kontrollflussgraphen zu entdecken. Eine Kante k = (u, _, v)

Page 110: œbersetzerbau: Band 3: Analyse und Transformation

100 1 Grundlagen und intraprozedurale Optimierung

nennen wir dabei eine Rücksprungkante, wenn der Zielknoten v den Startknoten uder Kante prädominiert.

Beispiel 1.14.3 Betrachten Sie das Beispiel der while-Schleife links in Abb. 1.48.Jeder Programmpunkt des Rumpfs der Schleife wird vom Programmpunkt 1 vor derEintrittsbedingung prädominiert. Deshalb ist die Kante (6, ; , 1) eine Rücksprung-kante. ��Zur Berechnung der Mengen der Prädominatoren für die einzelnen Programmpunkteentwerfen wir eine einfache Analyse. Sie sammelt entlang jedes Pfades die jeweilsdurchlaufenen Programmpunkte auf. Die Menge der Prädominatoren für einen Pro-grammpunkt v ergibt sich dann als der Durchschnitt all dieser Mengen. Als vollstän-digen Verband wählen wir deshalb:

P = 2Nodes, mit der Ordnungsrelation ⊇Als abstrakten Kanteneffekt definieren wir:

[[(u, lab, v)]]� P = P ∪ {v}für alle Kanten (u, lab, v). Beachten Sie, dass hier die Beschriftungen der Kantenkeine Rolle spielen; anstatt dessen werden jeweils die Endpunkte der Kanten aufge-sammelt. Mit den so definierten Kanteneffekten definieren wir die Menge P∗[v] derPrädominatoren des Programmpunkts v als:

P [v] =⋂{[[π ]]� {start} | π : start →∗ v}

Da sämtliche Kanteneffekte distributiv sind, lassen sich diese Mengen als kleinsteLösung des zugehörigen Ungleichungssystems ermitteln.

Beispiel 1.14.4 Betrachten wir den Kontrollflussgraphen, etwa zu dem Beispiel-Programm aus Bsp. 1.14.1. Die zugehörigen Prädominator-Mengen zeigt Abb. 1.51.Die zugehörige Halbordnung ⇒ zeigt Abbildung 1.52. Wie bei unseren Darstellun-gen von Halbordnungen üblich, zeichnen wir dabei nur die direkten Beziehungen undlassen die transitiven Beziehungen implizit. Offenbar ist das Ergebnis ein Baum! Wieder nächste Satz zeigt, ist das kein Zufall. ��Satz 1.14.1 Jeder Programmpunkt v hat maximal einen unmittelbaren Prädomina-tor.

Beweis. Nehmen wir an, ein Programmpunkt v habe zwei verschiedene unmit-telbare Prädominatoren u1, u2. Dann kann weder u1 ⇒ u2 gelten noch u2 ⇒ u1.Folglich kann nicht jeder Weg vom Startpunkt nach u1 noch jeder Weg von u1 nachv den Programmpunkt u2 enthalten. Damit gibt es einen Weg vom Startpunkt nachv, der nicht u2 enthält. Dann kann u2 aber nicht v prädominieren — entgegen unse-rer Annahme. Folglich besitzt v maximal einen unmittelbaren Prädominator, und derSatz ist bewiesen. ��

Page 111: œbersetzerbau: Band 3: Analyse und Transformation

1.14 Anwendung: Schleifeninvarianter Code 101

3

2

4

5

0

1

P0 {0}1 {0, 1}2 {0, 1, 2}3 {0, 1, 2, 3}4 {0, 1, 2, 3, 4}5 {0, 1, 5}

Abb. 1.51. Die Prädominator-Mengen für einen einfachen Kontrollflussgraphen.

3

2

4

0

1

5

P0 {0}1 {0, 1}2 {0, 1, 2}3 {0, 1, 2, 3}4 {0, 1, 2, 3, 4}5 {0, 1, 5}

Abb. 1.52. Die Prädominator-Relation für den Kontrollflussgraphen aus Abb. 1.51. Die Rich-tung geht von oben nach unten.

Die Eintrittsbedingung einer while-Schleife charakterisieren wir als einen Programm-punkt v mit zwei ausgehenden Bedingungskanten, wobei v alle Knoten des Rumpfs,also insbesondere den Anfangspunkt u der Rücksprungkante nach v dominiert. Indiesem Fall wollen wir die Bedingung an den Programmpunkt u kopieren. Dies führtzu der nächsten Transformation.

Transformation LR:

vv

Zero(e)

u v ∈ P [u] u

lab

Zero(e) NonZero(e)

Zero(e)NonZero(e)

lab

NonZero(e)

Page 112: œbersetzerbau: Band 3: Analyse und Transformation

102 1 Grundlagen und intraprozedurale Optimierung

Unsere Transformation zur Schleifenumkehr funktioniert bei allen while-Schleifen.Es gibt jedoch ungewöhnliche Schleifen, die so nicht umgekehrt werden können.Eine solche zeigt Abb. 1.53. Leider gibt es auch sehr gewöhnliche Schleifen, die

3

2

0

4

1Prädominatoren:

3

2

0

1

4

Abb. 1.53. Eine ungewöhnliche Schleife.

unsere Transformation LR nicht umkehrt. Eine solche zeigt Abb. 1.54. Hier müsste

3

2

4

5

0

1

5

3

2

4

1

0

Abb. 1.54. Eine gewöhnliche Schleife, die nicht leicht zu umzukehren ist.

man zusammen mit den Bedingungskanten den ganzen Pfad zwischen Rücksprungund Bedingung duplizieren. Diese Art von Schleifen kann z.B. entstehen, wenn einekomplexe Bedingung in mehreren Schritten ausgewertet wird.

1.15 Beseitigung teilweise toter Zuweisungen

Die Beseitigung teilweiser Redundanz kann als Erweiterung unserer Optimierungzur Vermeidung von Mehrfachberechnungen aufgefasst werden. Wir fragen uns des-halb, ob nicht auch die Beseitigung von Zuweisungen an tote Variablen verschärftwerden kann zu einer Beseitigung von Zuweisungen an teilweise tote Variablen.

Page 113: œbersetzerbau: Band 3: Analyse und Transformation

1.15 Beseitigung teilweise toter Zuweisungen 103

Beispiel 1.15.1 Betrachten Sie das Programm aus Abb. 1.55. Die Zuweisung T ←

0

1

2

3

4

T ← x + 1

M[x] ← T

Abb. 1.55. Eine Zuweisung an eine teilweise tote Variable.

x + 1 muss nur auf einem der beiden Pfade berechnet werden, da die Variable xentlang des anderen Pfads tot ist. Eine solche Zuweisung nennen wir teilweise tot.

Ziel der geplanten Transformation ist es, die Zuweisung T ← x + 1 so lan-ge wie möglich zu verzögern, d.h. entlang den Kontrollflusskanten nach hinten zuverschieben, bis die Zuweisung entweder gänzlich überflüssig oder sicher benötigtwird. Gänzlich überflüssig ist sie an einem Programmpunkt, wenn die Variable aufder linken Seite an dem Programmpunkt tot ist. Das gewünschte Ergebnis dieserTransformation zeigt Abb. 1.56. ��

0

1

4

2

3

;

T ← x + 1

M[x] ← T

Abb. 1.56. Die angestrebte Optimierung für das Programm aus Abb. 1.55.

Die Verzögerung einer Zuweisung x ← e darf die Semantik des Programms nichtändern. Deshalb müssen wir darauf achten, dass eine übersprungene Kante keineder Variablen der Zuweisung modifiziert, und zwar weder x noch eine Variable inVars(e), und dass die Beschriftung der Kante selbst nicht von der Variablen x ab-hängt (Korrektheit). Weiterhin dürfen wir die Zuweisung nur dann an einen Zusam-

Page 114: œbersetzerbau: Band 3: Analyse und Transformation

104 1 Grundlagen und intraprozedurale Optimierung

menlaufpunkt des Kontrollflusses schieben, wenn wir sie aus jeder Richtung zu die-sem Punkt schieben dürfen (Effizienz). Um diese Intuition zu formalisieren, definie-ren wir eine Analyse der verzögerbaren Zuweisungen (delayable assignments). Wiebei der Berechnung der sehr beschäftigten Zuweisungen betrachten wir den Verband2Ass aller Teilmengen von Zuweisungen x ← e, wobei x nicht in e vorkommt. Derabstrakte Effekt einer Kante entfernt diejenigen Zuweisungen, die nicht weiter ver-zögert werden dürfen, und fügt die Zuweisung hinzu, die an dieser Kante berechnetwird und damit neu zur Verzögerung zur Verfügung steht. wir definieren:

[[x ← e]]� D =

{D\(Ass(e) ∪Occ(x)) ∪ {x ← e} falls x �∈ Vars(e)D\(Ass(e) ∪Occ(x)) falls x ∈ Vars(e)

wobei wieder Ass(e) die Menge aller Zuweisungen an Variablen ist, die in e vorkom-men und Occ(x) die Menge aller Zuweisungen bezeichnet, in denen x vorkommt.Mit diesen Konventionen definieren wir für die übrigen Kantenbeschriftungen desKontrollflussgraphen:

[[x ← M[e]]]� D = D\(Ass(e) ∪Occ(x))[[M[e1] ← e2]]� D = D\(Ass(e1) ∪ Ass(e2))

[[Zero(e)]]� D = [[NonZero(e)]]� D = D\Ass(e)

Am Startpunkt des Programms liegen noch keine Zuweisungen vor, die verzögertwerden könnten. Dort nehmen wir darum als Startwert die Menge D0 = ∅ an. Da aneinem Programmpunkt eine Zuweisung nur verzögert werden kann, wenn sie entlangjeder eingehenden Kante verzögert wurde, wählen wir als Halbordnung die Ober-mengenrelation ⊇.

Für die folgenden Transformationsregeln nehmen wir an, D[ . ] und L[ . ] sei-en die kleinsten Lösungen der Ungleichungssysteme für die Verzögerbarkeit vonZuweisungen und die (gegebenenfalls echte) Lebendigkeit von Variablen relativ zueiner Menge X von Variablen, die am Programmende lebendig sind. Da wir zurFormulierung der Voraussetzungen die Kanteneffekte für beide Analysen benötigen,identifizieren wir sie durch Angabe der entsprechenden Indizes D bzw. L.

Transformation PDE für die leere Anweisung:

v

u

v

ss

u

;

An diese Kante werden alle Zuweisungen verschoben, die nicht über den Endpunktv hinaus verschoben werden können, deren linke Seite aber an v lebendig ist, d.h. ssbesteht aus allen x ← e′ ∈ D[u]\D[v] mit x ∈ L[v].

Transformation PDE für Zuweisungen:Falls y ∈ Vars(e), dann ist die Zuweisung y ← e nicht verschiebbar, und wir trans-formieren:

Page 115: œbersetzerbau: Band 3: Analyse und Transformation

1.15 Beseitigung teilweise toter Zuweisungen 105

v

u

v

ss2

ss1

y ← e

u

y ← e

Die Folge ss1 sammelt diejenigen nützlichen Zuweisungen, die nicht über die Zuwei-sung y ← e hinweg verzögert werden können. Die Folge ss2 dagegen sammelt dienützlichen Zuweisungen, die zwar entlang der Kante verzögerbar sind, jedoch nichtüber ihren Endpunkt hinaus. Das heißt, dass ss1 eine Anordnung der Zuweisungenx ← e′ ∈ D[u]∩ (Ass(e)∪Occ(y)) ist mit x in L[v]\{y} ∪Vars(e). Weiterhin istss2 eine Anordnung der Zuweisungen x ← e′ ∈ D[u]\(Ass(e) ∪ Occ(y) ∪ D[v])mit x ∈ L[v].Falls y �∈ Vars(e), ist die Zuweisung y ← e verschiebbar, und wir haben:

v

v

u

ss1

ss2

u

y ← e

Die Folge ss1 ist hier genauso definiert wie bei nichtverschiebbaren Zuweisungenoben. Auch die Folge ss2 ist analog definiert: sie sammelt die nützlichen Zuweisun-gen, die zwar entlang der Kante verzögerbar sind, jedoch nicht über ihren Endpunkthinaus. Gegebenenfalls kann nun ss2 auch die Zuweisung y ← e selbst enthalten.

Das heißt, ss1 ist eine Anordnung der Zuweisungen x ← e′ ∈ D[u]∩ (Ass(e)∪Occ(y)) mit x in L[v]\{y} ∪Vars(e). Weiterhin ist ss2 eine Anordnung der Zuwei-sungen x ← e′ ∈ (D[u]\(Ass(e) ∪Occ(y)) ∪ {y ← e})\D[v] mit x ∈ L[v].

Transformation PDE für Verzweigungen:

u

v2v1

uNonZero(b)Zero(b) NonZero(b)Zero(b)

v1 v2

ss0

ss1 ss2

Dabei beinhaltet die Folge ss0 alle nützlichen Zuweisungen, die zwar an u verzöger-bar sind, nicht aber über die Bedingungskanten hinweg verschoben werden können.Die Folgen ssi , i = 1, 2, wiederum bestehen aus allen nützlichen Zuweisungen, die

Page 116: œbersetzerbau: Band 3: Analyse und Transformation

106 1 Grundlagen und intraprozedurale Optimierung

zwar über die Bedingung hinaus verzögerbar sind, aber nicht über den Nachfolge-programmpunkt vi hinaus.

Das heißt, dass die Folge ss0 alle Zuweisungen x ← e ∈ D[u] enthält mitx ∈ Vars(b), und für i = 1, 2, die Folge ssi aus allen Zuweisungen besteht mitx ← e ∈ D[u]\(Ass(b) ∪D[vi]) und x ∈ L[vi].

Transformation PDE für Ladeoperationen:

v

v

u

u

y ← M[e]

ss2

ss1

y ← M[e]

Wir verzichten hier darauf, Ladeoperationen zu verschieben. Wir behandeln siedeshalb analog zu nichtverschiebbaren Zuweisungen. Das heißt, dass die Folgess1 aus den Zuweisungen x ← e′ ∈ D[u] ∩ (Ass(e) ∪ Occ(y)) besteht mitx ∈ L[v]\{y} ∪ Vars(e). Weiterhin besteht die Folge ss2 aus den Zuweisungenx ← e′ ∈ D[u]\(Occ(y) ∪ Ass(e) ∪D[v]) mit x ∈ L[v].

Transformation PDE für Schreiboperationen:

Die nächste Regel behandelt Kanten, an denen in den Speicher geschrieben wird.Auch diese Operationen verschieben wir nicht.

v

v

u

u

ss2

ss1

M[e1] ← e2 M[e1] ← e2

Wieder benötigen wir die Folgen ss1 und ss2 der Zuweissungen, die vor und nachder ursprünglichen Anweisung an der Kante eingefügt werden müssen. Die Folge ss1besteht aus den Zuweisungen x ← e′ ∈ D[u] ∩ (Ass(e1) ∪ Ass(e2)), und die Folgess2 ist eine Anordnung der Zuweisungen x ← e′ ∈ D[u]\(Ass(e1) ∪ Ass(e2) ∪D[v]) mit x ∈ L[v].

Nach unserer Annahme ist X die Menge aller Variablen, die am Programmende le-bendig sind. Die letzte Regel behandelt das Programmende in dem Fall, in dem Xnicht die leere Menge ist. Bevor die Programmausführung des transformierten Pro-gramms beendet wird, müssen nämlich noch alle Zuweisungen an Variablen aus der

Page 117: œbersetzerbau: Band 3: Analyse und Transformation

1.15 Beseitigung teilweise toter Zuweisungen 107

Menge X ausgeführt werden, die am Endpunkt des Programms verzögerbar sind.Dazu dient die folgende Regel:

Transformation PDE für das Programmende:

Sei u der Endpunkt des ursprünglichen Programms. Dann führen wir einen neuenEndpunkt ein:

u u

ss

Dabei ist ss die Menge aller Zuweisungen x ← e in D[u] mit x ∈ X.

Beispiel 1.15.2 Betrachten wir unser einführendes Beispiel und nehmen wir an, dassam Programmende keine Variable lebendig ist. In diesem Fall brauchen wir keinenneuen Endknoten für das Programm einführen. Die Analysen der lebendigen Varia-blen bzw. verzögerbaren Zuweisungen ergeben:

L D0 {x} ∅1 {x, T} {T ← x + 1}2 {x, T} {T ← x + 1}3 ∅ ∅4 ∅ ∅

Die Anwendung der Transformation PDE liefert für den Kontrollflussgraphen ausAbb. 1.55 den Kontrollflussgraphen aus Abb. 1.56. ��Bei unseren Überlegungen zur Korrektheit der transformation PDE müssen wir be-achten, dass in der Regel für Zuweisungen möglicherweise Zuweisungen aus demKontrollflussgraphen entfernt werden. Die entfernten Zuweisungen gehen jedoch nurdann verloren, wenn ihre rechte Seite am nächsten Programmpunkt tot ist. Andern-falls werden sie in der jeweiligen Analyseinformation vermerkt und entlang der Kon-trollflusskanten weiter propagiert. Kann eine Zuweisung entlang einer Kante nichtweiter propagiert werden, fügen wir sie wieder in den Kontrollflussgraphen ein –sofern ihre linke Seite an der Stelle lebendig ist.

Eine Anwendung der Transformation PDE beseitigt möglicherweise einzelneZuweisungen. Damit kann sie weitere Chancen zur Beseitigung teilweise toten Co-des eröffnen. Wie bei der Transformation PRE kann es sich deshalb lohnen, dieTransformation wiederholt anzuwenden. Wir fragen uns, ob unsere Transformationdas Programm nicht gegebenenfalls verschlechtert – etwa, indem eine Zuweisung ineine Schleife hinein geschoben wird.

Beispiel 1.15.3 Betrachten Sie die Schleife aus Abb. 1.57. Die Zuweisung T ← x +1 ist jedoch an keinem Programmpunkt verzögerbar. Etwas anderes ist es, wenn die

Page 118: œbersetzerbau: Band 3: Analyse und Transformation

108 1 Grundlagen und intraprozedurale Optimierung

0

1

2

3

4

T ← x + 1

y ← M[T]

Abb. 1.57. Eine Beispielschleife.

2

34

0

1

0

1

2

34

y ← M[T]

T ← x + 1

T ← x + 1

y ← M[T]

Abb. 1.58. Eine umgekehrte Schleife und ihre Optimierung.

Schleife umgekehrt ist wie in Abb. 1.58. Dann lässt sich die Zuweisung zumindesthinter die Eintrittskante schieben, und der teilweise tote Code ist beseitigt. ��Im Beispiel war die Transformation PDE nicht verschlechternd. Es lässt sich sogarbeweisen, dass die Optimierung PDE nie verschlechternd ist.

Fazit 1.15.1 Wir haben eine Reihe optimierender Transformationen kennengelernt.Wir stellten fest, dass durch eine Transformation wie z.B. den ersten Teilschritt derTransformation RE zur Beseitigung von Redundanzen Ineffizienzen entstehen kön-nen, die durch weitere Transformationen, hier CE (Beseitigung von Kopien), gefolgtvon DE (Beseitigung von Zuweisungen an tote Variablen) erst wieder beseitigt wer-den müssen. Es ist deshalb eine keineswegs triviale Frage, in welcher Reihenfolgeunsere Optimierungen angewendet werden sollen. Hier ist eine sinnvolle Abfolge dervon uns vorgestellten Transformationen:

Page 119: œbersetzerbau: Band 3: Analyse und Transformation

1.16 Aufgaben 109

LR Schleifenumkehr

CF Konstantenfaltung

Intervallanalyse

Alias-Analyse

RE Beseitigung redundanter Berechnungen

CE Propagation von Kopien

DE Beseitigung toter Zuweisungen

PRE Beseitigung teilweise redundanter Zuweisungen

PDE Beseitigung teilweise toter Zuweisungen

1.16 Aufgaben

1. Verfügbare Zuweisungen. Betrachten Sie den Kontrollflussgraphen der Funk-tion swap aus der Einleitung.

a) Bestimmen Sie für jeden Programmpunkt u die Menge A[u] der an u ver-fügbaren Zuweisungen.

b) Wenden Sie die Transformation RE zur Beseitigung redundanter Berech-nungen an.

2. Vollständige Verbände. Betrachten Sie den vollständigen Verband M der mo-notonen booleschen Funktionen mit zwei Variablen:

0

x ∧ y

x y

x ∨ y

1

a) Bestimmen Sie die Menge aller monotonen Funktionen, die M in den voll-ständigen Verband 2 = {0, 1} mit 0 < 1 abbilden.

b) Bestimmen Sie die Anordnung dieser Funktionen!

3. Vollständige Verbände. Zeigen Sie:a) Sind D1, D2 vollständige Verbände, dann auch

D1 ×D2 = {(x, y) | x ∈ D1, y ∈ D2}wobei (x1, y1) � (x2, y2) genau dann wenn x1 � x2 und y1 � y2.

Page 120: œbersetzerbau: Band 3: Analyse und Transformation

110 1 Grundlagen und intraprozedurale Optimierung

b) Eine Funktion f : D1 × D2 → D ist genau dann monoton, wenn die Funk-tionen:

fx :D2→D fx(y)= f (x, y) (x ∈ D1)f y:D1→D f y(x)= f (x, y) (y ∈ D2)

monoton sind.4. Vollständige Verbände. Für einen vollständigen Verband D sei h(D) = n die

maximale Länge einer echt aufsteigenden Kette ⊥ � d1 � · · · � dn. ZeigenSie, dass für vollständige Verbände D1, D2 gilt:a) h(D1 ×D2) = h(D1) + h(D2)b) h(Dk) = k · h(D)c) h([D1 → D2]) = #D1 · h(D2), wobei [D1 → D2] die Menge der monoto-

nen Funktionen f : D1 → D2 und #D1 die Kardinalität von D1 ist.5. Einführung von Hilfsvariablen für Ausdrücke. Führen Sie für ausgewählte

Ausdrücke e Hilfsvariablen Te ein, in der der Wert von e nach jeder Auswertungabgelegt wird.a) Definieren Sie eine Programmtransformation, die diese Hilfsvariablen ein-

führt und argumentieren Sie, dass diese Transformation die Semantik IhresProgramms nicht ändert.

b) Welche Auswirkung hat diese Transformation auf die Beseitigung redun-danter Berechnungen RE?

c) Für welche Ausdrücke lohnt sich das Einführen der Hilfsvariablen? Wiekann die Anzahl der Umspeicherungen durch Einführung der Hilfsvariablennachträglich wieder verringert werden?

d) Wie könnte die Transformation PRE durch die Hilfsvariablen profitieren?6. Verfügbare Speicherzugriffe. Erweitern Sie Analyse und Transformation für

verfügbare Zuweisungen so, dass die Verfügbarkeit von Speicherzugriffen x ←M[e] berücksichtigt wird.

7. Vollständige Verbände. Sei U eine endliche Menge und D = 2U derTeilmengen-Verband über U , geordnet durch �=⊆. Sei F die Menge allerFunktionen f : D → D von der Form f x = (x ∩ a) ∪ b mit a, b ⊆ D.Zeigen Sie:

a) F enthält die Identität und besitzt ein kleinstes und größtes Element;b) F ist abgeschlossen unter Komposition, � und �;c) auf F lässt sich eine (Postfix-) Operation * definieren mit

f ∗x =⊔j≥0

f jx

8. Fixpunktiteration. Betrachten Sie ein Ungleichungssystem der Form:

xi � fi(xi+1), wobei fi monoton, für i = 1, . . . , n

Zeigen Sie:a) Die Fixpunktiteration terminiert nach maximal n Iterationen.b) Bei geschickter Anordnung der Variablen genügt eine Round-Robin-Iteration.

Page 121: œbersetzerbau: Band 3: Analyse und Transformation

1.16 Aufgaben 111

c) Kann die obere Schranke n für die maximale Anzahl an Iterationen erreichtwerden?

9. Tote Variablen. Definieren Sie eine Programm-Analyse, die direkt für jedenProgrammpunkt die Menge der toten Variablen bestimmt.a) Definieren Sie den zugehörigen Verband!b) Definieren Sie die zugehörigen abstrakten Kanteneffekte!c) Erweitern Sie die Analyse zu einer Analyse wahrer Totheit!

Wie könnte man die Korrektheit der Analyse beweisen?10. Distributivität I. Seien f1, f2 : D → D zwei distributive Funktionen über

einem vollständigen Verband D. Zeigen Sie:a) f1 ◦ f2 ist ebenfalls distributiv;b) f1 � f2 ist ebenfalls distributiv.

11. Distributivität II. Beweisen Sie Satz 1.7.1.12. Distributivität III. Zeigen Sie, dass die Funktion:

f (X) = (a ∈ X)?A : B

(A, B ⊆ U) vollständig distributiv ist, sofern B ⊆ A.13. Optimierung der Funktion swap. Wenden Sie nach der Optimierung RE auch

die Optimierungen CE und DE auf das Beispielprogramm swap an!14. Konstantenpropagation: Vorzeichen. Vereinfachen Sie die Konstantenpropa-

gation so, dass sie nur die Vorzeichen von Werten berücksichtigt.

a) Definieren Sie für diese Erweiterung eine geeignete partielle Ordnung vonWerten!

b) Wie sieht die Beschreibungsrelation Δ aus?c) Definieren sie sinnvolle abstrakte Operatoren auf den Werten!d) Weisen Sie nach, dass Ihre Operatoren die Beschreibungsrelation Δ respek-

tieren!e) Geben Sie die abstrakten Kanteneffekte für Bedingungen an. Argumentieren

Sie, dass diese korrekt sind!

15. Konstantenpropagation: ausgeschlossene Werte. Erweitern Sie die Konstan-tenpropagation so, dass nicht nur mögliche Werte für Variablen, sondern auchdefinitiv ausgeschlossene berücksichtigt werden.Betrachten Sie z.B. eine Abfrage:

if (x = 3) y ← x;else z ← x;

Im else-Teil hat die Variable x offenbar definitiv nicht den Wert 3.

a) Definieren Sie für diese Erweiterung eine geeignete partielle Ordnung aufden Werten!

b) Wie sieht die Beschreibungsrelation Δ aus?c) Definieren Sie sinnvolle abstrakte Operatoren auf den Werten!d) Weisen Sie nach, dass Ihre Operatoren die Beschreibungsrelation Δ respek-

tieren!

Page 122: œbersetzerbau: Band 3: Analyse und Transformation

112 1 Grundlagen und intraprozedurale Optimierung

e) Geben Sie verbesserte abstrakte Effekte für bestimmte Bedingungen an undargumentieren Sie, dass diese korrekt sind!

16. Konstantenpropagation: Speicherzellen. Erweitern Sie Konstantenpropagati-on so, dass auch die Inhalte einiger Speicherzellen mit verwaltet werden.

a) Wie sieht der neue abstrakte Zustand aus?b) Wie sehen die neuen abstrakten Kanteneffekte für Kanten mit Lade- bzw.

Speicheroperationen aus?c) Argumentieren Sie, dass Ihre neuen Kanteneffekte korrekt sind!

17. Intervallanalyse. Definieren Sie eine partielle Ordung von Intervallen, diees erlaubt, sowohl die untere wie die obere Schranke maximal jeweils r-mal zumodifizieren!Definieren Sie für diesen Bereich die Beschreibungsrelation Δ sowie ein neuesWidening!

18. Intervallanalyse. Beweisen Sie, dass die abstrakte Multiplikation für Inter-valle die Beschreibungsrelation Δ respektiert, d.h. dass aus z1 Δ I1 und z2 Δ I2immer folgt, dass auch

(z1 · z2) Δ (I1 ·� I2)

19. Intervallanalyse. Definieren Sie die abstrakten Operationen ! (Negation) und�= (Ungleichheit).

20. Intervallanalyse. Geben Sie ein Beispielprogramm an, für das die Intervall-analyse ohne Widening nicht terminiert!

21. Aliasanalyse. Gegeben sei das folgende Programm:

for (i ← 0; i < 3; i++) {R ← new();R[1] ← i;R[2] ← l;l ← x;

}Wenden Sie die Points-to- und die Aliasanalyse aus Kapitel 1.11 auf das Pro-gramm an.

22. Aliasanalyse: Semantik. Weisen Sie nach, dass die instrumentierte opera-tionelle Semantik, die wir in Kapitel 1.11 zum Nachweis der Korrektheit derPoints-to-Analysen eingeführt haben, zur „natürlichen“ operationellen Semantikfür Programme mit dynamischer Allokation von Speicherblöcken äquivalent ist.Formalisieren Sie dazu einen Äquivalenzbegriff, der die unterschiedliche Men-gen von Adressen, die die beiden Semantiken einsetzen, zwar nicht global, aberfür jede konkrete Programmausführung in Beziehung setzen.

23. Aliasanalyse: Anzahl der Iterationen. Zeigen Sie, dass bei der gleichungsba-sierten Aliasanalyse aus Kapitel 1.11 der kleinste Fixpunkt des Gleichungssy-stems über Partitionen bereits nach genau einer Iteration über alle Kanten er-reicht ist.

Page 123: œbersetzerbau: Band 3: Analyse und Transformation

1.17 Literaturhinweise 113

24. Worklistiteration. Führen Sie die Worklistiteration für die Berechnung derverfügbaren Zuweisungen am Beispiel des Fakultätsprogramms durch. ErmittelnSie die Anzahl der Auswertungen rechter Seiten.

25. Schleifeninvarianter Code. Beseitigen Sie in dem folgenden Programm denschleifeninvarianten Code:

for (i ← 0; i < n; i++) {b ← a + 2;T ← b + i;M[T] ← i;if ( j > i) break;

}Ließe sich die invariante Berechnung auch verschieben, wenn die Abfrageif ( j > i) . . . am Anfang des Schleifenrumpfs stünde? Begründen Sie Ihre An-wort!

26. Schleifendominierte Programme. Ein Programm heiße schleifen-dominiert,falls jede Schleife genau einen Eintrittspunkt besitzt, d.h. einen Punkt u enthält,der alle Knoten der Schleife dominiert.

a) Zeigen Sie, dass in einem schleifen-dominierten Programm die Menge derEintrittspunkte in Schleifen ein Schleifenseparator für das Programm ist.

b) Transformieren Sie die Schleife des Beispielprogramms zur Intervallanalysein eine do-while-Schleife.

c) Führen Sie die Intervallanalyse (ohne Narrowing) auf dem transformiertenProgramm durch. Vergleichen Sie das Ergebnis mit demjenigen aus Kapitel1.10.

1.17 Literaturhinweise

Die Grundlagen der abstrakten Interpretation wurden von Patrick und Radhia Cou-sot gelegt in [CC77a]. Intervallanalyse wird erstmals beschrieben in [CC76]. Dortwird auch die Technik von Widening und Narrowing eingeführt. Eine Technik zurIntervallanalyse ganz ohne Widening und Narrowing präsentiert dagegen [GS07].Monotone Analyserahmen führen Kam und Ullman in [KU77, KU76] ein. Pro-grammanalysen mit distributiven Kanten-Effekten betrachtet Gary Kildall [Kil73].Die beschriebene Verstärkung der Lebendigkeitsanalyse wurde von Robert Giege-rich, Ulrich Möncke und Reinhard Wilhelm entwickelt [GMW81]. Die geschilder-ten Verfahren zur Beseitigung partieller Redundanzen und partiell toten Codes leh-nen sich an die Arbeiten von Jens Knoop, Oliver Rüthing and Bernhard Steffen an[KRS94b, KRS94a, Kno98]. Eine Verallgemeinerung von Konstantenpropagationauf lineare Gleichheitsbeziehungen untersuchen bereits Michael Karr [Kar76] undPhilippe Granger [Gra91], deren Ansätze in [MOS04, MOS05, MOS07] interproze-dural verallgemeinert werden. Die Analyse von Ungleichungsbeziehungen zwischen

Page 124: œbersetzerbau: Band 3: Analyse und Transformation

114 1 Grundlagen und intraprozedurale Optimierung

Variablen geht auf die Arbeit von Patrick Cousot und Nicolas Halbwachs [CH78]zurück. Praktische Anwendungen zur Analyse von C-Programmen diskutiert aus-führlich Axel Simon [Sim08].

In unserer knappen Übersicht wurden nur sehr einfache Ansätze zur Analyse dy-namischer Datenstrukturen diskutiert. Gerade für die Programmiersprache C stelltAliasanalyse, insbesondere in Programmen mit Zeigerarithmetik eine Herausforde-rung dar. Die einfachen Verfahren, die wir präsentiert haben, orientieren sich an denVerfahren von Bjarne Steensgaard [Ste96] bzw. Paul Anderson, David Binkley, Ge-nevieve Rosay und Tim Teitelbaum [ABRT02]. Interprozedurale Erweiterungen prä-sentieren Manuel Fähndrich, Jakob Rehof und Manuvir Das [FRD00] sowie DonglinLiang, Maikel Pennings und Mary Jean Harrold [LPH01]. Ramalingam [Ram02] be-handelt die Analyse der Schleifenstruktur in Kontrollflussgraphen im Detail und gibtsowohl axiomatische als auch konstruktive Definitionen derselben.

Elaboriertere Techniken wurden von Mooly Sagiv, Thomas W. Reps und Rein-hard Wilhelm entwickelt [SRW99, SRW02]. Diese Analysen sind allerdings ver-hältnismäßig aufwändig. Dafür erlauben sie aber sogar, Aussagen über die Gestaltdynamischer Datenstrukturen automatisch herzuleiten.

Page 125: œbersetzerbau: Band 3: Analyse und Transformation

2

Interprozedurale Optimierungen

In diesem Abschnitt erweitern wir unsere Programmiersprache um Prozeduren, ih-re Deklarationen und ihre Aufrufe. Prozeduraufrufe haben eine komplexe Semantik:die aktuelle Berechnung wird unterbrochen, bis die aufgerufene Prozedur ihre Be-rechnung beendet hat. Der Rumpf einer Prozedur bildet meist einen eigenen Gül-tigkeitsbereich für lokale, d.h. nur innerhalb des Rumpfs gültige Variablen. GlobaleVariablen werden möglicherweise von lokalen Variablen gleichen Namens verdeckt.Aktuelle Parameter werden bei Aufruf und Ergebnisse bei Verlassen der Prozedurübergeben. Wenn es Referenzparameter gibt, gibt es nach der Übergabe einer Varia-blen als aktuellem Referenzparameter zwei verschiedene Namen für diese Variable.

Unsere Programmiersprache hat globale und lokale Variablen. Die Namen derlokalen Variablen lassen wir zur Unterscheidung mit Großbuchstaben beginnen. InProgrammiersprachen mit Funktionszeigern wie in C kann man bei einem Funkti-onsaufruf über einen Funktionszeiger zur Übersetzungszeit unter Umständen nichtentscheiden, welche Funktion tatsächlich aufgerufen wird. Das Gleiche gilt fürobjekt-orientierte Sprachen wie JAVA oder C#, wo bei einem Methodenaufruf dieder dynamische Typ des Objekts darüber entscheidet, welche Methode ausgewähltwerden soll. Der Einfachkeit halber nehmen wir hier jedoch an, dass an jeder Auf-rufstelle die aufgerufene Prozedur statisch bekannt ist. Call-by-Value-Parameter undRückgabewerte lassen sich leicht durch globale und lokale Variablen simulieren (s.Aufg. 1). Deshalb betrachten wir nur Prozeduren ohne Parameter. Da unsere Proze-duren also weder Parameter besitzen noch Ergebnisse zurückliefern, reicht es, unsereProgrammiersprache um Aufrufe f() als einziger neuen Form von Anweisungen er-weitert.

Jede Prozedur f besitzt eine eigene Deklaration:

f() { stmt∗ }Die Programmausführung startet mit dem Aufruf einer speziellen Prozedur main ().

Beispiel 2.0.1 Zur Einführung betrachten wir die Fakultätsfunktion f und eine Pro-zedur main, welche f aufruft, nachdem die globale Variable b auf 3 gesetzt wurde.

H. Seidl et al., Übersetzerbau, eXamen.press, DOI 10.1007/978-3-642-03331-5_2,c© Springer-Verlag Berlin Heidelberg 2010

Page 126: œbersetzerbau: Band 3: Analyse und Transformation

116 2 Interprozedurale Optimierungen

main() {b ← 3;f();M[17] ← ret;

}

f() {A ← b;if (A ≤ 1) ret ← 1;else {

b ← A − 1;f();ret ← A · ret;

}}

Die globalen Variablen b und ret nehmen den aktuellen Parameter bzw. den Rück-gabewert der Funktion f auf. Der formale Parameter der Funktion f wird durch dielokale Variable A simuliert, die als erste Aktion im Rumpf der Funktion f den Wertdes aktuellen Parameters b erhält. ��Programme unserer erweiterten Programmiersprache lassen sich durch eine Mengevon Kontrollflussgraphen darstellen, je einer für den Kontrollfluss einer Funktion.Für das Programm aus Beispiel 2.0.1 zeigt Abbildung 2.1 diese beiden Kontroll-flussgraphen. Um über die Korrektheit von Programmen und optimierenden Trans-

0

2

1

3

4

5

6

7

8

10

9

Zero(A ≤ 1) NonZero(A ≤ 1)

f ()

b ← A − 1

f()

ret ← A ∗ ret

M[17] ← ret

f()

b ← 3

main()

ret ← 1

A ← b

Abb. 2.1. Das Fakultätsprogramm mit Prozeduren.

formationen solcher Programme reden zu können, müssen wir die Semantik vonKontrollflussgraphen auf Kontrollflussgraphen mit Prozeduraufrufen erweitern.

In Anwesenheit von Prozeduraufrufen wird die Ausführungen von Programmennicht mehr durch Pfade beschrieben werden, sondern durch Pfade, die entsprechendden Prozeduraufrufen ineinander geschachtelt sind: jeder Prozeduraufruf entspricht

Page 127: œbersetzerbau: Band 3: Analyse und Transformation

2 Interprozedurale Optimierungen 117

der Öffnung einer Klammer, die mit Beendigung dieses Aufrufs wieder geschlossenwird. Die Folge der geöffneten und noch nicht wieder geschlossenen Klammern kön-nen wir durch einen Keller, den Aufrufkeller (Call Stack) repräsentieren. Aufrufkellerbilden damit die Grundlage der operationellen Semantik von Programmen mit Pro-zeduren. Die operationelle Semantik beschreiben wir durch eine Einschritt-Berech-nungsrelation � zwischen Konfigurationen. Eine Konfiguration ist jetzt ein Tripel,bestehend aus einem Aufrufkeller aus stack, einer Belegung der globalen Variablenaus globals und einer Belegung des Speichers aus store. Ein Aufrufkeller besteht auseiner Folge von Kellerrahmen (Stack Frames). Jeder Kellerrahmen wiederum bestehtaus einem Programmpunkt aus der Menge in der zugehörigen Prozedur, bis zu demdie Berechnung gekommen ist, und einem lokalen Zustand, d.h. einer Belegung derlokalen Variablen der Prozedur:

configuration == stack × globals × storeglobals == Glob → Z

store == N → Z

stack == frame · frame∗

frame == point × localslocals == Loc → Z

Hier bezeichnen Glob und Loc die Mengen der globalen bzw. lokalen Variablen desProgramms, während die Menge point die Menge der Programmmpunkte ist. Bei dergrafischen Darstellung von Kellern wachsen Keller von unten nach oben; der Keller-rahmen der aktuellen Prozedur ist jeweils der oberste. Folgen von Kellern werdenvon links nach rechts angeordnet; der Keller zu einem Aufruf steht links von demKellerrahmen, der sich aus dem Aufruf ergibt.

Berechnungsschritte beziehen sich auf die aktuell aufgerufene Prozedur. Zusätz-lich zu den Schritten, die wir bereits bei Programmen ohne Prozeduren kennen ge-lernt haben, benötigen wir zwei Arten von Schritten:

Aufruf k = (u, f(), v) :

(σ · (u, ρLoc) , ρGlob, μ) � (σ · (v, ρLoc) · (uf , ρf) , ρGlob, μ)

uf Anfangspunkt von f

Rückkehr von einem Aufruf :

(σ · (v, ρLoc) · (rf , _) , ρGlob, μ) � (σ · (v, ρLoc) , ρGlob, μ)

rf Endpunkt von f

Die Abbildung ρf beschreibt die Werte der lokalen Variablen bei Betreten der Pro-zedur f. Wenn diese Werte beliebig sein können, ist die Programmausführung prin-zipiell nichtdeterministisch. Um diese Komplikation zu vermeiden, nehmen wir derEinfachheit halber an, dass die lokalen Variablen stets mit 0 initialisiert werden, d.h.ρf = {x → 0 | x ∈ Loc}. Diese Variablenbelegung bezeichnen wir auch mit 0.

Page 128: œbersetzerbau: Band 3: Analyse und Transformation

118 2 Interprozedurale Optimierungen

Damit die Programmausführung deterministisch ist, nehmen wir weiterhin an, dassEndpunkte von Prozeduren keine ausgehenden Kanten besitzen. Für das Fakultäts-programm aus Beispiel 2.0.1 ergibt sich eine Folge von Aufrufkellern, von der Abb.2.2 eine Auswahl zeigt.

1 2

5 A → 3

2

7 A → 3

2

8

5

A → 3

A → 2

2

8

7

A → 3

A → 2 8

2

8

5

A → 3

A → 1

A → 2

2

8

10

8 A → 3

A → 2

A → 1

2

8

8 A → 2

A → 3

2

10

8 A → 3

A → 2

2

8 A → 3 10

2

A → 3

2

Abb. 2.2. Eine Folge von Aufrufkellern für das Programm aus Beispiel 2.0.1.

Zur Bezeichnung einer Folge von Berechnungsschritten können wir wie im intra-prozeduralen Fall die Folge der Kanten verwenden, wobei wir zusätzlich bei einemProzeduraufruf ein 〈f〉 für das Betreten der Prozedur f und ein 〈/f〉 bei Verlassendieser Prozedur einfügen. Im Beispiel ergibt sich die Folge:

〈main〉0, 1 〈f〉 5, 6, 7

〈f〉 5, 6, 7〈f〉 5, 6, 7〈f〉 5, 9, 10 〈/f〉

8, 10 〈/f〉8, 10 〈/f〉8, 10 〈/f〉

2, 3 〈/main〉

wobei wir der Übersichtlichkeit halber anstelle der Kanten (u, lab, v) nur die vonden Kanten durchlaufenen Programmpunkte aufgelistet haben.

Eine Berechnungsfolge π , die von einer Konfiguration ((u, ρLoc), ρGlob, μ) in ei-ne Konfiguration ((v, ρ′

Loc), ρ′Glob, μ′) führt, nennen wir pegelerhaltend (same-level),

weil in einer solchen Berechnung jede Prozedur, die betreten wird, auch wieder ver-lassen wird und deshalb die Höhe des Aufrufkellers am Ende der Berechnung gleichder Höhe des Aufrufkellers am Anfang der Berechnung ist.

Page 129: œbersetzerbau: Band 3: Analyse und Transformation

2 Interprozedurale Optimierungen 119

Nehmen wir an, eine pegelerhaltende Berechnungsfolge führt von einer Konfigu-ration ((u, ρLoc), ρGlob, μ) in eine Konfiguration ((v, ρ′

Loc), ρ′Glob, μ′). Dann bedeutet

das, dass die Ausführung des Programms vom Programmpunkt u aus den Programm-punkt v erreicht (eventuell mit Hilfe zwischenzeitlicher Prozeduraufrufe), wenn dieVariablen die Werte entsprechend ρ = ρLoc ⊕ ρGlob haben und der Speicher durchμ beschrieben wird. Dabei ist der Zustand der Variablen und des Speichers nachder Ausführung durch ρ′ = ρ′

Loc ⊕ ρ′Glob und μ′ gegeben. Die pegelerhaltende Be-

rechnungsfolge π definiert damit eine partielle Abbildung [[π ]], die (ρLoc, ρGlob, μ)in (ρ′

Loc, ρ′Glob, μ′) transformiert. Diese Transformation kann man induktiv über den

Aufbau einer solchen Berechnungsfolge ermitteln:

[[π k]] = [[k]] ◦ [[π ]] für eine normale Kante k[[π1 〈f〉 π2 〈/f〉]] = H([[π2]]) ◦ [[π1]] für eine Prozedur f

Der Operator H(· · · ) liefert zu der Transformation, die eine Berechungsfolge füreinen Prozedurrumpf realisiert, die Transformation, die diese Berechnungsfolge fürdie aufrufende Prozedur bewirkt:

H(g) (ρLoc, ρGlob, μ) = let (ρ′Loc, ρ

′Glob, μ′) = g (0, ρGlob, μ)

in (ρLoc, ρ′Glob, μ′)

Neben pegelerhaltenden Berechnungen betrachten wir auch Berechnungen, die einenProgrammpunkt u erreichen. Diese beginnen in einem Aufruf der Prozedur mainund führen zu einem Aufrufkeller, auf dem oben ein Kellerrahmen mit Programm-punkt u liegt. Auch solche Berechnungen lassen sich durch Folgen von Kanten be-schreiben, in die für das Betreten und Verlassen von Prozeduren die Markierungen〈f〉 bzw. 〈/f〉 eingestreut sind. Jede solche Folge π ist von der Form

π = 〈main〉 π0 〈f1〉 π1 · · · 〈fk〉 πk

ist für Prozeduren f1, . . . , fk und pegelerhaltende Berechnungsfolgen π0, π1, . . . , πk.Auch eine solche Folge bewirkt eine Transformation [[π ]] eines Paars (ρLoc, ρGlob, μ)von Variablenbelegung und Speicherzustand vor der Ausführung in ein Paar nach derAusführung von π . Dabei ist:

[[π 〈f〉 π ′]] (ρLoc, ρGlob, μ) = let (_, ρ′Glob, μ′) = [[π ]] (ρLoc, ρGlob, μ)

in [[π ′]] (0, ρ′Glob, μ′)

für eine Prozedur f und eine pegelerhaltende Berechnungsfolge π ′.Unsere operationelle Semantik lehnt sich eng an eine mögliche Implementierung

von Prozeduren an. Mit Hilfe der Semantik lässt sich der Aufwand eines Funktions-aufrufs genauer angeben.

Aufgaben vor Betreten des Rumpfs:

• Anlegen eines Kellerrahmens;

• Retten der lokalen Variablen;

Page 130: œbersetzerbau: Band 3: Analyse und Transformation

120 2 Interprozedurale Optimierungen

• Retten der Fortsetzungsaddresse;

• Anspringen des Rumpfs.

Aufgaben bei Beenden des Aufrufs:

• Aufgeben des Kellerrahmens;

• Restaurieren der lokalen Variablen;

• Rücksprung hinter die Aufrufstelle.

Verwendet man eine rein kellerbasierte Implementierung, ist das Retten und Restau-rieren der lokalen Variablen sehr einfach. Realistische Implementierungen für realeRechner werden nach Möglichkeit Gebrauch von Registern machen. Gerade wennviele Register zur Verfügung stehen, kann hier – sofern keine spezielle Hardware-unterstützung für Prozeduraufrufe vorhanden ist – ein beträchtlicher Aufwand erfor-derlich sein.

2.1 Inlining

Die erste Idee, um die Kosten von Prozeduraufrufen zu reduzieren, besteht darin, denRumpf der aufgerufenen Prozedur an die Aufrufstelle zu kopieren. Diese Techniknennt man Inlining.

Beispiel 2.1.1 Betrachten wir das Programm:

abs () {b1 ← b;b2 ← −b;max ();

}

max () {if (b1 < b2) ret ← b2;else ret ← b1;

}

Inlining des Rumpfs der Prozedur max () liefert das Programm:

abs () {b1 ← b;b2 ← −b;

if (b1 < b2) ret ← b2;else ret ← b1;

}��

Die Transformation Inlining für parameterlose Prozeduren ist besonders einfach,weil die Behandlung der Parameterübergabe ja bereits durch die Simulation mithil-fe globaler und lokaler Variablen erledigt ist. Nichtsdestoweniger ergeben sich auchhier noch Probleme.

Page 131: œbersetzerbau: Band 3: Analyse und Transformation

2.1 Inlining 121

Inlining kann man zuerst einmal nur an Aufrufstellen durchführen, an denen dieaufzurufende Prozedur statisch bekannt ist. Dies ist in Programmiersprachen wie Cnicht immer gegeben, da Funktionen auch indirekt über Zeiger aufgerufen werdenkönnen. Auch bei objektorientierten Programmiersprachen hängt die jeweils aufzu-rufende Methode vom Laufzeittyp der betreffenden Objekte ab. In diesen Fällen sindzusätzliche Analysen erforderlich, um die Menge der möglicherweise aufzurufen-den Funktionen bzw. Methoden einzuschränken. Nur wenn sicher ist, dass sich derAufruf an der betreffenen Stelle nur auf eine einzige Prozedur beziehen kann, kannInlining für diese Prozedur durchgeführt werden.

Weiterhin muss sichergestellt werden, dass der einkopierte Rumpf der aufgerufe-nen Prozedur nicht die lokalen Variablen der aufrufenden Prozedur modifiziert. Dieskann z.B. dadurch erreicht werden, dass vor dem Einkopieren die lokalen Variablender Prozedur umbenannt werden.

Bei Mehrfachverwendung der selben Prozedur kann Inlining jedoch zu Codedup-lizierung führen. Schlimmer noch: vollständiges Inlining für rekursive Prozedurenterminiert nicht. Um mit rekursiven Programmen umgehen zu können, muss des-halb vorgängig Rekursion in Prozeduraufrufen identifiziert werden. Dabei hilft derAufrufgraph des Programms. Die Knoten dieses Graphen sind die Prozeduren desProgramms. Eine Kante führt von einer Prozedur p zu einer Prozedur q, falls derRumpf von p einen Aufruf der Prozedur q enthält.

Beispiel 2.1.2 Die Aufrufgraphen für die Programme aus Beispiel 2.0.1 bzw. Bei-spiel 2.1.1 sind sehr einfach (Abb. 2.3). Im ersten Fall besteht der Aufrufgraph ausden Knoten main und f, wobei die Prozedur main die Prozedur f und die letztereProzedur sich selbst aufruft. Im zweiten Fall besteht der Aufrufgraph aus den beidenKnoten abs und max zusammen mit einer Kante von abs nach max. ��

main

abs max

f

Abb. 2.3. Die Aufrufgraphen der Programme aus den Beispielen 2.0.1 und 2.1.1.

Zwei Strategien bieten sich an, um für einen Prozeduraufruf zu entscheiden, ob aufihn Inlining angewendet werden soll.

• Kopiere nur Blatt-Funktionen ein, d.h. solche Prozeduren ohne weitere Aufru-fe.

• Kopiere an allen nichtrekursiven Aufrufstellen, d.h. an allen Aufrufstellen, dienicht in starken Zusammenhangskomponenten des Aufrufgraphen liegen.

Page 132: œbersetzerbau: Band 3: Analyse und Transformation

122 2 Interprozedurale Optimierungen

Natürlich sind andere Auswahlstrategien denkbar. Für jeden wie auch immer ausge-wählten Aufruf führen wir die folgende Transformation aus.

Transformation PI:

u

v

v

u

Af = 0; (A ∈ Loc)

Kopie

;

f()

von f

Die mit der leeren Anweisung markierte Kante können wir einsparen, wenn der stop-Knoten von f selbst keine ausgehenden Kanten hat. Die Initialisierungen der lokalenVariablen der einkopierten Prozedur haben wir eingefügt, weil die von uns gewählteSemantik dies so vorschreibt.

2.2 Beseitigung letzter Aufrufe

Betrachten Sie das folgende Beispiel:

f () {if (b2 ≤ 1) ret ← b1;else {

b1 ← b1 · b2;b2 ← b2 − 1;f ();

}}

Nach dem Prozeduraufruf gibt es im Rumpf der aufrufenden Prozedur nichts mehrzu tun. Einen solchen Aufruf f() nennen wir einen letzten Aufruf. Letzte Aufrufebenötigen keinen eigenen Kellerrahmen. Stattdessen können sie im selben Keller-rahmen wie die aufrufende Prozedur ausgewertet werden. Einzig erforderlich ist, diealten lokalen Variablen durch die entsprechenden neuen der aufgerufenen Funktion

Page 133: œbersetzerbau: Band 3: Analyse und Transformation

2.2 Beseitigung letzter Aufrufe 123

f zu ersetzen. Technisch heißt das, dass der Aufruf der Prozedur f durch einen unbe-dingten Sprung an den Anfang des Rumpfs von f ersetzt wird. War der letzte Aufrufrekursiv, wird so Endrekursion in eine Schleife transformiert, d.h. in Iteration. Inunserem Beispiel finden wir:

f () {_f : if (b2 ≤ 1) ret ← b1;

else {b1 ← b1 · b2;b2 ← b2 − 1;goto _f ;

}}

Transformation LC:

v

u

f() :v

u

f()

f() :

A = 0; (A ∈ Loc)

Wieder haben wir dabei angenommen, dass vor Betreten des Rumpfs der angesprun-genen Prozedur die lokalen Variablen mit 0 initialisiert werden müssen.

Die Optimierung letzter Aufrufe ist besonders wichtig bei deklarativen Program-miersprachen, die nicht über Schleifen verfügen. Ein entscheidender Vorteil gegen-über Inlining besteht darin, dass keine Duplizierung von Code erforderlich ist. Auchbei nichtrekursiven Endaufrufen kann die Optimierung nützlich sein. Merkwürdigan der Optimierung ist, dass sie Sprünge aus einer Prozedur in andere Prozedureneinführt, die in dieser Form von modernen höheren Programmiersprachen nicht un-terstützt werden.

Den aktuellen Kellerrahmen für die in einem letzten Aufruf aufgerufene Prozedurwiederzuverwenden, ist nur dann möglich, wenn wie bei unserer Beispielprogram-miersprache die lokalen Variablen der aufrufenden Prozedur nach dem Aufruf nichtmehr zugänglich sind. Die Programmiersprache C z.B. erlaubt es jedoch, über Zeigerauf lokale Variablen an beliebiger Stelle im Aufrufkeller zuzugreifen. Ähnliche Ef-fekte sind möglich, wenn die Programmiersprache Parameterübergabe by-referenceunterstützt. Vor Anwendung dieser Transformation muss dann sichergestellt werden,dass die lokalen Variablen der aufrufenden Funktion tatsächlich nicht mehr zugreif-bar sind. Eine einfache Analyse, die eine Obermenge möglicherweise zugreifbarerVariablen ermittelt, entwickelt Aufg. 2.

Page 134: œbersetzerbau: Band 3: Analyse und Transformation

124 2 Interprozedurale Optimierungen

2.3 Interprozedurale Analyse

Mit unseren bisherigen Methoden sind wir nur in der Lage, jede Prozedur einzelnzu analysieren. Solche Analysen haben auf der einen Seite Vorteile. Da Prozedurentypischerweise nicht sehr groß sind, halten sich die Kosten für die Analyse, die proProzedur aufzuwenden sind, in Grenzen. Damit steigen die Gesamtkosten der Ana-lyse eines Programms im wesentlichen linear mit der Anzahl der Prozeduren. DieTechniken funktionieren ebenfalls bei getrennter Übersetzung. Der Preis, der dafürzu zahlen ist, ist dass die Analyse an jedem Prozeduraufruf jeweils das Schlimmsteannehmen muss. Das bedeutet, dass jegliche Information über alle Variablen und Da-tenstrukturen verloren geht, die der Prozedur möglicherweise zugänglich sind. FürKonstantenpropagation heißt das, dass im wesentlichen nur die Werte lokaler Varia-blen propagiert werden können, die garantiert von außen nicht zugänglich sind.

Selbst bei getrennter Übersetzung liegen jedoch oft die Implementierungen meh-rerer zusammengehöriger Prozeduren vor — etwa, weil sie in der selben Datei abge-legt wurden oder zur selben Programmeinheit (Modul, Klasse) gehören. Aus diesemGrund wollen wir der Frage nachgehen, wie Programme, die aus mehreren Prozedu-ren bestehen, analysiert werden können. Als Beispiel soll eine intraprozedurale Ana-lyse zu einer interprozeduralen Analyse verallgemeinert werden. Dazu betrachtenwir Kopienpropagation aus Kapitel 1.8. Diese Analyse ermittelt für eine gegebeneVariable x zu jedem Programmpunkt eine Menge von Variablen, die mit Sicherheitden zuletzt der Variablen x zugewiesenen Wert enthalten. Eine solche Analyse istauch über Prozedurgrenzen hinaus sinnvoll.

Beispiel 2.3.1 Betrachten Sie das folgende Beispielprogramm:

main () {A ← M[0];if (A) print();b ← A;work ();ret ← 1 − ret;

}

work () {A ← b;if (A) work ();ret ← A;

}

Die Kontrollflussgraphen zu diesem Programm zeigt Abb. 2.4. Innerhalb der Pro-zedur work lässt sich die Umspeicherung des Werts der globalen Variablen b in dielokale Variable A vermeiden. ��Das Problem bei der Verallgemeinerung unseres bisherigen intraprozeduralen An-satzes zur Propagation von Kopien besteht darin, dass wir nun nicht mehr mit einemKontrollfussgraphen arbeiten können, sondern zusätzlich die Effekte sich möglicher-weise rekursiv aufrufender Prozeduren berücksichtigen müssen.

Page 135: œbersetzerbau: Band 3: Analyse und Transformation

2.4 Der funktionale Ansatz 125

0

4

1

3

2

8

9

10

11

5

6

7work ()

NonZero(A)Zero(A)NonZero(A)Zero(A)

A ← M[0]

b ← A

A ← b

print ()

work ()

work ()

ret ← A

main()

ret ← 1 − ret

Abb. 2.4. Die Kontrollflussgraphen für Beispiel 2.3.1.

2.4 Der funktionale Ansatz

Nehmen wir an, wir hätten einen vollständigen Verband D von abstrakten Zuständenals mögliche Analyseinformation bei Erreichen eines Programmpunkts. Bei einemAusführungsschritt gemäß einer normalen Berechnungskante k wird diese Informa-tion durch den abstrakten Kanteneffekt [[k]]� : D → D transformiert, der zu derKantenmarkierung in der betreffenden Analyse gehört. Zusätzlich müssen wir auchmit Kanten k umgehen können, an denen eine Prozedur f aufgerufen wird.

Die grundlegende Idee des funktionalen Ansatzes besteht darin, sich auch denabstrakten Effekt eines Aufrufs als eine solche Transformation vorzustellen und denabstrakten Effekt einer Aufrufskante k durch eine Funktion [[k]]� : D → D zu be-schreiben. Im Gegensatz zu den abstrakten Effekten anderer Anweisungen ist derabstrakte Effekt einer Aufrufkante aber nicht bereits vor Durchführung der Analysebekannt, sondern ergibt sich während der Analyse des Rumpfs der Prozedur.

Zur Realisierung der Analyse benötigen wir Abstraktionen der Operationen enterund combine der operationellen Semantik. Die abstrakte Operation enter� initialisiertden abstrakten Wert für den Startpunkt der Prozedur aus der am Programmpunktvor dem Aufruf vorliegenden Information. Die Operation combine� kombiniert dieInformation, die sich am Ende des Rumpfs der aufgerufenen Prozedur ergeben hat,mit der Information, die vor dem Betreten der Prozedur vorlag. Diese Operationenhaben deshalb die Funktionalität:

enter� : D → D

combine� : D2 → D

Page 136: œbersetzerbau: Band 3: Analyse und Transformation

126 2 Interprozedurale Optimierungen

Der Gesamteffekt der Aufrufkante k ergibt sich dann als:

[[k]]� D = combine� (D, [[f]]� (enter� D))

sofern [[f]]� die Transformation ist, die der Rumpf der Prozedur f bewirkt.Schauen wir uns an, wie die Funktionen enter� und combine� für die Propagation

von Kopien aussehen. Der Fall, dass alle Variablen global sind, ist hier besonders ein-fach: die Funktion enter� ist die Identitätsfunktion, d.h. liefert ihr Argument zurück,und die Funktion combine� liefert ihr zweites Argument:

enter� V = V combine� (V1, V2) = V2

Betrachten wir nun eine Analyse von Programmen mit lokalen Variablen. Unser Zielist, für jeden Programmpunkt die Menge der Variablen zu ermitteln, die den letz-ten Wert enthalten, der in der globalen Variable x abgelegt wurde. Im Verlauf derProgrammausführung kann dieser Wert auch in einer lokalen Variable des Aufruferseiner Prozedur abgelegt sein. Diese lokale Variable ist innerhalb der aufgerufenenProzedur nicht sichtbar. Wird während des Aufrufs der Prozedur der Wert von x neuberechnet, können wir für keine lokale Variable des Aufrufers mehr mit Sicherheitannehmen, dass sie den richtigen Wert enthält. Um zu erkennen, dass während desProzeduraufrufs eine solche Neuberechnung stattgefunden hat, fügen wir eine neuelokale Hilfsvariable • ein, die bei Aufruf jeweils den Wert von x vor dem Aufrufenthalten soll und während des Aufrufs nicht modifiziert wird. Unsere Analyse be-stimmt, ob die Variable • nach Beendigung des Aufrufs mit Sicherheit immer nochden korrekten Wert enthält. Technisch bedeutet das, dass die Funktion enter� alsErgebnis die lokale Hilfsvariable • zusammen mit der die Menge der globalen Va-riablen zurückliefert, die vor dem Aufruf den Wert von x enthalten, zusammen mit•. Die lokalen Variablen des Aufrufers enthalten nach der Rückkehr aus dem Aufrufdann immer noch den zuletzt berechneten Wert von x, wenn • in der Menge enthaltenist, welche die aufgerufene Prozedur zurückliefert. Damit definieren wir:

enter� V = V ∩ Glob ∪ {•}combine� (V1, V2) = (V2 ∩ Glob) ∪ ((• ∈ V2) ? V1 ∩ Loc• : ∅)

wobei Loc• = Loc ∪ {•}. Insgesamt ist der vollständige Verband, den wir für inter-prozedurale Propagation von Kopien einer globalen Variablen x benötigen, gegebendurch:

V = {V ⊆ Vars• | x ∈ V}geordnet durch die Obermengenrelation ⊇, wobei Vars• = Vars ∪ {•}.

Für jeden vollständigen Verband D, für monotone Kanteneffekte [[k]]� sowie mo-notone Funktionen enter� und combine� definieren wir analog zur konkreten Seman-tik in einem ersten Schritt die abstrakte Transformation, die eine pegelerhaltendeBerechnungsfolge bewirkt. Wir definieren:

[[π k]]� = [[k]]� ◦ [[π ]]� für eine normale Kante k[[π1 〈f〉 π2 〈/f〉]]� = H�([[π2]]�) ◦ [[π1]]� für eine Prozedur f

Page 137: œbersetzerbau: Band 3: Analyse und Transformation

2.4 Der funktionale Ansatz 127

Dabei ist die Transformation

H� : (D → D) → D → D

definiert durch:H� g d = combine�(d, g(enter�(d)))

Der abstrakte Effekt [[f]]� für eine Prozedur f soll eine obere Schranke für den ab-strakten Effekt [[π ]]� jeder pegelerhaltenden Berechnung π für f, d.h. jeder pegeler-haltenden Berechnung vom Startpunkt zum Endpunkt von f zu sein. Um die Effekte[[f]]� zu ermitteln bzw. zu approximieren, stellen wir erneut ein Ungleichungssystemauf, nun über dem vollständigen Verband aller monotonen Funktionen in D → D:

[[startf ]]� � Id startf Startpunkt von Prozedur f

[[v]]� � H�([[f]]�) ◦ [[u]]� k = (u, f(), v) Aufrufkante

[[v]]� � [[k]]� ◦ [[u]]� k = (u, lab, v) normale Kante

[[f]]� � [[stopf ]]� stopf Endpunkt von f

Für einen Programmpunkt v einer Prozedur f beschreibt die Funktion [[v]]� : D → D

die Effekte aller pegelerhaltenden Berechnungsfolgen π , die vom Startpunkt von fnach v führen. Weil die Ausdrücke auf den rechten Seiten der Ungleichungen mo-notone Funktionen beschreiben, hat das Ungleichungssystem eine kleinste Lösung.Zur Korrektheit des Ansatzes beweist man die folgende Verallgemeinerung des Sat-zes 1.6.1:

Satz 2.4.1 Sei [[ . ]]� die kleinste Lösung des interprozeduralen Ungleichungssystems.Dann gilt:

1. [[v]]� � [[π ]]� für jede pegelerhaltende Berechnungsfolge π von startf nach v,sofern v ein Programmpunkt der Prozedur f ist;

2. [[f]]� � [[π ]]� für jede pegelerhaltende Berechnungsfolge π der Prozedur f. ��Satz 2.4.1 lässt sich mit Induktion über die Struktur von pegelerhaltenden Berech-nungsfolgen π beweisen. Der Satz gewährleistet, dass jede Lösung des Unglei-chungssystems verwendet werden kann, um die abstrakten Effekte von Prozedurauf-rufen zu approximieren.

Zwei grundlegende Probleme ergeben sich mit diesem Ansatz. Zum einen müs-sen die einzelnen monotonen Funktionen aus D → D effektiv repräsentiert werden.Nicht immer besitzen die auftretenden Funktionen eine einfache Darstellung. Ist dervollständige Verband D endlich, können wir sämtliche vorkommenden Funktionenzumindest im Prinzip durch ihre jeweiligen Wertetabellen repräsentieren. Dies istnicht mehr möglich, wenn der vollständige Verband D nicht endlich ist wie z.B.bei der Konstantenpropagation. Im Falle unendlicher vollständiger Verbände kommterschwerend hinzu, dass aufsteigende Ketten auftreten können, die niemals stabilwerden.

Page 138: œbersetzerbau: Band 3: Analyse und Transformation

128 2 Interprozedurale Optimierungen

Für unsere Beispielanwendung der Propagation von Kopien hilft uns die Be-obachtung, dass der vollständige Verband V atomar ist. Die Menge der atomarenElemente in V ist dabei gegeben durch:

{Vars•\{z} | z ∈ Vars•\{x}}Weiterhin sind alle auftretenden Kanteneffekte nicht nur monoton, sondern sogar dis-tributiv (bzgl. ⊇). Statt darum alle monotonen Funktionen in V → V zu betrachten,genügt es, innerhalb des Teilverbands der distributiven Funktionen zu rechnen.

Distibutive Funktionen über einem atomaren Verband D besitzen eine kompakteDarstellung. Sei A ⊆ D die Menge der atomaren Elemente in D. Gemäß Satz 1.7.1aus Kapitel 1.7 lässt sich jede distributive Funktion g : D → D darstellen als

g(V) = b � ⊔{h(a) | a ∈ A ∧ a � V}für eine Funktion h : A → D und ein Element b ∈ D.

Jede distributive Funktion g zur Propagation von Kopien lässt sich deshalb durchmaximal k Mengen darstellen, falls k die Anzahl der Variablen des Programms ist.Damit ist auch die Höhe des vollständigen Verbands distributiver Funktionen be-schränkt durch k2.

Beispiel 2.4.1 Betrachten wir das Programm aus Beispiel 2.3.1. Die Menge Vars• isthier gegeben durch {A, b, ret, •}. Nehmen wir an, wir wollen die globale Variable bverfolgen. Den Zuweisungen A ← b und ret ← A entsprechen die Funktionen

[[A ← b]]�C = C ∪ {A} =: g1(C)[[ret ← A]]�C = (A ∈ C) ? (C ∪ {ret}) : (C\{ret}) =: g2(C)

Die beiden Kanteneffekte g1, g2 können wir durch die Paare (h1, Vars•) bzw. (h2, Vars•)repräsentieren mit

h1 h2

{b, ret, •} Vars• {b, •}{b, A, •} {b, A, •} Vars•{b, A, ret} {b, A, ret} {b, A, ret}

In einer ersten Runde Round-Robin-Iteration ergibt sich die Identität für Programm-punkt 7, die Funktion g1 für die Programmpunkte 8, 9 und 10 und für den Programm-punkt 11 die Komposition g2 ◦ g1. Diese Funktion ist gegeben durch:

g2(g1(C)) = C ∪ {A, ret} =: g3(C)

Diese Funktion liefert uns die erste Approximation für den Rumpf der Funktionwork. Damit erhalten wir für einen Aufruf der Funktion work:

combine�(C, g3(enter�(C))) = C ∪ {ret} =: g4(C)

Page 139: œbersetzerbau: Band 3: Analyse und Transformation

2.4 Der funktionale Ansatz 129

In der zweiten Runde der Round-Robin-Iteration ist der neue Wert für den Pro-grammpunkt 10 gegeben durch die Funktion g1 ∩ g4 ◦ g1. Dabei ist:

g4(g1(C)) = C ∪ {A, ret} und folglich:

g1(C) ∩ g4(g1(C)) = C ∪ {A} = g1(C)

wie in der letzten Iteration. In diesem Beispiel wird der Fixpunkt damit bereits nachder ersten Iteration erreicht. ��Für Analysen mit distributiven Kanteneffekten kann ein Koinzidenztheorem ähnlichSatz 1.6.3 für intraprozedurale Analysen bewiesen werden. Zusätzlich benötigen wirbei dieser Verallgemeinerung, dass sich auch der Effekt [[k]]� einer Aufrufkante k =(u, f(), v) distributiv verhält. Wir finden:

Satz 2.4.2 Nehmen wir an, zu jeder Prozedur f und jedem Programmpunkt v von fgebe es mindestens eine pegelerhaltende Berechnungsfolge vom Startpunkt startf derProzedur f nach v. Nehmen wir weiterhin an, alle Effekte [[k]]� der normalen Kantenwie auch die Transformation H� seien distributiv, d.h. insbesondere:

H�(⊔F ) =

⊔{H�(g) | g ∈ F}für jede nichtleere Menge F distributiver Funktionen. Dann gilt für jede Prozedur fund jeden Programmpunkt v von f,

[[v]]� =⊔{[[π ]]� | π ∈ Tv}

Dabei ist Tv die Menge aller pegelerhaltenden Berechnungsfolgen vom Startpunktstartf der Prozedur f nach v.

Der Beweis dieses Satzes ist eine Verallgemeinerung des entsprechenden Satzes fürintraprozedurale Analysen. Um Satz 2.4.2 leicht anwenden zu können, benötigenwir eine möglichst große Klasse von Funktionen enter� und combine�, so dass dieentsprechende Transformation H� distributiv ist. Dazu beobachten wir:

Satz 2.4.3 Sei enter� : D → D distributiv und combine� : D2 → D von der Form:

combine�(x1, x2) = h1(x1) � h2(x2)

für zwei distributive Funktionen h1, h2 : D → D. Dann ist H� distributiv, d.h. es gilt:

H�(⊔F ) =

⊔{H�(g) | g ∈ F}für jede nichtleere Menge F distributiver Funktionen.

Beweis. Sei F eine nichtleere Menge distributiver Funktionen. Dann haben wir:

H�(⊔F ) = h1 � h2 ◦ (

⊔F ) ◦ enter�

= h1 � h2 ◦ (⊔{g ◦ enter� | g ∈ F})

= h1 � (⊔{h2 ◦ g ◦ enter� | g ∈ F})

=⊔{h1 � h2 ◦ g ◦ enter� | g ∈ F}

=⊔{H�(g) | g ∈ F}

Page 140: œbersetzerbau: Band 3: Analyse und Transformation

130 2 Interprozedurale Optimierungen

Dabei gilt die zweite Gleichung, weil die Komposition ◦ im ersten Argument distri-butiv ist. Die dritte Gleichung gilt dann, weil bei distributivem ersten Argument dieKomposition auch distributiv im zweiten Argument ist. ��Erinnern wir uns an die Definitionen der beiden Funktionen enter� und combine� fürdie Propagation von Kopien. Wir hatten:

enter� V = V ∩ Glob ∪ {•}combine� (V1, V2) = (V2 ∩ Glob) ∪ (• ∈ V2) ? V1 ∩ Loc : ∅

= ((V1 ∩ Loc•) ∪ Glob) ∩((V2 ∩ Glob) ∪ Loc•) ∩ (Glob ∪ (• ∈ V2) ? Vars• : Glob)

Die Funktion enter� ist damit distributiv, und die Funktion combine� lässt sich alsDurchschnitt einer distributiven Funktion des ersten Arguments und einer distribu-tiven Funktion des zweiten Arguments darstellen. Deshalb lässt sich Satz 2.4.3 an-wenden (für die Ordnung ⊇). Wir folgern, dass die Transformation H� distributivfür distributive Funktionen ist. Deshalb gilt für unsere Analyse das interprozeduraleKoinzidenztheorem aus Satz 2.4.2.

2.5 Interprozedurale Erreichbarkeit

Nehmen wir an, wir hätten in einem ersten Schritt die abstrakten Effekte [[ f ]]� derRümpfe der Prozeduren f ermittelt oder zumindest sicher approximiert. Im zweitenSchritt wollen wir dann für jeden Programmpunkt u eine Eigenschaft D[u] ∈ D

berechnen, die mit Sicherheit bei Erreichen des Programmpunkts u angenommenwird. Dazu stellen wir erneut ein Ungleichungssystem auf:

D[startmain] � enter�(d0)D[startf ] � enter�(D[u]) (u, f(), v) Aufrufkante

D[v] � combine�(D[u], [[f]]�(enter�(D[u]))) (u, f(), v) Aufrufkante

D[v] � [[k]]�(D[u]) k = (u, lab, v) normale Kante

wobei d0 ∈ D die Information vor der Programmausführung beschreibt.Weil alle rechten Seiten monoton sind, besitzt dieses Ungleichungssystem eine

kleinste Lösung. Zum Nachweis der Korrektheit unseres Vorgehens definieren wir inAnalogie zur konkreten Semantik für jede Berechnungsfolge π , die v erreicht, denabstrakten Effekt [[π ]]� : D → D. Der folgende Satz setzt die abstrakten Effekte derden Programmpunkt v erreichenden Berechnungsfolgen, zu den abstrakten WertenD[v] in Beziehung, die das Lösen des Ungleichungssystems liefert.

Satz 2.5.1 Sei D[ . ] die kleinste Lösung des zweiten interprozeduralen Unglei-chungssystems. Dann gilt:

D[v] � [[π ]]� d0

für jede Berechnungsfolge, die v erreicht. ��

Page 141: œbersetzerbau: Band 3: Analyse und Transformation

2.6 Bedarfsgetriebene interprozedurale Analyse 131

Beispiel 2.5.1 Betrachten wir erneut das Programm aus Beispiel 2.3.1. Dann erhal-ten wir für die Programmpunkte 0 bis 11:

0 {b}1 {b}2 {b}3 {b}4 {b}5 {b, ret}6 {b}

7 {b, •}8 {b, A, •}9 {b, A, •}

10 {b, A, •}11 {b, A, •, ret}

Wir schließen, dass innerhalb der Prozedur work anstelle der lokalen Variablen Astets auch direkt die globale Variable b verwendet werden kann. ��

Sind alle Programmpunkte erreichbar, dann gilt auch für den zweiten Teil der inter-prozeduralen Analyse ein entsprechendes Koinzidenztheorem.

Satz 2.5.2 Nehmen wir an, zu jedem Programmpunkt v gebe es mindestens eine Be-rechnungsfolge, die v erreicht. Nehmen wir weiterhin an, alle Effekte [[k]]� : D → D

der normalen Kanten wie auch die Transformation H� seien distributiv. Dann giltfür jeden Programmpunkt v,

D[v] =⊔{[[π ]]�d0 | π ∈ Pv}

Dabei ist Pv die Menge aller Berechnungsfolgen, die v erreichen. ��

2.6 Bedarfsgetriebene interprozedurale Analyse

Die Annahme, dass der vollständige Verband D endlich und die benötigten monoto-nen Funktionen aus D → D kompakt repräsentierbar sind, ist in vielen praktischenFällen gewährleistet. Dies gilt insbesondere für eine interprozedurale Analyse derverfügbaren Zuweisungen. Es gibt jedoch interessante Analysen, die man interpro-zedural durchführen möchte, für die entweder der Verband nicht endlich oder fürdie Funktionen keine kompakten Repräsentationen zur Verfügung stehen. In diesemFall können wir auf eine Beobachtung zurückgreifen, die bereits in einer sehr frühenArbeit von Patrick Cousot wie in der Arbeit von Sharir/Pnueli über interprozeduraleAnalyse zu finden ist. Oft werden Prozeduren nur in wenigen verschiedenen abstrak-ten Situationen aufgerufen. In diesem Fall reicht es aus, nur die abstrakten Aufrufeder Prozeduren zu analysieren, deren Werte wirklich benötigt werden.

Diese Idee wollen wir nun umsetzen. Dazu stellen wir (zumindest konzeptuell)das folgende Ungleichungssystem auf:

Page 142: œbersetzerbau: Band 3: Analyse und Transformation

132 2 Interprozedurale Optimierungen

D[v, a] � a v Eintrittspunkt

D[v, a] � combine� (D[u, a],D[ f , enter�(D[u, a])])(u, f(), v) Aufrufkante

D[v, a] � [[lab]]� (D[u, a]) k = (u, lab, v) normale Kante

D[ f , a] � D[stopf , a] stopf Endpunkt von f

Dabei bezeichnet die Unbekannte D[v, a] den abstrakten Zustand bei Erreichen desProgrammpunkts v einer Prozedur, die im abstrakten Zustand a aufgerufen wurde.Sie entspricht dem Wert [[v]]�(a), den unsere Analyse der abstrakten Effekte vonProzeduren für den Programmpunkt v berechnen würde.

Beachten Sie, dass in diesem Ungleichungssystem bei der Modellierung vonProzeduraufrufen geschachtelte Unbekannte auftreten: der Wert der inneren Unbe-kannten beeinflusst, für welche zweite Komponente b wir den Wert einer VariablenD[ f , b] benötigen. Diese indirekte Adressierung hat zur Folge, dass die Variablen-abhängigkeiten während der Fixpunktiteration nicht mehr statisch bekannt sind, son-dern sich während der Iteration ändern können!

Dieses Ungleichungssystem wird im Allgemeinen sehr groß sein: jeder Pro-grammpunkt wird ja so oft kopiert wie es Elemente in D gibt! Andererseits wol-len wir es auch nicht komplett lösen. Uns reicht es, die korrekten Werte für jeneAufrufe zu ermitteln, die vorkommen, d.h. deren Werte während der Analyse tat-sächlich angefragt werden. Technisch heißt das, dass wir uns auf diejenigen Un-bekannten beschränken, die bei der Fixpunktiteration zur Berechnung des WertsD[main, enter�(d0)] erforderlich sind.

Die bedarfsgetriebene Auswertung eines Ungleichungssystems erfordert einengeeigneten Fixpunktalgorithmus. Hier bietet sich der lokale Fixpunktalgorithmus ausKapitel 1.12 an. Dieser Fixpunktalgorithmus exploriert den Variablenraum entspre-chend den (eventuell dynamischen) Variablenabhängigkeiten. Als Ergebnis der Fix-punktiteration erhalten wir für jede Prozedur f die Menge der abstrakten Zustände,in denen f betreten wird sowie die Werte an allen ihren Programmpunkten für jedendieser Aufrufe.

Die bedarfsgetriebene Variante des funktionalen Ansatzes wollen wir an einerBeispielanalyse praktisch erproben. Dazu wählen wir interprozedurale Konstanten-propagation. Der vollständige Verband ist hier gegeben durch:

D = (Vars → Z�)⊥Dieser vollständige Verband hat zwar endliche Höhe, ist aber nicht endlich. Die inter-prozedurale Konstantenpropagation wird darum im schlimmsten Fall nicht terminie-ren! Bei Konstantenpropagation sind die Funktionen enter� und combine� gegebendurch:

enter� D =

{⊥ falls D = ⊥D ⊕ {A → � | A lokal} sonst

combine�(D1, D2) =

{⊥ falls D1 = ⊥∨ D2 = ⊥D1 ⊕ {b → D2(b) | b global} sonst

Page 143: œbersetzerbau: Band 3: Analyse und Transformation

2.6 Bedarfsgetriebene interprozedurale Analyse 133

Zusammen mit den intraprozeduralen abstrakten Kanteneffekten zur Konstantenpro-pagation liefert uns das Ungleichungssystem ein Analyseverfahren, dass für Ver-bände D endlicher Höhe genau dann terminiert, wenn der Fixpunktalgorithmus nurendlich viele Unbekannte D[v, a] betrachten muss.

Beispiel 2.6.1 Betrachten wir eine leichte Modifikation des Programms aus Beispiel2.3.1 mit den Kontrollflussgraphen aus Abb. 2.5. Sei d0 die Variablenbelegung:

0

4

1

3

2

8

9

10

11

5

6

7work ()

NonZero(A)Zero(A)NonZero(A)Zero(A)

b ← A

A ← b

print ()

work ()

work ()

ret ← A

main()

ret ← 1 − ret

A ← 0

Abb. 2.5. Die Kontrollflussgraphen für Beispiel 2.6.1.

d0 = {A → �, b → �, ret → �}Dann ergibt sich die folgende Reihenfolge von Auswertungen:

Page 144: œbersetzerbau: Band 3: Analyse und Transformation

134 2 Interprozedurale Optimierungen

A b ret

0, d0 � � �1, d0 0 � �2, d0 ⊥3, d0 0 � �4, d0 0 0 �7, d1 � 0 �8, d1 0 0 �9, d1 ⊥

10, d1 0 0 �11, d1 0 0 0

5, d0 0 0 06, d0 0 0 1

main, d0 0 0 1

für d1 = {A → �, b → 0, ret → �}. Die rechte Seite jeder Unbekannten wird imBeispiel höchstens einmal ausgewertet. ��Im Beispiel terminiert die Analyse bereits nach der ersten Iteration. Für jeden Pro-grammpunkt u musste nur eine einzige Kopie in Betracht gezogen werden. Im Allge-meinen wird man allerdings mit mehreren Kopien zu rechnen haben. Weil der voll-ständige Verband für Konstantenpropagation keine unendlichen echt aufsteigendenKetten besitzt, terminiert das Verfahren, sofern jede Prozedur während der Iterationnur mit endlich vielen verschiedenen Argumenten aufgerufen wird.

2.7 Der Call-String-Ansatz

Ein alternativer Ansatz zur Analyse von Programmen mit Prozeduren basiert auf ei-ner Abstraktion der kellerbasierten operationellen Semantik. Das Ziel besteht darin,die Menge aller erreichbaren Aufrufkeller zu berechnen. Im allgemeinen wird dieseMenge jedoch unendlich sein. Alternativ könnte man nur Keller bis zu einer festenTiefe d exakt behandeln und sich bei tieferen Kellern z.B. nur den oberen Abschnittder Länge d berücksichtigen. Diese Idee wurde bereits in der grundlegenden Arbeitvon Sharir und Pnueli vorgestellt.

Die Komplexität steigt jedoch drastisch mit der Tiefe der berücksichtigten Kel-lerabschnitte an. In praktischen Anwendungen wird man sich deshalb meistens mitKellertiefen 1 oder gar 0 begnügen.

Kellertiefe 0 bedeutet, dass wir das Betreten einer Prozedur f als unbedingtenSprung an den Anfang der Prozedur f und das Verlassen der Prozedur f als möglichenRücksprung hinter die Aufrufkante approximieren. Bei diesen Rücksprüngen müssendie Werte der lokalen Variablen vor dem Aufruf rekonstruiert werden.

Page 145: œbersetzerbau: Band 3: Analyse und Transformation

2.7 Der Call-String-Ansatz 135

Beispiel 2.7.1 Betrachten wir erneut unser Programm aus Beispiel 2.6.1. Durch Ein-fügen der entsprechenden Einsprung- und Rücksprungkanten erhält man den Gra-phen aus Abb. 2.6. Den so konstruierten Graph nennt man auch interprozeduralen

0

4

5

1

3

2

8

9

10

11

6

7work ()

enter�

combine�

combine�

NonZero(A)Zero(A)NonZero(A)Zero(A)

A ← b

enter�

b ← A

A ← 0

main()

print ()

ret ← 1 − ret

ret ← A

Abb. 2.6. Der interprozedurale Supergraph zu dem Beispiel aus Abb. 2.4.

Supergraph. ��Sei D der vollständige Verband für unsere Analyse mit den Kanteneffekten [[lab]]�und den Funktionen enter� : D → D und combine� : D2 → D zur Behandlungvon Prozeduren. Zur Berechnung von Invarianten D[v] für jeden Programmpunkt vstellen wir das folgende Ungleichungssystem auf:

D[startmain] � enter�(d0)D[startf ] � enter�(D[u]) (u, f(), v) Aufrufkante

D[v] � combine� (D[u],D[f]) (u, f(), v) Aufrufkante

D[v] � [[lab]]� (D[u]) k = (u, lab, v) normale Kante

D[f] � D[stopf ] stopf Endpunkt von f

Beispiel 2.7.2 Betrachten wir erneut das Programm aus Beispiel 2.6.1. Die Unglei-chungen für die Programmpunkte 5, 7, 10 sind dann:

D[5] � combine� (D[4],D[work])

D[7] � enter� (D[4])D[7] � enter� (D[9])

D[10] � combine� (D[9],D[work])

Page 146: œbersetzerbau: Band 3: Analyse und Transformation

136 2 Interprozedurale Optimierungen

��

0

4

5

1

3

2

8

9

10

11

6

7work ()

enter�

enter�

combine�

combine�

NonZero(A)Zero(A)NonZero(A)Zero(A)

b ← A

A ← bA ← 0

ret ← A

ret ← 1 − ret

print ()

main()

Abb. 2.7. Ein unmöglicher Pfad im interprozeduralen Supergraphen aus Abb. 2.6.

Die Korrektheit dieser Analyse zeigt man relativ zu unserer operationellen Semantik.Im Beispiel findet die Konstantenpropagation zwar die gleichen Ergebnisse wie dievolle Konstantenpropagation. Im interprozeduralen Supergraphen gibt es jedoch zu-sätzliche (tatsächlich unmögliche) Pfade, die die berechneten Ergebnisse möglicher-weise verschlechtern. Einen solchen unmöglichen Pfad für unser Beispielprogrammzeigt Abb. 2.7.

Da für jeden Programmpunkt aber nur genau ein abstrakter Wert berechnet wird,terminiert die Analyse – sofern in dem verwendeten vollständigen Verband alle auf-steigenden Ketten irgendwann stabil werden.

2.8 Aufgaben

1. Parameterübergabe. Geben Sie ein allgemeines Verfahren an, wie globale Va-riablen zur Call-by-Value-Parameterübergabe eingesetzt werden können.Zeigen Sie, dass globale Variablen auch zur Rückgabe der Ergebnisse eines Pro-zeduraufrufs taugen.

2. Referenzparameter. Erweitern Sie unsere Beispielprogrammiersprache um dieMöglichkeit, die Adresse &A einer lokalen Variablen A als Wert in einer ande-ren Variablen oder im Speicher abzulegen.

Page 147: œbersetzerbau: Band 3: Analyse und Transformation

2.9 Literaturhinweise 137

a) Entwerfen Sie eine einfache Analyse, die eine Obermenge der lokalen Varia-blen ermittelt, deren Adresse ermittelt und in einer anderen Variablen oderim Speicher abgelegt wird.

b) Verbessern Sie die Genauigkeit Ihrer Analyse, indem Sie zusätzlich die lo-kalen Variablen A ermitteln, deren Adresse &A zwar ermittelt wird, dannaber selbst nur lokalen Variablen zugewiesen wird.

c) Erläutern Sie, wie Ihre Analysen bei der Beseitigung letzter Aufrufe einge-setzt werden können.

3. Beseitigung von Rekursion, Inlining. Betrachten Sie das Programm:

f1() {if (n ≤ 1) z ← y;else {

n ← n − 1;z ← x + y;x ← y;y ← z;f1();

}}

f() {x ← 1;y ← 1;f1();

}

main() {n ← M[17];f();M[42] ← z;

}

Beseitigen Sie die Rekursion in der Funktion f1. Führen Sie dann Inlining durch!

2.9 Literaturhinweise

Ein Ansatz zur Analyse von Programmen mit rekursiven Prozeduren findet sichbereits in der Arbeit von Patrick und Radhia Cousot [CC77b]. Unabhängig davondiskutieren Micha Sharir und Amir Pnueli den funktionalen wie den Call-String-Ansatz in [SP81]. Diese Arbeit bietet auch ein interprozedurales Koinzidenztheo-rem für den Fall von Prozeduren ohne lokale Variablen. Die Verallgemeinerung die-ses Theorems auf Prozeduren mit lokalen Variablen bietet [KS92]. Eine Diskussionverschiedener lokaler Fixpunktalgorithmen mit Anwendungen bei der Analyse vonProlog-Programmen findet sich in [FS99]. Die Anwendung interprozeduraler Ana-lysemethoden zur genauen Analyse von Schleifen schlagen Florian Martin, MartinAlt, Reinhard Wilhelm und Christian Ferdinand vor [MAWF98].

Eine wichtige Optimierung für objektorientierte Programmiersprachen versucht,Datenobjekte fester Größe zu erkennen, die nicht aus einem gegebenen Methoden-(bzw. Prozedur-) Aufruf entkommen: diese brauchen nicht auf der Halde, sondernkönnen direkt auf dem Keller allokiert werden [CGS+99].

Für objektorientierte Programme spielt Inlining eine herausragende Rolle. Diesliegt daran, dass in vielen objektorientierten Systemen viele kleine Funktionen (Me-thoden) existieren (Setter und Getter). Diese bestehen nur aus wenigen Anweisun-gen, so dass der Aufwand des Rufens der Methode stärker zu Buche schlägt als die

Page 148: œbersetzerbau: Band 3: Analyse und Transformation

138 2 Interprozedurale Optimierungen

eigentlich ausgeführte Berechnung. Allerdings steht aggressivem Inlining in objekt-orientierten Systemen der dynamische Funktionsaufruf im Wege. Weil der statischeTyp eines Objekts von dem Laufzeittyp des Objektes abweichen kann, ist im all-gemeinen die aufzurufende Methode nicht genau bekannt. Um diese genau zu er-mitteln, benötigt man eine interprozedurale Analyse, die den dynamischen Typendes Objektes an der Aufrufstelle ermittelt. Spielt die Effizienz der Übersetzung eineRolle (was bei just-in-time-Übersetzern der Fall ist), greift man hier zu einfachen,kontext-insensitiven Ansätzen wie z.B. der Rapid Type Analysis [Bac97]. Diese be-trachtet nur die Aufrufstellen des Programms und ignoriert alle Variablen und derenZuweisungen. Auf Kosten der Effizienz der Übersetzung kann die Präzision der Ana-lyse durch Betrachtung von Zuweisungen an Variablen und deren Typen gesteigertwerden [SHR+00].

Unsere Liste von Programmoptimierungen ist keineswegs vollständig. Nicht dis-kutiert wurden z.B. Methoden zur Reduktion der Stärke von Operatoren [PS77,Pai90, SLGA03] sowie ausgefeilte Techniken zum Umgang mit Feldern oder dy-namischen Datenstrukturen oder Nebenläufigkeit.

Page 149: œbersetzerbau: Band 3: Analyse und Transformation

3

Optimierung funktionaler Programme

Oberflächlich betrachtet, sind funktionale Programme imperative Programme, in de-nen es keine Zuweisungen gibt.

Beispiel 3.0.1 Betrachten Sie das folgende Programm der Programmiersprache OCAML.

let rec fac2 x y = if y ≤ 1 then xelse fac2 (x · y) (x − 1)

in let fac x = fac2 1 x

Bestimmte Konzepte, die uns von imperativen Programmen vertraut sind, fehlen. Esgibt z.B. keinen sequentiellen Kontrollfluss und keine Schleifen. Andererseits sindso gut wie alle Funktionen rekursiv. ��Außer Rekursion kommen in funktionalen Sprachen wie OCAML, SCALA und HAS-KELL weitere Konzepte hinzu, die bei imperativen Programmiersprachen selten an-geboten werden. Dazu gehören Fallunterscheidungen durch Pattern Matching aufzusammengesetzten Werten, partielle Anwendung höherer Funktionen und, gegebe-nenfalls, verzögerte Auswertung von Abschlüssen. Die für diese Programmierspra-chen bereitgestellte automatische Typinferenz führt dazu, dass viele Funktionen po-lymorph getypt werden und darum sämtliche Werte (zunächst einmal) auf der Haldeangelegt werden.

Um die Portabilität des Übersetzers zu erhöhen, übersetzen einige Übersetzerfür funktionale Programmiersprachen zuerst einmal in eine imperative Zielsprache.Der Glasgow Haskell Übersetzer ghc etwa bietet eine Option an, nach C zu über-setzen. Ein gängiger Übersetzer für die imperative Zielsprache kann dann verwendetwerden, um lauffähigen Objekt-Code zu erzeugen. Andere Übersetzer übersetzendirekt in die Sprache einer geeigneten virtuellen Maschine. Der Übersetzer für SCA-LA erzeugt Code der Java Virtual Machine, während der Übersetzer für F# .NET-Instruktionen erzeugt.

Eine Möglichkeit zur Optimierung funktionaler Programme in Übersetzern mitimperativer Zwischensprache besteht darin, die Optimierungen eines Übersetzers

H. Seidl et al., Übersetzerbau, eXamen.press, DOI 10.1007/978-3-642-03331-5_3,c© Springer-Verlag Berlin Heidelberg 2010

Page 150: œbersetzerbau: Band 3: Analyse und Transformation

140 3 Optimierung funktionaler Programme

dieser Sprache auszunutzen. Diese Strategie ist keineswegs abwegig, wenn man be-denkt, dass Übersetzer für funktionale Sprachen typischerweise einen „künstlichen“intraprozeduralen Kontrollfluss generieren, indem sie Folgen von let-Definitionen inFolgen von Zuweisungen und letzte Aufrufe in Sprünge übersetzen. Beide Aufrufevon fac2 etwa in unserem Beispielprogramm sind letzte Aufrufe. Sieht man einmalvom Anlegen sämtlicher Werte – und das heißt in diesem Fall auch von int-Werten –in der Halde ab, könnte ein für die Funktion fac erzeugtes imperatives Programm soaussehen:

int fac(int x) {int a, a1, b, b1

a ← 1; b ← x;fac2 : if (b ≤ 1) return a;

else {a1 ← a · b; b1 ← b − 1;a ← a1; b ← b1;goto fac2;}

}Die bisher beschriebenen intraprozeduralen Optimierungen für imperative Program-me können deshalb auch benutzt werden, um in funktionalen Programmen einfacheOptimierungen wie die Beseitigung von Zuweisungen an tote Variablen oder diePropagation von Konstanten bzw. Kopien vorzunehmen. Im Beispiel könnten so dieHilfsvariablen a1, b1 beseitigt werden, die für die Berechnung der aktuellen Parame-ter des rekursiven Aufrufs von fac2 eingeführt wurden.

Im Allgemeinen jedoch ist der Kontrollfluss, der sich bei der Übersetzung einesfunktionalen Programms ergibt, ziemlich unübersichtlich. Bessere Ergebnisse bei derAnalyse und damit bei der Optimierung lassen sich erzielen, wenn die speziellenEigenschaften und Ineffizienzen funktionaler Programme berücksichtigt werden.

3.1 Eine einfache funktionale Programmiersprache

Wie im Buch Wilhelm/Seidl: Virtuelle Maschinen beschränken wir uns auf ein einfa-ches Fragment der funktionalen Programmiersprache OCAML. Wir betrachten Aus-drücke e und Muster p gemäß der folgenden Grammatik:

Page 151: œbersetzerbau: Band 3: Analyse und Transformation

3.2 Einige einfache Optimierungen 141

e ::= b | (e1, . . . , ek) | c e1 . . . ek | fun x → e| (e1 e2) | (�1 e) | (e1 �2 e2) |let x1 = e1 in e0 |let rec x1 = e1 and . . . and xk = ek in ematch e0 with p1 → e1 | . . . | pk → ek

if e0 then e1 else e2

p ::= b | x | c x1 . . . xk | (x1, . . . , xk)

wobei b einen Wert eines Basistyps, x eine Variable, c einen Datenkonstruktor und �i

einen i-stelligen Operator bezeichnen. Die Operatoren liefern Basiswerte zurück. Be-achten Sie, dass Funktionen grundsätzlich einstellig sind. Andererseits stellt OCAML

Tupel (e1, . . . , ek) beliebiger Stelligkeit k ≥ 0 zur Verfügung, welche als Ersatz fürMehrstelligkeit verwendet werden können. Auch verzichten wir auf das Auflistenformaler Parameter x1, . . . , xk auf der linken Seite von Funktionsdefinitionen undverwenden stattdessen konsequent Funktionsabstraktionen fun x1 → fun x2 →. . . fun xk → . . .. In unserer Kernsprache haben wir weiterhin auf Funktionsdefini-tionen mit Fallunterscheidung verzichtet, da sich diese durch Fallunterscheidung mitHilfe von match-Ausdrücken ausdrücken lassen. Weiterhin nehmen wir implizit an,dass unsere Programme stets wohlgetypt sind.

Die Definition einer Funktion max, die das Maximum zweier Zahlen berechnet,sieht dann etwa so aus:

let max = fun x → fun y → if x1 < x2 then x2

else x1

3.2 Einige einfache Optimierungen

In diesem Abschnitt stellen wir einige einfache Optimierungen für funktionale Pro-gramme vor. Die wesentliche Idee dieser Optimierungen besteht darin, Auswertungs-schritte aus der Laufzeit in die Übersetzungszeit vorzuverlegen.

Eine Funktionsanwendung (fun x → e0) e1 lässt sich in den let-Ausdruck:let x = e1 in e0 umschreiben.

Eine Fallunterscheidung lässt sich optimieren, wenn über den Ausdruck, mit des-sen Wert eine Liste von Mustern verglichen wird, bereits zur Übersetzungszeit etwasbekannt ist. Betrachten Sie einen Ausdruck:

match c e1 . . . ek with . . . c x1 . . . xk → e . . .

wobei alle Muster, die links von c x1 . . . xk stehen, mit einem Konstruktor beginnen,der verschieden von c ist. Dann wissen wir, dass nur die Alternative für c x1 . . . xkausgewählt werden kann. Wir transformieren deshalb diesen Ausdruck in:

let x1 = e1 . . . in let xk = ek in e

Page 152: œbersetzerbau: Band 3: Analyse und Transformation

142 3 Optimierung funktionaler Programme

In beiden Fällen ist die Transformation semantikerhaltend und ersetzt kompliziertereProgrammkonstrukte durch let-Ausdrücke.

Einen let-Ausdruck let x = e1 in e0 kann man umschreiben in e0[e1/x], d.h. inden Hauptausdruck e0, in dem jedes freie Vorkommen von x durch den Ausdruck e1ersetzt ist. Diese Transformation entspricht der β-Reduktion

im λ-Kalkül. Sie ist jedoch nur dann anwendbar, wenn keine der in e1 freienVariablen durch die Substitution in e0 gebunden wird.

Beispiel 3.2.1 Betrachten Sie den Ausdruck:

let x = 17in let f = fun y → x + yin let x = 4in f x

Die Variable x, die in der Definition von f sichtbar ist, repräsentiert den Wert 17,während die Variable x, die in der Anwendung f x sichtbar ist, den Wert 4 repräsen-tiert. Der Ausdruck liefert deshalb den Wert 21.

Die Anwendung der let-Optimierung auf das zweite let liefert dagegen den Aus-druck:

let x = 17in let x = 4in (fun y → x + y) x

Die Variable x, die nun sowohl in der Funktion wie ihrem Argument sichtbar ist,repräsentiert den Wert 4. Der Ausdruck liefert darum den Wert 8. ��Es gibt verschiedene Möglichkeiten, dieses Problem zu lösen. Die einfachste Mög-lichkeit, die wir verwenden werden, besteht darin, die in e0 gebundenen Variablen soumzubenennen, dass sie verschieden von den in e1 freien Variablen sind. Umbenen-nungen dieser Art nennt man α-Konversion.

Beispiel 3.2.2 Betrachten Sie erneut den Ausdruck aus Beispiel 3.2.1. Die freie Va-riable x der Funktion fun y → x + y tritt als gebundene Variable in dem Ausdruckauf, in den wir die Funktion substituieren wollen. Umbenennen dieses Vorkommensvon x liefert den Ausdruck:

let x = 17in let f = fun y → x + yin let x′ = 4in f x′

Die Substitution von fun y → x + y für f liefert dann:

let x = 17in let x′ = 4in (fun y → x + y) x′

Page 153: œbersetzerbau: Band 3: Analyse und Transformation

3.2 Einige einfache Optimierungen 143

Die Auswertung dieses Ausdrucks liefert das richtige Ergebnis 21. ��Keine Umbenennung von Variablen ist erforderlich, wenn die freien Variablen desAusdrucks e1 nicht gebunden in e0 vorkommen. Dies ist insbesondere der Fall, wenne1 gar keine freien Variablen besitzt.

Die eben beschriebene Transformation von let-Ausdrücken ist jedoch nur danneine Verbesserung, wenn durch ihre Anwendung keine Berechnung zusätzlich aus-geführt werden muss. Das ist sicherlich in den drei folgenden Spezialfällen der Fall:

• Die Variable x kommt in e0 gar nicht vor. Dann wird die Auswertung von e1durch die Transformation vollständig eingespart.

• Die Variable x kommt genau einmal in e0 vor. Dann wird die Auswertung vone1 durch die Transformation nur an eine andere Stelle verschoben.

• Der Ausdruck e1 ist selbst eine Variable z. Dann werden in e0 nur sämtlicheZugriffe auf die Variable x durch Zugriffe auf die Variable z ersetzt.

Aber Vorsicht! Auch beiα-Konversion erhält die Anwendung der let-Transformationdie Semantik nur, sofern unsere funktionalen Sprache für den let-Ausdruck wie inHASKELL verzögerte Auswertung (lazy evaluation) vorsieht. Bei verzögerter Aus-wertung wird das Argument e1 in der Funktionsanwendung (fun x → e0) e1 erstausgewertet, wenn bei der Auswertung von e0 auf den Wert der Variablen x zuge-griffen wird.

Gierige Auswertung (eager evaluation) des let-Ausdrucks wie in OCAML wirdden Ausdruck e1 dagegen auf jeden Fall auswerten. Falls die Auswertung von e1 nichtterminiert, terminiert die Auswertung des gesamten let-Ausdrucks nicht. Kommt da-gegen x nicht im Hauptausdruck e0 vor oder nur in einem Teilausdruck, der nichtausgewertet wird, könnte die Auswertung des transformierten Ausdrucks terminie-ren, obwohl die Auswertung des ursprünglichen Ausdrucks nicht terminierte.

Beispiel 3.2.3 Betrachten Sie etwa das Programm:

let rec f = fun x → 1 + f xin let y = f 0in 42

Bei verzögerter Auswertung liefert das Programm den Wert 42 zurück. Bei gieri-ger Auswertung dagegen terminiert das Programm nicht, weil vor der Rückgabe desWerts 42 erst die Auswertung von f 0 angestoßen wird. Da die Variable y jedochnicht im Hauptausdruck vorkommt, wäre nach Anwendung der let-Optimierung dieBerechnung von f 0 beseitigt.Folglich würde die Auswertung terminieren und 42zurück liefern. ��Möglicherweise wird man sich an einem verbesserten Terminierungsverhalten nichtstören. Anders sieht es aus, wenn die Auswertung von e1 erwünschte Seiteneffekteproduziert. Das ist zwar in unserer kleinen Kernsprache nicht vorgesehen. OCAML-Ausdrücke können aber sehr wohl Ausnahmen auslösen oder mit ihrer Umgebungwechselwirken – unabhängig davon, ob auf ihren Rückgabewert zugegriffen wird

Page 154: œbersetzerbau: Band 3: Analyse und Transformation

144 3 Optimierung funktionaler Programme

oder nicht. In diesem Fall müssen wir die Anwendbarkeit der Transformation aufAusdrücke e1 einschränken, die selbst mittel- oder unmittelbar keine Seiteneffektehervorrufen. Das ist sicherlich bei Variablen oder Ausdrücken der Fall, die direktWerte wie z.B. Funktionen darstellen.

Weitere Optimierungsschritte werden ermöglicht, wenn let-Definitionen vor dieBerechnung eines Ausdrucks gezogen werden:

((let x = e in e0) e1) = (let x = e in e0 e1),falls x nicht frei in e1 ist.

(let y = e1 in let x = e in e0) = (let x = e in let y = e1 in e0),falls x nicht frei in e1 ist und y nicht frei in e.

(let y = let x = e in e1 in e0) = (let x = e in let y = e1 in e0),falls x nicht frei in e0 ist.

Falls es keine Seiteneffekte gibt, ist die Anwendung dieser Regeln uneingeschränktmöglich. Selbst das Terminierungsverhalten ändert sich nicht. Durch Anwendungdieser Regeln können let-Definitionen weiter nach außen geschoben werden, wel-ches die Anwendbarkeit der Transformation Inlining des nächsten Abschnitts unter-stützt. Weitere Verschiebungen von let-Definitionen diskutieren die Aufgaben 1, 2und 3.

3.3 Inlining

Inlining für funktionale Sprachen soll die mit einem Funktionsaufruf verbundenenKosten einsparen, indem der Rumpf der Funktion an die Aufrufstelle kopiert wird.Dieses ist ganz analog zum Inlining für imperative Sprachen, welches wir in Kapitel2.1 behandelt haben. Bei der imperativen Kernsprache stellten wir uns allerdings vor,dass die Parameterübergabe mit Hilfe von (globalen) Variablen bzw. dem Speicherrealisiert wird. Prozeduren konnten wir deshalb als parameterlos annehmen. Unterdieser Voraussetzung bedeutete Inlining einer Prozedur f, den Aufruf von f durcheine Kopie ihres Rumpfs zu ersetzen.

Bei funktionalen Sprachen können wir es uns nicht ganz so leicht machen. Neh-men wir an, die Funktion f sei definiert durch let f = fun x → e0. Dann wollen wirdie Anwendung f e1 ersetzen durch:

let x = e1 in e0

Beispiel 3.3.1 Betrachten wir das Programmfragment:

let fmax = fun f → fun x → fun y →if x > y then f xelse f y

in let max = fmax (fun z → z)

Page 155: œbersetzerbau: Band 3: Analyse und Transformation

3.3 Inlining 145

Dann können wir die Definition von max vereinfachen zu:

let max = let f = fun z → zin fun x → fun y → if x > y then f x

else f y

Inlining von f liefert:

let max = let f = fun z → zin fun x → fun y → if x > y then let z = x

in zelse let z = y

in z

Die Umbenennung von Variablen und das Beseitigen nicht benötigter Definitionenergibt:

let max = fun x → fun y → if x > y then xelse y

Die Transformation Inlining ergibt sich durch die Kombination eines eingeschränk-ten Falls der let-Optimierung aus dem letzten Abschnitt mit der Optimierung vonFunktionsanwendungen. Die let-Optimierung wird nur auf let-Ausdrücke der Form:let f = fun x → e0 in e angewendet. Der funktionale Wert fun x → e0 wird nuran solche Vorkommen in e kopiert, an denen f auf ein Argument angewendet wird.Anschließend wird an diesen Stellen die Optimierung für Funktionsanwendungendurchgeführt. Wie bei den bisherigen Optimierungen müssen wir auch beim Inliningbezüglich der Korrektheit und der Terminierung aufpassen!

Eine α-Konversion des Ausdrucks e vor Anwendung von Inlining stellt sicher,dass keine freie Variable in e0 durch die Transformation gebunden wird.

Wie bei imperativen Programmen beschränken wir uns bei der Anwendung vonInlining auf nichtrekursive Funktionen, d.h. für unsere Kernsprache auf let-definierteFunktionen. Bei funktionalen Programmiersprachen reicht dies jedoch nicht aus, umdie Terminierung der Transformation zu garantieren.

Beispiel 3.3.2 Betrachten Sie das Programmfragment:

let w = fun f → fun y → f(y f y)in let fix = fun f → w f w

Sowohl w wie fix sind nicht rekursiv, und wir können Inlining auf den Rumpf w f wder Funktion fix anwenden. Mit der Definition von w liefert das für fix die Funktion:

fun f → let f = f in let y = w in f(y f y) ,

was sich vereinfachen lässt zu:

Page 156: œbersetzerbau: Band 3: Analyse und Transformation

146 3 Optimierung funktionaler Programme

fun f → f(w f w) .

Darauf kann erneut Inlining angewendet werden. Nach k Wiederholungen ergibtsich:

fun f → fk(w f w) ,

und Inlining kann erneut angewendet werden. ��Nichtterminierung wie in unserem Beispiel kann in ungetypten Programmierspra-chen wie LISP vorkommen. In getypten Programmiersprachen wie OCAML oderHASKELL wird die Funktion w als nicht typisierbar zurück gewiesen. In diesen Spra-chen terminiert Inlining, so wie wir es definiert haben, immer und liefert (bis auf dieNamen gebundener Variablen) eine eindeutige Normalform. Aber selbst in ungety-pten funktionalen Progammiersprachen lässt sich das Problem möglicher Nichtter-minierung zumindest pragmatisch leicht lösen: wenn Inlining zu lange dauert, brichtman die Transformation einfach ab!

3.4 Spezialisierung rekursiver Funktionen

Inlining ist zuerst einmal eine Technik, um Aufrufe nichtrekursiver Funktionen zuoptimieren. Was können wir tun, um die Effizienz rekursiver Funktionen zu erhöhen?Eine gängige Technik funktionaler Programmierung verwendet rekursive polymor-phe Funktionen höherer Ordnung wie die Funktion map. Solche Funktionen reali-sieren die algorithmische Essenz einer Problemstellung und werden auf die konkreteAnwendung mit Hilfe geeigneter (eventuell funktionaler) Argumente angepasst.

Beispiel 3.4.1 Betrachten Sie das folgende Programmfragment:

let f = fun x → x · xin let rec map = fun g → fun y → match y

with [ ] → [ ]| x1 :: z → g x1 :: map g z

in map f list

Der aktuelle Parameter der Funktionsanwendung map f ist die Funktion fun x →x · x. Die Funktionsanwendung map f list repräsentiert also eine Funktion, die alleElemente der Liste list quadriert. Beachten Sie, dass wir hier wie üblich den Listen-konstruktor :: infix zwischen seine beiden Argumente geschrieben haben. ��Sei f eine rekursive Funktion und f v eine Funktionsanwendung auf einen Ausdruckv, der einen Wert, d.h. entweder eine andere Funktion oder eine Konstante repräsen-tiert. Unser Ziel ist, für den Ausdruck f v eine neue Funktion h einzuführen. DieseOptimierung nennt man auch Funktionsspezialisierung.

Sei f definiert durch let rec f = fun x → e. Dann definieren wir h durch:

let h = let x = v in e

Page 157: œbersetzerbau: Band 3: Analyse und Transformation

3.4 Spezialisierung rekursiver Funktionen 147

wobei wir anschließend die gebundenen Variablen dieser Definition durch neue Va-riablen ersetzen.

Beispiel 3.4.2 Betrachten wir das Programmfragment aus Beispiel 3.4.1.

let h = let g = fun x → x · xin fun y → match y

with [ ] → [ ]| x1 :: z → g x1:: map g z

Weil die Funktion map rekursiv ist, gibt es im Rumpf der Funktion h einen wei-teren Aufruf von map. Spezialisierung der Funktion map für diesen Aufruf würdeeine Funktion h1 einführen mit der gleichen Definition (bis auf Umbenennung ge-bundener Variablen) wie h. Statt eine weitere Funktion h1 einzuführen, ersetzt manumgekehrt den Aufruf map g durch die Funktion h. Dieses Ersetzen der rechtenSeite einer Definition durch ihre linke Seite nennt man auch Funktions-Faltung.

Im Beispiel erhalten wir damit:

let rec h = let g = fun x → x · xin fun y → match y

with [ ] → [ ]| x1 :: z → g x1 :: h z

Die Definition von h enthält keinen expliziten Aufruf der Funktion map mehr und istnun selbst rekursiv. Das Inlining der Funktion g ergibt dann:

let rec h = let g = fun x → x · xin fun y → match y

with [ ] → [ ]| x1 :: z → ( let x = x1

in x · x ) :: h z

Die Beseitigung überflüssiger Definitionen und Variablen-Variablen-Bindungen lie-fert schließlich:

let rec h = fun y → match ywith [ ] → [ ]| x1 :: z → x1 · x1 :: h z

��Im Allgemeinen können wir nicht davon ausgehen, dass die rekursiven Aufrufe derFunktion, die wir spezialisieren wollen, sich sofort zu einem Aufruf der Spezialisie-rung zurück falten lassen, schlimmer noch: es kann passieren, dass die fortgesetzteSpezialisierung zu einer unendlichen Menge von Hilfsfunktionen führt und damitselbst nicht terminiert. Hier können wir uns aber erneut auf einen pragmatischenStandpunkt stellen und mit der Einführung neuer Funktionen zu irgend einem Zeit-punkt aufhören, der uns angemessen erscheint.

Page 158: œbersetzerbau: Band 3: Analyse und Transformation

148 3 Optimierung funktionaler Programme

3.5 Eine verbesserte Wertanalyse

Inlining und Funktionsspezialisierung optimieren Funktionsanwendungen f e. Fürdie Funktion f gingen wir davon aus, dass sie in einem umfassenden let- bzw. letrec-Ausdruck definiert wurde. In funktionalen Sprachen können Funktionen jedoch auchals Parameter übergeben oder als Ergebnisse zurück geliefert werden. Die Anwend-barkeit von Inlining und Funktionsspezialisierung kann deshalb durch eine Analyseerhöht werden, die für jede Variable eine Obermenge ihrer möglichen Laufzeitwerteberechnet. Dies ist das Ziel der nächsten Analyse.

Innerhalb der Menge der in einem Programm auftretenden Ausdrücke identifi-zieren wir die Teilmenge E aller Ausdrücke, deren Werte ermittelt werden sollen.Diese Menge E besteht aus allen Teilausdrücken des Programms, die eine Variable,eine Funktionsanwendung, ein let-, letrec-, match- oder if-Ausdruck sind.

Beispiel 3.5.1 Betrachten Sie das folgende Programm:

let rec from = fun i → i :: from (i + 1)and first = fun l → match l with x :: xs → xin first (from 2)

Die Menge E besteht dann aus den Ausdrücken:

E = {from, i, from (i + 1), first, l, x, from 2, first (from 2),match l with . . . , let rec from = . . .}

��Sei V die Menge der übrigen Teilausdrücke des Programms. Die Ausdrücke aus Vstellen somit entweder Werte dar wie Funktionsabstraktionen oder Konstanten odersie liefern zumindest den obersten Konstruktor eines Werts bzw. die äußerste Opera-toranwendung. Im Programm aus Beispiel 3.5.1 ist die Menge V gegeben durch:

V = {fun i → . . . , i :: from (i + 1), i + 1, fun l → . . . , 2}Jeder Teilausdruck e in V lässt sich eindeutig in einen oberen Teil zerlegen, in demnur Konstruktoren, Werte oder Operatoren vorkommen, und in die darunter liegen-den maximalen Teilausdrücke e1, . . . , ek aus der Menge E. Den oberen Teil repräsen-tieren wir durch ein k-stelliges Muster, d.h. einen Term, in dem an den Blättern dieMustervariablen •1, . . . , •k für die Ausdrücke e1, . . . , ek vorkommen. Der Ausdrucke hat dann die Form: e ≡ t[e1/•1, . . . , ek/•k] oder kurz: e ≡ t[e1, . . . , ek].

In unserem Beispiel lässt sich etwa der Ausdruck e ≡ (i :: from (i + 1)) zerlegenin e ≡ t[i, from (i + 1)] für Ausdrücke ifrom (i + 1) aus E und das Muster t ≡(•1 :: •2).

Unser Ziel besteht darin, für jeden Ausdruck e ∈ E eine Teilmenge von Aus-drücken aus V zu ermitteln, zu welchen sich e möglicherweise entwickelt. Bevor wirein Verfahren zur Berechnung solcher Teilmengen aus V angeben, wollen wir kurz

Page 159: œbersetzerbau: Band 3: Analyse und Transformation

3.5 Eine verbesserte Wertanalyse 149

festhalten, in welchem Sinn eine Relation G ⊆ E×V für jeden Ausdruck aus E eineMenge von Wertausdrücken definiert.

Ein Wertausdruck ist ein Ausdruck v, der gemäß folgender Grammatik gebildetist:

v ::= b | fun x → e | c v1 . . . vk | (v1, . . . , vk) | �1 v | v1�2v2

für Basiswerte b, beliebige Ausdrücke e, Konstruktoren c und ein- bzw. zweistelligeOperatoren �1, �2, die Basiswerte zurück liefern.

Sei G ⊆ E × V eine Relation zwischen Ausdrücken aus E und V. Den Aus-drücken e ∈ E ordnen wir die Menge [[e]]�G aller Wertausdrücke zu, welche für e mitHilfe von G hergeleitet werden können. Jedes Paar (e, t[e1, . . . , ek]) ∈ G für Aus-drücke e, e1, . . . , ek ∈ E und Muster t kann aufgefasst werden als die Ungleichung:

[[e]]�G ⊇ t[[[e1]]�G , . . . , [[ek]]�G]

Dabei interpretieren wir die Anwendung eines Musters t auf Mengen V1, . . . , Vk alsdie Menge:

t[V1, . . . , Vk] = {t[v1, . . . , vk] | vi ∈ Vi}Die Mengen [[e]]�G, e ∈ E, definieren wir dann als die kleinste Lösung dieses Systemsvon Ungleichungen.

Beispiel 3.5.2 Sei G die Relation

G = {(i, 2), (i, i + 1)} .

Dann besteht die Menge [[i]]�G aus allen Ausdrücken der Form 2 oder (. . . (2 +1) . . .) + 1. Beachten Sie, dass wir im Zusammenhang mit der Analyse Operatoran-wendungen nicht ausrechnen, sondern wie Datenkonstruktoren behandeln. Für

G′ = {(from (i + 1), i :: from (i + 1))}ist die Menge [[from (i + 1)]]�G′ dagegen leer. ��Eine Relation G kann als eine reguläre Baumgrammatik aufgefasst werden mitNichtterminalen E und Konstanten bzw. Funktionsabstraktionen als 0-stelligen Ter-minalsymbolen sowie Operatoren und Konstruktoren als mehrstelligen Terminal-symbolen. Für einen Ausdruck e ∈ E (d.h. ein Nichtterminal) bezeichnet die Menge[[e]]�G dann die Menge der aus e bzgl. der Grammatik ableitbaren terminalen Aus-

drücke (siehe Aufg. 4 und 5). Die Mengen [[e]]�G sind im Allgemeinen unendlich.Die Relation G stellt jedoch eine endliche Repräsentation dieser Mengen dar, diees erlaubt, einfache Eigenschaften der Mengen [[e]]�G zu entscheiden. Die wichtigste

Frage ist, ob [[e]]�G einen bestimmten Term v enthält oder nicht. Diese Frage ist be-sonders leicht zu beantworten, wenn v eine Funktionsabstraktion fun x → e′ ist. Dabei jedem Paar (e, u) aus G die rechte Seite u entweder eine Konstante, eine Funk-tion oder die Anwendung eines Konstruktors oder eines Operators ist, folgt dass(fun x → e′) ∈ [[e]]�G genau dann gilt, wenn (e, fun x → e′) ∈ G.

Weitere Beispiele für leicht entscheidbare Eigenschaften sind:

Page 160: œbersetzerbau: Band 3: Analyse und Transformation

150 3 Optimierung funktionaler Programme

• Ist [[e]]�G nicht-leer?

• Ist [[e]]�G endlich, und wenn ja, aus welchen Elementen besteht diese Menge?

Ziel unserer Wertanalyse ist es, für ein Programm eine Relation G zu konstruieren,so dass für jeden Ausdruck e des Programms [[e]]�G alle Werte enthält, zu denen siche zur Laufzeit (relativ zu den sich zur Laufzeit ergebenden Bindungen für die in efreien Variablen) möglicherweise auswertet. Die Relation G ⊆ E×V definieren wirmit Hilfe von Axiomen und Schlussregeln. Zunächst erweitern wir die Relation G,indem wir alle Paare (v, v) für v ∈ V zu G hinzu fügen. Diese Relation bezeichnenwir mit ⇒ und schreiben dieses Relationssymbol zur besseren Lesbarkeit als infix-Symbol.

Axiome stellen diejenigen Beziehungen bereit, die ohne Voraussetzung gelten:

v ⇒ v (v ∈ V)

d.h. Ausdrücke aus der Menge V stehen zu sich selbst in Beziehung. Für jedes Pro-grammkonstrukt werden nun Regeln aufgestellt:

Funktionsanwendung. Sei e ≡ (e1 e2). Dann haben wir die Regeln:

e1 ⇒ fun x → e0 e0 ⇒ ve ⇒ v

e1 ⇒ fun x → e e2 ⇒ vx ⇒ v

Wertet sich der Funktionsausdruck einer Funktionsanwendung zu einer Funkti-on fun x → e0 aus und der Hauptausdruck e0 der Funktion zu einem Wert v,dann könnte die Funktionsanwendung selbst sich zu v entwickeln. Wertet sichandererseits das Argument der Funktionsanwendung zu einem Wert v aus, dannist v auch ein möglicher Wert des formalen Parameters x der Funktion.

let-Definition. Sei e ≡ let x1 = e1 in e0. Dann haben wir die Regeln:

e0 ⇒ ve ⇒ v

e1 ⇒ vx ⇒ v

Wertet sich der Hauptausdruck e0 eines let-Ausdrucks zu einem Wert v aus, dannauch der gesamte let-Ausdruck. Jeder Wert für den Ausdruck e1 stellt anderer-seits einen möglichen Wert für die lokale Variable x dar. Eine ähnliche Überle-gung rechtfertigt auch die Regeln für letrec-Ausdrücke.

letrec-Definition. Für e ≡ let rec x1 = e1 . . . and xk = ek in e0. haben wir:

e0 ⇒ ve ⇒ v

ei ⇒ vxi ⇒ v

Fallunterscheidungen. Sei e ≡ match e0 with p1 → e1 | . . . | pm → em.Falls pi ein Basiswert ist, haben wir die Regeln:

Page 161: œbersetzerbau: Band 3: Analyse und Transformation

3.5 Eine verbesserte Wertanalyse 151

ei ⇒ ve ⇒ v

Ist andererseits pi ≡ c y1 . . . yk, haben wir:

e0 ⇒ c e′1 . . . e′k ei ⇒ ve ⇒ v

e0 ⇒ c e′1 . . . e′k e′j ⇒ v

yj ⇒ v( j = 1, . . . , k)

Ist schließlich pi eine Variable y, haben wir:

ei ⇒ ve ⇒ v

e0 ⇒ vy ⇒ v

Wertet sich eine Alternative zu einem Wert aus, kann sich die gesamte Fall-unterscheidung zu diesem Wert auswerten — sofern das zugehörige Muster beider Auswertung des Ausdrucks e0 nicht ausgeschlossen werden kann. Da wirdie genauen Werte von Operatoranwendungen nicht verfolgen, nehmen wir beiBasiswerten stets an, dass sie möglich sind. Anders sieht es bei einem Musterc y1 . . . yk. Dieses Muster passt auf den Wert von e0 nur, wenn e0 ⇒ v gilt,wobei c der oberste Konsttruktor von v ist. In diesem Fall hat v die Form v =c e′1 . . . e′k, und die Werte für e′i sind mögliche Werte für die Variable xi.

Bedingte Ausdrücke. Sei e ≡ if e0 then e1 else e2. Für i = 1, 2 haben wir:

ei ⇒ ve ⇒ v

Diese Regeln sind analog zu den Regeln für match-Ausdrücke, bei denen Basis-werte als Muster verwendet werden.

Beispiel 3.5.3 Betrachten wir erneut das Programm:

let rec from = fun i → i :: from (i + 1)and first = fun l → match l with x :: xs → xin first (from 2)

Dieses Programm terminiert nur mit verzögerter Auswertung wie in HASKELL. InOCAML dagegen mit gieriger Auswertung würde der Aufruf from 2 nicht terminie-ren und damit auch das ganze Programm nicht. Eine Beispielableitung der Beziehungx ⇒ 2 sieht so aus:

first ⇒ fun l → . . .from ⇒ fun i → i :: from (i + 1)

from 2 ⇒ i :: from (i + 1)l ⇒ i :: from (i + 1)

from ⇒ fun i → i :: from (i + 1)i ⇒ 2

x ⇒ 2

Dabei haben wir Vorkommen von Axiomen v ⇒ v weggelassen. Für e ∈ E seiG(e) die Menge aller Ausdrücke v ∈ V, für die e ⇒ v abgeleitet werden kann. Fürdie Variablen und Funktionsanwendungen dieses Programms liefert die Analyse:

Page 162: œbersetzerbau: Band 3: Analyse und Transformation

152 3 Optimierung funktionaler Programme

G(from) = {fun i → i :: from (i + 1)}G(from (i + 1)) = {i :: from (i + 1)}G(from 2) = {i :: from (i + 1)}G(i) = {2, i + 1}G(first) = {fun l → match l . . .}G(l) = {i :: from (i + 1)}G(x) = {2, i + 1}G(xs) = {i :: from (i + 1)}G(first (from 2)) = {2, i + 1}

Wir schließen, dass die Auswertung der Ausdrücke from 2 und from (i + 1) nie-mals einen endlichen Wert liefert. Die Variable i andererseits wird möglicherweisean Ausdrücke gebunden mit Werten 2, 2 + 1, 2 + 1 + 1, . . .. Gemäß dieser Analyseliefert damit der Hauptausdruck einen der Werte 2, 2 + 1, 2 + 1 + 1, . . .. ��Die Mengen G(e) können durch Fixpunktiteration berechnet werden. Eine etwasgeschicktere Implementierung rechnet nicht mit Mengen von Ausdrücken, sondernpropagiert Ausdrücke v ∈ V einzeln. Wird v einer Menge G(e) hinzugefügt, sindmöglicherweise die Voraussetzungen weiterer Regeln erfüllt, welche wiederum zuneuen Hinzufügungen von Ausdrücken v′ zu Mengen G(e′) führen können. Das istdie Idee des kubischen Algorithmus von Heintze [Hei94].

Die Korrektheit dieser Analyse kann mit Hilfe einer operationellen Semantikfür Programme mit verzögerter Auswertung gezeigt werden. Wir werden hier die-sen Beweis nicht führen, weisen aber darauf hin, dass die Regeln zur Ableitung derBeziehungen e ⇒ v ganz analog sind zu entsprechenden Regeln einer solchenSemantik – mit den folgenden wesentlichen Unterschieden:

• Operatoren �1, �2 auf Basistypen werden nicht weiter ausgewertet;• An Verzweigungen, die von Basiswerten abhängen, werden nichtdeterministisch

alle Möglichkeiten verfolgt;• Bei Fallunterscheidungen bleibt die Reihenfolge der Muster unberücksichtigt.• Die Berechnung der Rückgabewerte einer Funktion wird von der Bestimmung

der möglichen aktuellen Parameter dieser Funktion entkoppelt.

Die beschriebene Analyse ist ebenfalls korrekt für Programme einer Programmier-sprache mit gieriger Ausdrucksauswertung. Für diese können wir die Genauigkeitder Analyse erhöhen, indem wir bei der Anwendung der Regeln zusätzliche Vorbe-dingungen verlangen:

• Bei Funktionsaufrufen mit Argument e2 sollte die Menge [[e2]]�G nicht leer sein;• Bei let- und letrec-Ausdrücken sollte für die rechten Seiten ei der lokal einge-

führten Variablen jeweils [[ei]]�G nicht leer sein;

• Bei bedingten Ausdrücken sollte jeweils die Menge [[e0]]�G für die Bedingung e0nicht leer sein;

Page 163: œbersetzerbau: Band 3: Analyse und Transformation

3.6 Beseitigung von Zwischendatenstrukturen 153

• Analog sollte bei Fallunterscheidungen match e0 . . . die Menge [[e0]]�G nicht leersein. Und bei den Regeln für Muster c y1 . . . yk sollten für den Wert c e′1 . . . e′k in

[[e0]]�G auch die Mengen [[e′j]]�G für j = 1, . . . , k nicht leer sein.

Im Beispiel hätte das zur Folge, dass

[[l]]�G = [[x]]�G = [[xs]]�G = [[match l . . .]]�G = [[first (from 2)]]�G = ∅Die Analyse findet heraus, dass die gierige Auswertung des Aufrufs first (from 2)nicht terminiert.

Die hier vorgestellte Wertanalyse liefert erstaunlich genaue Ergebnisse. Sie lässtsich erweitern auf eine Analyse der möglicherweise bei der Auswertung eines Aus-drucks geworfenen Ausnahmen (Aufg. 7) bzw. der Menge der während der Auswer-tung aufgetretenen Seiteneffekte (Aufg. 8). Ungenauigkeiten treten allerdings auf,da die Analyse etwa bei Funktionen die Approximation der möglichen Werte der Pa-rameter von der Berechnung der Rückgabewerte entkoppelt. Bei der Analyse poly-morpher Funktionen bedeutet das, dass die Argumentwerte unterschiedlicher Typenvermengt werden.

3.6 Beseitigung von Zwischendatenstrukturen

Eine der wichtigsten Datenstrukturen, die von funktionalen Programmiersprachenunterstützt werden, sind Listen. Funktionale Programme sammeln Zwischenergeb-nisse in Listen, wenden Funktionen auf alle Elemente von Listen an und berechnenaus Listen das Ergebnis. Gängige Bibliotheken stellen deshalb höhere Funktionenauf Listen zur Verfügung, die diesen Programmierstil unterstützen. Beispiele für sol-che höheren Funktionen sind:

map = fun f → fun l → match lwith [ ] → [ ]| h :: t → f x :: map f t

filter = fun p → fun l → match lwith [ ] → [ ]| h :: t → if p h then h :: filter p t

else filter p t)

fold_left = fun f → fun a → fun l → match l with [ ] → a| h :: t → fold_left f (f a h) t)

Funktionen lassen sich mit Hilfe der Funktionskomposition verknüpfen:

comp = fun f → fun g → fun x → f (g x)

Wie mit diesen einfachen Hilfsmitteln komplexere Funktionalität realisiert werdenkann, zeigt das nächste Beispiel.

Page 164: œbersetzerbau: Band 3: Analyse und Transformation

154 3 Optimierung funktionaler Programme

Beispiel 3.6.1 Das folgende Programmfragment stellt Funktionen zur Berechnungder Summe der Elemente einer Liste, zur Berechnung der Länge einer Liste und zurBerechnung der Standardabweichung der Elemente einer Liste zur Verfügung:

let sum = fold_left (+) 0in let length = comp sum (map (fun x → 1))in let der = fun l → let s1 = sum l

in let n = length lin let mean = s1/nin let s2 = sum (

map (fun x → x · x) (map (fun x → x − mean) l))

in s2/n

Dabei steht (+) für die Funktion fun x → fun y → x + y. In den angegebenenDefinitionen kommt Rekursion nicht mehr explizit vor, sondern ist nur noch implizitin den Funktionen map und fold_left enthalten. Die Definition von length kommtsogar ohne explizite Funktionsabstraktion fun . . . → aus. Einerseits wird die Imple-mentierung so übersichtlicher, andererseits führt dieser Programmierstil aber dazu,dass für Zwischenergebnisse Hilfsdatenstrukturen angelegt werden, die vermiedenwerden können. Die Funktion length könnte direkt implementiert werden durch:

let length = fold_left (fun a → fun y → a + 1) 0

Diese Implementierung vermeidet das Anlegen der Hilfsliste, die für jedes Elementder Eingabe eine 1 enthält. ��Die folgenden Regeln erlauben es, einige offensichtlich überflüssige Hilfsdatenstruk-turen zu beseitigen:

comp (map f) (map g) = map (comp f g)comp (fold_left f a) (map g) = fold_left (fun a → comp (f a) g) acomp (filter p1) (filter p2) = filter (fun x → if p2 x then p1 x

else false)comp (fold_left f a) (filter p) = fold_left (fun a → fun x → if p x then f a x

else a) a

Während die Auswertung der linken Seite jeweils eine Hilfsdatenstruktur benötigt,kommt die Auswertung der rechten Seite ohne diese Hilfsdatenstrukur aus! DieseRegeln erlauben die Optimierung der Funktion length aus Beispiel 3.6.1. Linke undrechte Seite der Regeln sind jedoch nicht unter allen Umständen äquivalent! Viel-mehr dürfen diese Regeln nur dann angewendet werden, wenn die dabei auftretendenFunktionen f , g, p1, p2 keine Seiteneffekte haben.

Page 165: œbersetzerbau: Band 3: Analyse und Transformation

3.6 Beseitigung von Zwischendatenstrukturen 155

Ein weiteres Problem dieser Optimierung besteht darin zu erkennen, wann sie an-gewendet werden kann. Oft wird ein Programmierer nicht einen expliziten Operatorverwenden, um Funktionen hintereinander auszuführen, sondern direkt geschachtelteFunktionsanwendungen hinschreiben. Dieser Fall liegt bei der Definition der Funk-tion der aus Beispiel 3.6.1 vor. Für diesen Fall können wir aber die Ersetzungsregelnauch schreiben als:

map f (map g l) = map (fun z → f (g z)) lfold_left f a (map g l) = fold_left (fun a → fun z → f a (g z)) a lfilter p1 (filter p2 l) = filter (fun x → if p2 x then p1 x

else false) lfold_left f a (filter p l) = fold_left (fun a → fun x → if p x then f a x

else a) a l

Beispiel 3.6.2 Anwendung dieser Regeln auf die Definition der Funktion der ausBeispiel 3.6.1 liefert:

let sum = fold_left (+) 0in let length = fold_left (fun a → fun z → a + 1) 0in let der = fun l → let s1 = sum l

in let n = length lin let mean = s1/nin let s2 = fold_left (fun a → fun z →

(+) a ((fun x → x · x) ((fun x → x − mean) z))) 0 l

in s2/n

Wiederholte Anwendung der Optimierung von Funktionsanwendungen sowie derlet-Optimierung liefert dafür:

let sum = fold_left (+) 0in let length = fold_left (fun a → fun z → a + 1) 0in let der = fun l → let s1 = sum l

in let n = length lin let mean = s1/nin let s2 = fold_left (fun a → fun z →

let x = z − meanin let y = x · xin a + y) 0 l

in s2/n

Page 166: œbersetzerbau: Band 3: Analyse und Transformation

156 3 Optimierung funktionaler Programme

Alle Zwischendatenstukturen sind verschwunden. Nur Aufrufe der Funktion fold_leftsind übrig geblieben. Weil die Funktion fold_left sogar endrekursiv ist, wird für dieseAufrufe Code erzeugt, der so effizient ist wie Schleifen in imperativen Programmen.��Gelegentlich wird eine erste Liste von Zwischenergebnissen durch Tabellierung ei-ner Funktion erstellt. Tabellierung von n Werten einer Funktion f : int → τ lieferteine Liste:

[f 0; . . . ; f (n − 1)]

Eine Funktion tabulate, um diese Liste zu berechnen, könnte in OCAML so definiertwerden:

let tabulate = fun n → fun f →let rec tab = fun j → if j ≥ n then [ ]

else (f j) :: tab ( j + 1)in tab 0

Unter der Voraussetzung, dass alle vorkommenden Funktionsaufrufe terminieren undkeine Seiteneffekte haben, gilt dann:

map f (tabulate n g) = tabulate n (comp f g)= tabulate n (fun j → f (g j))

fold_left f a (tabulate n g) = loop n (fun a → comp (f a) g) a= loop n (fun a → fun j → (f a (g j)) a

Dabei ist:

let loop = fun n → fun f → fun a →let rec doit = fun a → fun j → if j ≥ n then a

else doit (f a j) ( j + 1)in doit a 0

Die endrekursive Funktion loop entspricht einer for-Schleife: die lokalen Daten ste-hen in ihrem akkumulierenden Parameter a, während ihr funktionaler Parameter ffestlegt, wie der neue Wert für a nach dem j-ten Durchlauf aus a vor dem Durchlaufund j berechnet wird. Die Funktion loop berechnet dabei ihr Ergebnis ganz ohneListe als Hilfsdatenstruktur.

Die Anwendbarkeit der Regeln hängt wesentlich davon ab, dass Kompositio-nen der Funktionen fold_left f a, map f, filter p erkannt werden. Diese Strukturkann in einem konkreten Programm aber eventuell nur sehr mittelbar vorkommen:Teilausdrücke können in let-Definitionen ausgelagert sein oder gelangen über Para-meter an die entsprechende Stelle. Hier können erfolgreich die let-Optimierungenaus Abschnitt 3.2 sowie in komplexeren Situationen die Wertanalyse aus Kapitel 3.5eingesetzt werden.

Das Prinzip der hier vorgestellten Optimierungen kann in verschiedene Richtun-gen verallgemeinert werden:

Page 167: œbersetzerbau: Band 3: Analyse und Transformation

3.7 Verbesserung der Auswertungsreihenfolge: Die Striktheitsanalyse 157

• Neben den betrachteten Funktionen können weitere gängige Listenfunktionenin Betracht gezogen werden wie z.B. die Funktion rev, welche die Anordnungeiner Liste umdreht, die endrekursive Version rev_map der Funktion map unddie Funktion fold_right (siehe Aufg. 10).

• Die Unterdrückung von Zwischendatenstrukturen bietet sich auch für index-abhängige Versionen der Funktionen map und fold_left an (siehe Aufg. 11).Bezeichne l die Liste [x0; . . . ; xn−1] vom Typ: ′b list.Die index-abhängige Version von map erhält als Argument eine Funktion f vomTyp: int →′ b →′ c und liefert für l die Liste:

[f 0 x0; . . . ; f (n − 1) xn−1]

Entsprechend erhält die index-abhängige Version von fold_left als Argumenteeine Funktion f vom Typ: int →′ a →′ b →′ a, einen Anfangswert a vom Typ:′a und berechnet den Wert:

f (n − 1) (. . . f 1 (f 0 a x0) x1 . . .) xn−1

• Die Funktionen map und fold_left lassen sich ganz allgemein für benutzerdefi-nierte funktionale Datentypen definieren, wenn sie auch dort i.A. weniger ver-breitet sind. Zumindest prinzipiell steht damit der Anwendung analoger Opti-mierungen, wie wir sie für Listen angegeben haben, in diesem verallgemeinertenKontext nichts im Wege (siehe Aufg. 12).

3.7 Verbesserung der Auswertungsreihenfolge: DieStriktheitsanalyse

Programmiersprachen wie HASKELL verzögern die Auswertung von Ausdrückenso lange, bis ihre Auswertung zwingend erforderlich ist. Deshalb werten sie let-definierte Variablen wie die aktuellen Parameter einer Funktion erst aus, wenn aufihren Wert zugegriffen wird. Eine so verzögerte Auswertung gestattet die eleganteBehandlung (potentiell) unendlicher Listen, von denen aber in jeder Anwendung nurein Teil zur Berechnung des Ergebnisses benötigt wird. Die Verzögerung der Aus-wertung eines Ausdrucks e verursacht aber zusätzliche Kosten, da für die spätereAuswertung von e ein Abschluss angelegt werden muss.

Beispiel 3.7.1 Betrachten Sie das folgende Programm:

let rec from = fun n → n :: from (n + 1)and take = fun k → fun s → if k ≤ 0 then [ ]

else match s with [ ] → [ ]| h :: t → h :: take (k − 1) t

Verzögerte Auswertung des Ausdrucks take 5 (from 0) liefert die Liste [0; 1; 2; 3; 4],während gierige Auswertung mit CBV-Parameterübergabe nicht terminiert. ��

Page 168: œbersetzerbau: Band 3: Analyse und Transformation

158 3 Optimierung funktionaler Programme

Verzögerte Auswertung hat aber auch Nachteile. Selbst endrekursive Funktionen ha-ben unter Umständen keinen konstanten Platzverbrauch mehr.

Beispiel 3.7.2 Betrachten Sie das folgende Programmfragment:

let rec fac2 = fun x → fun a → if x ≤ 0 then aelse fac2 (x − 1) (a · x)

Verzögerte Auswertung wird für die Multiplikationen im akkumulierenden Parame-ter jeweils eigene Abschlüsse anlegen. Erst wenn der rekursive Abstieg bei dem Auf-ruf fac2 x 1 ankommt, wird die geschachtelte Folge von Abschlüssen ausgewertet.Die Multiplikation jeweils sofort auszuführen wäre da erheblich effizienter. ��Statt eine Berechnung zu verzögern, ist es wie in dem Beispiel oft billiger, dieseBerechnung sofort auszuführen und damit das Anlegen eines Abschlusses zu ver-meiden. Das ist das Ziel der folgenden Optimierung.

Zur Vereinfachung betrachten wir zuerst einmal nur Programme ohne zusam-mengesetzte Datenstrukturen und ohne höhere Funktionen. Zusätzlich nehmen wiran, alle Funktionen wären auf der obersten Programmebene definiert. Für die Trans-formation führen wir ein Konstrukt:

let# x = e1 in e0

ein, das die Auswertung von e1 erzwingt, wann immer der Wert von e0 benötigtwird. Ziel der Optimierung ist es, so viele let-Ausdrücke wie möglich durch let#-Ausdrücke zu ersetzen – ohne jedoch das Terminierungsverhalten des Programms zuändern. Striktheitsanalyse liefert uns die dazu erforderliche Information über das Ter-minierungsverhalten von Ausdrücken. Eine k-stellige Funktion f nennen wir strikt inihrem j-ten Argument, 1 ≤ j ≤ k, falls die Auswertung eines Ausdrucks f e1 . . . ekimmer dann nicht terminiert, wenn die Auswertung von e j nicht terminiert. Ist dieFunktion strikt in dem Argument j, können wir die Berechnung des j-ten Argumentse j vorziehen, ohne das Terminierungsverhalten zu ändern. Wir können also f e1 . . . ekersetzen durch:

let# x = e j in f e1 . . . e j−1 x ej+1 . . . ek

Analog können wir einen let-Ausdruck let x = e1 in e0 durch den Ausdruck:

let# x = e1 in e0

ersetzen, sofern die Auswertung von e0 immer dann nicht terminiert, wenn die Be-rechnung des Werts für x in e0 nicht terminiert.

Die einfachste Form der Striktheitsanalyse unterscheidet nur, ob die Auswertungeines Ausdrucks definitiv nicht terminiert oder möglicherweise einen Wert liefert. Sei2 der endliche Verband, der aus den Elementen 0 und 1 besteht mit 0 < 1. Den Wert0 wollen wir einem Ausdruck zuordnen, dessen Auswertung definitiv nicht termi-niert, während der Wert 1 mögliche Terminierung bedeutet. Eine k-stellige Funktionf beschreiben wir dann durch eine k-stellige abstrakte Funktion:

Page 169: œbersetzerbau: Band 3: Analyse und Transformation

3.7 Verbesserung der Auswertungsreihenfolge: Die Striktheitsanalyse 159

[[f]]� : 2 → . . . → 2 → 2

Aus [[f]]� 1 . . . 1 0 1 . . . 1 = 0 (0 im j-ten Argument) können wir folgern, dass einFunktionsaufruf von f mit Sicherheit nicht terminiert, wenn die Auswertung des j-ten Arguments nicht terminiert. Die Funktion f ist folglich strikt im j-ten Argument.

Um für alle Funktionen f des Programms zugehörige abstrakte Beschreibungenf� zu berechnen, stellen wir ein Gleichungssystem auf. Dazu benötigen wir als Hilfs-funktion die abstrakte Auswertung von Ausdrücken e relativ zu einer Variablenbe-legung ρ für die freien Variablen von einem Basistyp und einer Zuordnung φ vonFunktionen zu ihren (aktuellen) abstrakten Beschreibungen:

[[b]]� ρ φ = 1[[x]]� ρ φ = ρ x[[�1 e]]� ρ φ = [[e]]� ρ φ

[[e1 �2 e2]]� ρ φ = [[e1]]� ρ φ ∧ [[e2]]� ρ φ

[[if e0 then e1 else e2]]� ρ φ = [[e0]]� ρ φ ∧ ([[e1]]� ρ φ ∨ [[e2]]� ρ φ)[[f e1 . . . ek]]� ρ φ = φ(f) ([[e1]]� ρ φ) . . . ([[ek]]� ρ φ)[[let x1 = e1 in e]]� ρ φ = [[e]]� (ρ ⊕ {x1 → [[e1]]� ρ}) φ

[[let# x1 = e1 in e]]� ρ φ = ([[e1]]� ρ φ) ∧ ([[e]]� (ρ ⊕ {x1 → 1}) φ)

Die Auswertungsfunktion [[.]]� interpretiert Konstanten durch den abstrakten Wert1, während die Werte für Variablen in ρ nachgeschlagen werden. Einstellige Ope-ratoren �1 werden durch die Identität approximiert, da die Auswertung einer An-wendung eines solchen Operators nicht terminiert, wann immer das Argument desOperators nicht terminiert. Entsprechend werden binäre Operatoren durch Konjunk-tion interpretiert. Die abstrakte Auswertung eines if-Ausdrucks ist gegeben durchb0 ∧ (b1 ∨ b2), sofern b0 der abstrakte Wert für die Bedingung und b1, b2 die ab-strakten Werte für die Alternativen darstellen. Hier ist die Intuition, dass bei dem be-dingten Ausdruck die Bedingungg mit Sicherheit ausgewertet werden muss, währendnur eine der beiden Alternativen eine Rolle spielt. Bei einer Funktionsanwendungwird der (gegenwärtige) abstrakte Wert der Funktion in der Funktionsumgebung φ

nachgeschlagen und auf die Werte angewendet, den die Auswertung rekursiv für dieArgumentausdrücke ermittelt. Bei einer let-definierten Variable x in einem Ausdrucke0 wird erst der abstrakte Wert für x ermittelt und dann der Wert des Hauptausdruckse0 relativ zu diesem Wert berechnet. Ist die Variable x dagegen let#-definiert, müssenwir zusätzlich sicherstellen, dass der Gesamtausdruck den abstrakten Wert 0 erhält,falls der Wert, der für x ermittelt wurde, 0 ist.

Beispiel 3.7.3 Betrachten Sie den Ausdruck e, der gegeben ist durch:

if x ≤ 0 then aelse fac2 (x − 1) (a · x)

Für Werte b1, b2 ∈ 2, sei ρ die Variablenbelegung ρ = {x → b1, a → b2}. Au-ßerdem ordne die Abbildung φ der Funktion fac2 die abstrakte Funktion fun x →fun a → x ∧ a zu. Dann liefert die abstrakte Auswertung von e den Wert:

Page 170: œbersetzerbau: Band 3: Analyse und Transformation

160 3 Optimierung funktionaler Programme

[[e]]� ρ φ = (b1 ∧ 1) ∧ (b2 ∨ (φ fac2) (b1 ∧ 1) (b2 ∧ b1))= b1 ∧ (b2 ∨ (b1 ∧ b2))= b1 ∧ b2

��Mit der abstrakten Ausdrucksauswertung stellen wir für jede im Programm definierteFunktion f = fun x1 → . . . → fun xk → e die Gleichungen:

φ(f) b1 . . . bk = [[ei]]� {xj → bj | j = 1, . . . , k} φ

auf für alle b1, . . . , bk ∈ 2. Weil die rechten Seiten monoton von den abstraktenWerten φ(f) abhängen, besitzt dieses Gleichungssystem eine kleinste Lösung. DieseLösung bezeichnen wir mit [[f]]�.

Beispiel 3.7.4 Für die Funktion fac2 aus Beispiel 3.7.2 erhalten wir die Gleichun-gen:

[[fac2]]� b1 b2 = b1 ∧ (b2 ∨ [[fac2]]� b1 (b1 ∧ b2))

Eine Fixpunktiteration liefert für [[fac2]]� sukzessive die abstrakten Funktionen:

0 fun x → fun a → 01 fun x → fun a → x ∧ a2 fun x → fun a → x ∧ a

Beachten Sie, dass wir hier die auftretenden abstrakten Funtionen durch boolescheAusdrücke anstelle ihrer Wertetabellen repräsentiert haben.

Wir folgern, dass die Funktion fac2 in beiden Argumenten strikt ist. Damit kön-nen wir die Definition von fac2 transformieren in:

let rec fac2 = fun x → fun a → if x ≤ 0 then aelse let# x′ = x − 1

in let# a′ = x · ain fac2 x′ a′

��Im Beispiel liefert die Analyse die richtigen Ergebnisse. Tatsächlich ist die angege-bene abstrakte Ausdrucksauswertung die Abstraktion der denotationellen Semantikunserer funktionalen Sprache. Die denotationelle Semantik verwendet für die ganzenZahlen eine partielle Ordnung, die aus der Menge Z der Zahlwerte zusammen miteinem speziellen Wert ⊥ besteht, der eine (noch) nicht terminierte Auswertung re-präsentiert. Die Ordnungsbeziehung in dieser partiellen Ordnung ist gegeben durch⊥ � z für alle z ∈ Z. Die abstrakte denotationelle Semantik interpretiert statt-dessen Basiswerte und Operatoren über dem Verband 2. Als Beschreibungsrelationzwischen konkreten und abstrakten Werten verwenden wir:

Page 171: œbersetzerbau: Band 3: Analyse und Transformation

3.7 Verbesserung der Auswertungsreihenfolge: Die Striktheitsanalyse 161

⊥ Δ 0 und z Δ 1 für z ∈ Z

Wir verzichten auf den Beweis, dass die angegebene Analyse stets korrekte Ergeb-nisse liefert, weisen aber darauf hin, dass die Korrektheit mit Induktion über dieFixpunktiteration nachgewiesen werden kann.

Im Folgenden sollen neben Basiswerten auch strukturierte Daten berücksichtigtwerden. Bisher wurde bei Funktionen nur unterschieden, ob ein Argument ganz odergar nicht für die Berechnung des Ergebnisses benötigt wird. Bei zusammengesetz-ten Datenstrukturen können Funktionen jedoch auf unterschiedlich große Teile ihrerArgumente zugreifen.

Beispiel 3.7.5 Die Funktion

let hd = fun l → match l with h :: t → h

besucht nur den obersten Listen-Konstruktor ihres Arguments und liefert das ersteElement zurück. Die Funktion length aus Beispiel 3.6.1 benötigt dagegen sämtlicheListen-Konstruktoren und die leere Liste am Ende der Argumentliste, um ihr Ergeb-nis zu berechnen. ��Um die unterschiedliche Benutzung von Argumenten zu studieren, betrachten wirnun Programme, die neben Basiswerten zusätzlich mit Tupeln und Listen arbeiten.Wir erweitern deshalb die Syntax für Ausdrücke e, indem wir die entsprechendenKonstrukte für zusammengesetzte Datenstrukturen zulassen:

e ::= . . . | [ ] | e1 :: e2 | match e0 with [ ] → e1 | h :: t → e2

| (e1, . . . , ek) | match e0 with (x1, . . . , xk) → e1

Wir betrachten zuerst den Fall, dass man nur jeweils am obersten Konstruktor derWerte interessiert ist. Eine Funktion f heißt wurzel-strikt im i-ten Argument, fallsfür die Berechnung des obersten Konstruktors einer Funktionsanwendung von f deroberste Konstruktor des i-ten Arguments erforderlich ist. Für Basiswerte stimmtWurzel-Striktheit mit Striktheit, wie wir sie bisher betrachtet haben, überein. Wiebei der Striktheit für Basiswerte verwenden wir ein Konstrukt let# x = e1 in e0,das vor der Berechnung des Wurzelkonstruktors von e0 den Wert von x bis zumWurzelkonstruktor auswertet.

Wie die Striktheitseigenschaften von Funktionen auf Basiswerten beschreibenwir auch Wurzel-Striktheit mit Hilfe boolescher Funktionen. Der Wert 0 repräsen-tiert nur den konkreten Wert ⊥ (nicht terminierte Berechnung), der Wert 1 dagegensämtliche übrigen Werte, also z.B. die Liste [1; 2] wie auch die teilweise berechnetenListen [1;⊥] oder 1 ::⊥.

Bei der Analyse gehen wir vor wie bei der Striktheitsanalyse für Basiswerte,erweitern aber die abstrakte Auswertungsfunktion [[e]]� ρ φ mit Regeln für Listen,Tupel und Fallunterscheidung:

Page 172: œbersetzerbau: Band 3: Analyse und Transformation

162 3 Optimierung funktionaler Programme

[[match e0 with [ ] → e1 | h :: t → e2]]� ρ φ =[[e0]]� ρφ ∧ ([[e1]]� ρφ ∨ [[e2]]� (ρ ⊕ {h, t → 1})) φ

[[match e0 with (x1, . . . , xk) → e1]]� ρ φ =[[e0]]� ρ φ ∧ [[e1]]� (ρ ⊕ {x1, . . . , xk → 1}) φ

[[[ ]]]� ρ φ = [[e1 :: e2]]� ρ φ = [[(e1, . . . , ek)]]� ρ φ = 1

Wenn ein Ausdruck bereits selbst den obersten Konstruktor des Ergebnisses bereitstellt, liefert seine Auswertung den Wert 1. Ein match-Ausdruck für Listen wird ana-log zu einem if-Ausdruck ausgewertet. Weil wir nichts über die Werte der beidenneu eingeführten Variablen im Fall einer zusammengesetzten Liste aussagen kön-nen, beschreiben wir diese jeweils mit dem Wert 1. Die abstrakte Auswertung einesmatch-Ausdrucks für Tupel entspricht der Konjunktion des Werts für den Ausdrucke0 und des Werts für den Rumpf e1, bei der als Werte für die neu eingeführten Varia-blen 1 angenommen wird.

Beispiel 3.7.6 Überprüfen wir unsere Analyse für die Funktion app, welche zweiListen konkateniert:

let rec app = fun x → fun y → match x with [ ] → y| h :: t → h :: app t y

Abstrakte Interpretation liefert die Gleichungen:

[[app]]� b1 b2 = b1 ∧ (b2 ∨ 1)= b1

für Werte b1, b2 ∈ 2. Wir schließen, dass zur Berechnung des Wurzelkonstruktorsdes Ergebnisses mit Sicherheit der Wurzelkonstruktor des ersten Arguments erfor-derlich ist. ��In vielen Anwendungen wird nicht nur der Wurzelkonstruktor des Ergebniswertsbenötigt, sondern der gesamte Wert. Welche Argumente einer Funktion werdenganz benötigt, wenn das Ergebnis ganz benötigt wird? Diese Verallgemeinerung derStriktheit auf Basiswerten auf zusammen gesetzte Werte nennen wir totale Strikt-heit. Nun beschreibt der abstrakte Wert 0 alle konkreten Werte, die definitiv ein ⊥enthalten, während 1 nach wie vor sämtliche Werte beschreibt.

Auch das totale Striktheitsverhalten einer Funktion wollen wir durch boolescheFunktionen beschreiben. Die Regeln zur Ausdrucksauswertung der Striktheit fürAusdrücke ohne zusammengesetzte Datentypen erweitern wir erneut auf die Kon-strukte für Tupel, Listen und match-Ausdrücke:

Page 173: œbersetzerbau: Band 3: Analyse und Transformation

3.7 Verbesserung der Auswertungsreihenfolge: Die Striktheitsanalyse 163

[[match e0 with [ ] → e1 | h :: t → e2]]� ρ φ = let b = [[e0]]� ρ φ

in b ∧ [[e1]]� ρ φ

∨ [[e2]]� (ρ ⊕ {h → b, t → 1} φ

∨ [[e2]]� (ρ ⊕ {h → 1, t → b} φ

[[match e0 with (x1, . . . , xk) → e1]]� ρ φ = let b = [[e0]]� ρ φ

in [[e1]]� (ρ ⊕ {x1 → b, x2, . . . , xk → 1}) φ

∨ . . . ∨ [[e1]]� (ρ ⊕ {x1, . . . , xk−1 → 1, xk → b}) φ

[[[ ]]]� ρ φ = 1[[e1 :: e2]]� ρ φ = [[e1]]� ρ φ ∧ [[e2]]� ρ φ

[[(e1, . . . , ek)]]� ρ φ = [[e1]]� ρ φ ∧ . . . ∧ [[ek]]� ρ φ

[[let# x1 = e1 in e]]� ρ φ = [[e]]� (ρ ⊕ {x1 → [[e1]]� ρ φ}) φ

Bei der Analyse totaler Striktheit müssen Konstruktoranwendungen anders behan-delt werden als bei der Analyse von Wurzelstriktheit. Die Anwendung eines Daten-konstruktors wird nun als Konjunktion der abstrakten Werte interpretiert, welche dieAuswertung der Komponenten ergibt. Bei der Analyse eines let#-Ausdrucks mussbeachtet werden, dass der vorgezogene Ausdruck nur bis zum Wurzelkonstruktorausgewertet wird. Der so berechnete Wert kann damit durchaus ⊥ enthalten, ohnedass die Auswertung des Gesamtausdrucks nicht terminiert. Die abstrakte Auswer-tung eines let#-Ausrucks unterscheidet sich deshalb nicht von der abstrakten Aus-wertung eines let-Ausdrucks. Auch die Zerlegung des Werts eines Ausdrucks mitHilfe des match-Konstrukts hat sich geändert: Wird der Ausdruck e0, für den dieFallunterscheidung vorgenommen wird, zur leeren Liste ausgewertet, dann ist seinabstrakter Wert nicht 0. Diesem Fall entspricht deshalb die Konjunktion des abstrak-ten Werts von e0 und dem abstrakten Wert des Ausdrucks für den Fall einer leerenListe. Ergibt der Ausdruck e0 andererseits eine zusammengesetzte Liste, betrach-ten wir zwei Fälle. Liefert die abstrakte Auswertung von e0 den Wert 1, kann fürdie Komponenten der Liste ebenfalls nur der Wert 1 angenommen werden. Liefertdie abstrakte Auswertung für e0 dagegen 0, muss entweder das erste Element oderder Rest der Liste ⊥ enthalten. Folglich muss entweder die lokale Variable h oderdie lokale Variable t den Wert 0 erhalten. Sei b der Wert des Ausdrucks e0. Diesebeiden Fälle kann man dann kompakt zusammenfassen durch die Disjunktion derErgebnisse, die die abstrakten Auswertungen des Ausdrucks für zusammengesetzteListen liefern, bei denen für die neu eingeführten lokalen Variablen h, t jeweils b, 1bzw. 1, b eingesetzt werden. Eine ähnliche Disjunktion ergibt sich bei der abstraktenAuswertung eines match-Ausdrucks für Tupel. Wird ein Tupel durch 0 beschrieben,d.h. enthält es als Bestandteil ⊥, dann muss einer der Komponenten ebenfalls ⊥enthalten. Diese Komponente kann dann durch 0 beschrieben werden. Wird ein Tu-pel dagegen durch 1 beschrieben, ist nichts über seine Komponenten bekannt. Siemüssen dann sämtlich durch 1 beschrieben werden.

Beispiel 3.7.7 Wir testen unseren Ansatz zur Analyse totaler Strikheit erneut an derFunktion app aus Beispiel 3.7.6. Abstrakte Interpretation liefert die Gleichungen:

Page 174: œbersetzerbau: Band 3: Analyse und Transformation

164 3 Optimierung funktionaler Programme

[[app]]� b1 b2 = b1 ∧ b2 ∨ b1 ∧ [[app]]� 1 b2 ∨ 1 ∧ [[app]]� b1 b2

= b1 ∧ b2 ∨ b1 ∧ [[app]]� 1 b2 ∨ [[app]]� b1 b2

für b1, b2 ∈ 2. Fixpunktiteration liefert die folgenden Approximationen an den klein-sten Fixpunkt:

0 fun x → fun y → 01 fun x → fun y → x ∧ y2 fun x → fun y → x ∧ y

Wir schließen, dass beide Argumente definitiv ganz benötigt werden, sofern das Er-gebnis ganz benötigt wird. ��Ob der Wert eines Ausdrucks aber ganz benötigt wird, hängt vom Kontext ab, indem der Ausdruck steht. Für eine Funktion f, die möglicherweise in einem solchenKontext vorkommt, sollte eine Variante f# bereit gehalten werden, die ihr Ergeb-nis gegebenenfalls effizienter berechnet. Der Einfachheit halber betrachten wir nurFunktionen, die für das ganze Ergebnis die Werte sämtlicher Argumente ganz be-nötigen. Für die Implementierung der Funktion f# nehmen an, dass ihre Argumentebereits ganz ausgewertet sind. Die Implementierung muss dann garantieren, dassdies auch für alle rekursiven Aufrufe der Varianten g# gilt und auch das Ergebnisbereits ganz ausgewertet ist. Für die Funktion app lässt sich die Variante app# soimplementieren:

let rec app# = fun x → fun y → match x with [ ] → y| h :: t → let# t1 = app# t y

in h :: t1

Dabei nehmen wir an, dass sowohl für Variablen wie für Konstruktoren, die nurauf Variablen angewendet werden, keine eigenen Abschlüsse angelegt werden. Ei-ne allgemeine Transformation, die Informationen über totale Striktheit systematischausnutzt, entwickelt Aufg. 13.

Das Programmiersprachenfragment, für das wir bisher Striktheitsanalysen ent-wickelt haben, ist sehr eingeschränkt. Im Folgenden wollen wir knapp skizzieren,wie diese Einschränkungen zumindest teilweise abgemildert werden können.

Als erstes haben wir angenommen, dass alle Funktionen auf der obersten Ebenedefiniert werden. Jedes Programm unseres OCAML-Fragments kann so transformiertwerden, dass diese Eigenschaft erfüllt ist (siehe Aufg. 15). Alternativ ordnen wir beider Analyse einer lokalen Funktion sämtlichen freien Variablen der Funktion denWert 1 (don’t know) zu. Das liefert möglicherweise ungenauere Informationen, aberzumindest korrekte Ergebnisse.

Weiterhin haben wir uns auf k-stellige Funktionen ohne funktionale Parame-ter oder Ergebnisse und ohne partielle Anwendungen beschränkt. Der Grund ist,dass der vollständige Verband aller k-stelligen monotonen abstrakten Funktionen2 → . . . → 2 echt aufsteigende Ketten enthält, deren Länge exponentiell in k, d.h.der Anzahl der Parameter ist. Die Anzahl der Elemente in diesem Verband ist sogar

Page 175: œbersetzerbau: Band 3: Analyse und Transformation

3.8 Aufgaben 165

doppelt exponentiell groß. Mit zunehmend komplexen Typen steigt die Komplexi-tät allein der potenziell zu betrachtenden abstrakten Funktionen dramatisch an. EinAusweg besteht darin, die abstrakten Funktionsbereiche selbst durch kleinere voll-ständige Verbände zu abstrahieren. Zum Beispiel könnten wir für Funktionsbereichewieder den booleschen Verband 2 verwenden: 0 repräsentiert dann etwa die konstan-te 0-Funktion. Bei einer so starken Abstraktion wird man in Programmen, die sy-stematisch höhere Funktionen einsetzen, wenig brauchbare Striktheitsinformationenableiten können. Ein Teil der höheren Funktionen kann jedoch durch Funktionsspe-zialisierung aus Kapitel 3.4 beseitigt werden — wodurch sich die Möglichkeiten füreine Striktheitsanalyse verbessern.

Striktheitsanalyse, wie wir sie hier betrachtet haben, ist nur für monomorpheFunktionen bzw. monomorphe Instanzen polymorpher Funktionen geeignet. Für denProgrammierer ist oft nicht leicht nachvollziehbar, wann der Übersetzer in der Lageist, Striktheitsinformationen zu berechnen und auszunutzen. Die Programmierspra-che HASKELL bietet deshalb Annotationen an, die es dem Programmierer erlauben,die Auswertung von Ausdrücken zu erzwingen, wann immer er es aus Effizienzgrün-den für wichtig erachtet.

3.8 Aufgaben

1. let-Optimierung. Betrachten Sie die folgende Gleichung:

(fun y → let x = e in e1) = (let x = e in fun y → e1)

falls y nicht frei in e vorkommt.a) Geben Sie Bedingungen an, unter denen die Ausdrücke auf beiden Seiten

semantisch äquivalent sind.b) Geben Sie Bedingungen an, unter denen die Anwendung dieser Gleichung

von links nach rechts zur Erhöhung der Effizienz der Auswertung beitragenkann.

2. letrec-Optimierung. Geben Sie Regeln an, wie let-Definitionen auch aus letrec-Ausdrücken herausgezogen werden können. Geben Sie Bedingungen an, unterdenen Ihre Transformationen Semantik-erhaltend sind bzw. zu einer Verbesse-rung der Effizienz führen. Testen Sie Ihre Optimierungen an einigen Beispielen.

3. let-Optimierung. Was halten Sie von den Regeln:

(if let x = e in e0 then e1 else e2) = (let x = e in if e0 then e1 else e2)(if e0 then let x = e in e1 else e2) = (let x = e in if e0 then e1 else e2)

wobei x nicht frei in den Ausdrücken e0, e1, und e2 vorkommt.4. Baumgrammatik. Eine Baumgrammatik ist ein Tupel G = (N, T, P), wobei

N eine endliche Menge von Nichtterminalsymbolen ist, T eine endliche Mengevon terminalen Konstruktoren und P eine Menge von Regeln der Form A ⇒ t,

Page 176: œbersetzerbau: Band 3: Analyse und Transformation

166 3 Optimierung funktionaler Programme

wobei t ein Term ist, der nicht-terminalen Symbolen aus N mit Hilfe der Kon-struktoren aus T aufgebaut ist. Die Sprache LG(A) der regulären Baumgramma-tik G für ein Nichtterminal A ist die Menge aller terminalen Ausdrücke t, die ausdem Nichtterminal A mit Hilfe der Regeln aus P ableitbar sind. Ein Ausdruckheißt dabei terminal, wenn in ihm kein Nichtterminal vorkommt.Geben Sie reguläre Baumgrammatiken an für die folgenden Mengen von Bäu-men:a) alle Listen (innere Knoten : “::”) mit einer geraden Anzahl von Elementen

aus {0, 1, 2};b) alle Listen mit Elementen aus {0, 1, 2}, so dass die Summe der Elemente

gerade ist;c) alle Terme mit inneren Knoten :: und Blättern {0, 1, 2} oder [ ], die vom Typ

list list int sind.5. Baumgrammatik (Forts.). Sei G eine reguläre Baumgrammatik der Größe n

und A ein Nichtterminalsymbol von G. Zeigen Sie:a) LG(A) �= ∅ gdw. t ∈ LG(A) für ein t der Tiefe ≤ n;b) LG(A) ist unendlich gdw. t ∈ LG(A) für ein t der Tiefe d mit n ≤ d < 2n.

(Definieren Sie insbesondere “Größe einer Grammatik” so, dass diese Behaup-tungen gelten.)

6. Wertanalyse: Fallunterscheidung. Modifizieren Sie den Algorithmus zurWertanalyse so, dass er bei Fallunterscheidungen die Reihenfolge der Musterberücksichtigt.

7. Wertanalyse: Ausnahmen. Betrachten Sie die funktionale Kernsprache, erwei-tert um die Konstrukte:

e ::= . . . | raise e | (try e with p1 → e1 | . . . | pk → ek)

Der Ausdruck raise e wirft eine Ausnahme vom Wert e, während der try-Ausdruck den Hauptausdruck e auswertet und, sofern die Berechnung mit einerAusnahme v endet, diese Ausnahme fängt, falls eines der Muster pi auf v passtund andernfalls die Ausnahme erneut wirft.Wie muss die Wertanalyse modifiziert werden, um die Menge der Ausnahmenzu ermitteln, die die Auswertung eines Ausdrucks möglicherweise werfen kann?

8. Wertanalyse: Referenzen. Betrachten Sie die funktionale Kernsprache, erwei-tert um destruktiv modifizierbare Referenzen:

e ::= . . . | ref e | (e1 := e2) |!eErweitern Sie die Wertanalyse auf diese erweiterte Programmiersprache. Wiekönnte man mit Hilfe dieser Analyse heraus finden, dass ein Ausdruck pur ist,d.h. dass seine Auswertung keine Referenzen modifiziert?

9. Vereinfachungsregeln für die Identität. Sei id = fun x → x. Stellen Sie einSystem von Regeln auf, mit dem Ausdrücke, die id enthalten, vereinfacht werdenkönnen!

10. Vereinfachungsregeln für rev. Definieren Sie Funktionen rev, fold_right,rev_map, rev_tabulate und ref_loop, wobei ref zu einer Liste eine Liste mit den

Page 177: œbersetzerbau: Band 3: Analyse und Transformation

3.8 Aufgaben 167

gleichen Elementen, aber in umgekehrter Reihenfolge liefert. Für die übrigenFunktionen gilt:

fold_right f a = comp (fold_left f a) rev

rev_map f = comp (map f) rev

rev_tabulate n = comp rev tabulate n

rev_loop n soll sich wie loop n verhalten, nur dass die Iteration von n − 1 bis 0läuft und nicht umgekehrt.Entwerfen Sie Regeln für die Kompositionen dieser Funktionen sowie dieserFunktionen mit map, fold_left, filter und tabulate! Verwenden Sie dabei, dasscomp rev rev die Identität ist.Erläutern Sie, unter welchen Umständen diese Regeln anwendbar sind undwarum sie die Effizienz verbessern!

11. Vereinfachungsregeln für indexabhängige Funktionen. Definieren Sie index-abhängige Varianten der Funktionen map und fold_left. Stellen Sie Vereinfa-chungsregeln auf und argumentieren Sie, unter welchen Bedingungen diese an-wendbar sind!Wie verhalten sich die neuen Funktionen bei Komposition mit map, fold_left,filter und tabulate?

12. Vereinfachungsregeln für allgemeine Datenstrukturen. Stellen Sie Vereinfa-chungsregeln auf für Funktionen map und fold_left auf baumartigen Datenstruk-turen. Die Funktion map soll dabei eine Funktion auf alle Datenelemente an-wenden, die in der Datenstruktur enthalten sind, während fold_left alle in derDatenstruktur enthaltenen Datenelemente in einen Wert zusammenfasst.Geben Sie Beispiele für Ihr generelles Schema und diskutieren Sie seine An-wendbarkeit. Wie könnte eine Verallgemeinerung der Funktion tabulate von Li-sten auf baumartige Datenstrukturen aussehen?Gibt es in Ihrem Schema auch ein Analogon zu der Listenfunktion filter? De-finieren Sie zusätzlich Funktionen to_list und from_list, die Ihre Datenstrukturin eine Liste transformiert bzw. aus einer Liste rekonstruiert. Welche Vereinfa-chungsregeln gibt es für diese Funktionen?

13. Optimierung für totale Striktheit. Entwickeln Sie eine Transformation, die zueinem Ausdruck, dessen Ergebnis sicher ganz benötigt wird, einen optimiertenAusdruck liefert.

14. Kombinierung von Wurzel- und totaler Striktheit. Definieren Sie eine Strikt-heitsanalyse, die simultan totale Striktheit und Wurzelstriktheit analysiert. Ver-wenden Sie dazu einen Verband 3 = {0 < 1 < 2}.Definieren Sie eine Beschreibungsrelation zwischen konkreten Werten und ab-strakten Werten aus 3 und definieren Sie die benötigte abstrakte Ausdrucksaus-wertung.Probieren Sie Ihre Analyse an der Funktion app aus.Verallgemeinern Sie Ihre Analyse zu einer Analyse, die für ein gegebenes k ≥ 1ermittelt, bis zu welcher Tiefe ≤ k − 1 die Argumente einer Funktion aus-

Page 178: œbersetzerbau: Band 3: Analyse und Transformation

168 3 Optimierung funktionaler Programme

gewertet werden müssen (oder ganz), wenn das Ergebnis bis zu einer Tiefe0 ≤ j ≤ k − 1 oder ganz benötigt wird.

15. Verschiebung lokaler Funktionen auf die oberste Ebene. Transformieren Sieein gegebenes OCaml-Programm so, dass sämtliche Funktionen auf der ober-sten Ebene definiert werden.

16. Monotone Funktionen über 2. Konstruieren Sie die folgenden vollständigenVerbände monotoner Funktionen:a) 2 → 2;b) 2 → 2 → 2;c) (2 → 2) → 2 → 2!

17. Striktheitsanalyse höherer Funktionen. Analysieren Sie die totalen Strikt-heitseigenschaften der monomorphen Instanzen der Funktionen map und fold_leftmit den Typen:

map : (int → int) → list int → list intfold_left : (int → int → int) → int → list int → list int

3.9 Literaturhinweise

Der λ-Kalkül mit β-Reduktion und α-Konversion bildet die theoretische Grundla-ge funktionaler Programmiersprachen. Er basiert auf Arbeiten zur Grundlegung derMathematik von Alonzo Church und Stephen Cole Kleene aus den 1930er Jahren.Nach wie vor ist das Buch von Hendrik Barendregt [Bar84] das Standardwerk, indem wichtige Eigenschaften und Theoreme umfassend dargestellt werden.

Einen Überblick über die Optimierungen im HASKELL-Compilers bietet [JS98].Dort werden auch ausführlich Optimierungen geschachtelter let-Ausdrücke behan-delt [JPS96].

Die fold/unfold-Transformationen werden erstmals ausführlich in [BD77] dis-kutiert. Inlining und Funktionsspezialisierung sind einfache Formen partieller Aus-wertung von Programmen [SS99]. Unsere Wertanalyse lehnt sich an die von NevinHeintze beschriebene an [Hei94]. Eine typbasierte Analyse von Seiteneffekten wur-de von Torben Amtoft, Flemming Nielson und Hanne Riis Nielson [ANN97] vorge-schlagen.

Die Idee, Zwischendatenstrukturen systematisch zu unterdrücken, stammt vonPhil Wadler [Wad90]. Eine Erweiterung auf Programme mit höheren Funktionenbietet [SS98]. Die hier vorgestellte, besonders einfache Variante für Listen wur-de von Andrew J. Gill, John Launchbury und Simon L. Peyton Jones vorgeschla-gen [GLJ93]. Verallgemeinerungen auf beliebige algebraische Datenstrukturen stu-dieren Akihiko Takano und Erik Meijer [TM95].

Die Idee, Striktheitsanalyse zur Umwandlung von CBV in CBN einzusetzen,geht auf Alan Mycroft zurück [Myc80]. Eine Verallgemeinerung auf monomorpheProgramme mit höheren Funktionen haben Geoff Burn, Chris Hankin und Samson

Page 179: œbersetzerbau: Band 3: Analyse und Transformation

3.9 Literaturhinweise 169

Abramsky vorgestellt [BHA86]. Das hier beschriebene Verfahren zur Analyse to-taler Striktheit für Programme mit strukturierten Daten ist eine Vereinfachung desVerfahrens von R.C. Sekar, I.V. Ramakrishnan und Prateek Mishra [SPR90].

Gar nicht behandelt wurde in unserem Kapitel Optimierungen, die auf der Re-präsentation funktionaler Programme in continuation-passing style basieren. Eineausführliche Darstellung dieser Technik bietet Andrew W. Appel [App07].

Page 180: œbersetzerbau: Band 3: Analyse und Transformation

Literaturverzeichnis

[ABRT02] Paul Anderson, David Binkley, Genevieve Rosay, Tim Teitelbaum. Flow insen-sitive points-to sets. Information & Software Technology, 44(13):743–754, 2002.

[AG04] Andrew W. Appel, Maia Ginsburg. Modern Compiler Implementation in C.Cambridge University Press, 2004.

[AH87] Samson Abramsky, Chris Hankin (Hrsg.). Abstract Interpretation of DeclarativeLanguages. Ellis Horwood, 1987.

[ALSU07] Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman. Compilers: Prin-ciples, Techniques, & Tools. Addison-Wesley, 2007. 2nd revised Edition.

[ANN97] Torben Amtoft, Flemming Nielson, Hanne Riis Nielson. Type and BehaviourReconstruction for Higher-Order Concurrent Programs. J. Funct. Program.,7(3):321–347, 1997.

[App07] Andrew W. Appel. Compiling with Continuations. Cambridge University Press,2007.

[Bac97] David Francis Bacon. Fast and Effective Optimization of Statically Typed Object-oriented Languages. PhD thesis, Berkeley, 1997.

[Bar84] Hendrik Pieter Barendregt. The Lambda Calculus: Its Syntax and Semantics,volume 103 of Studies in Logic and the Foundations of Mathematics. NorthHolland, Amsterdam, 1984. Revised edition.

[BD77] Rod M. Burstall, John Darlington. A Transformation System for DevelopingRecursive Programs. J. ACM, 24(1):44–67, 1977.

[BHA86] Geoffrey L. Burn, Chris Hankin, Samson Abramsky. Strictness Analysis forHigher-Order Functions. Sci. Comput. Program., 7(3):249–278, 1986.

[CC76] Patrick Cousot, Radhia Cousot. Static Determination of Dynamic Propertiesof Programs. In 2nd Int. Symp. on Programming, pp. 106–130. Dunod, Paris,France, 1976.

[CC77a] Patrick Cousot, Radhia Cousot. Abstract Interpretation: A Unified Lattice Modelfor Static Analysis of Programs by Construction or Approximation of Fixpoints.In 4th ACM Symp. on Principles of Programming Languages (POPL), pp. 238–252, 1977.

[CC77b] Patrick Cousot, Radhia Cousot. Static Determination of Dynamic Properties ofRecursive Procedures. In E.J. Neuhold (Hrsg.), IFIP Conf. on Formal Descripti-on of Programming Concepts, pp. 237–277. North-Holland, 1977.

[CC02] Patrick Cousot, Radhia Cousot. Systematic Design of Program TransformationFrameworks by Abstract Interpretation. In 29th ACM Symp. on Principles ofProgramming Languages (POPL), pp. 178–190, 2002.

H. Seidl et al., Übersetzerbau, eXamen.press, DOI 10.1007/978-3-642-03331-5,c© Springer-Verlag Berlin Heidelberg 2010

Page 181: œbersetzerbau: Band 3: Analyse und Transformation

172 Literaturverzeichnis

[CGS+99] Jong-Deok Choi, Manish Gupta, Mauricio Serrano, Vugranam C. Sreedhar, SamMidkiff. Escape Analysis for Java. SIGPLAN Not., 34(10):1–19, 1999.

[CH78] Patrick Cousot, Nicolas Halbwachs. Automatic Discovery of Linear Restraintsamong Variables of a Program. In 5th ACM Symp. on Principles of Program-ming Languages (POPL), pp. 84–97, 1978.

[CLRS09] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clif Stein. Intro-duction to Algorithms (Third Edition). MIT Press, 2009.

[CT04] Keith D. Cooper, Linda Torczon. Engineering a Compiler. Morgan Kaufmann,2004.

[FRD00] Manuel Fähndrich, Jakob Rehof, Manuvir Das. Scalable Context-sensitive FlowAnalysis Using Instantiation Constraints. SIGPLAN Not., 35(5):253–263, 2000.

[FS99] Christian Fecht, Helmut Seidl. A Faster Solver for General Systems of Equations.Science of Computer Programming (SCP), 35(2):137–161, 1999.

[GLJ93] Andrew J. Gill, John Launchbury, Simon L. Peyton Jones. A Short Cut to Defo-restation. In Functional Programming and Computer Architecture (FPCA), pp.223–232, 1993.

[GMW81] Robert Giegerich, Ulrich Möncke, Reinhard Wilhelm. Invariance of Approxima-te Semantics with Respect to Program Transformations. In GI Jahrestagung, pp.1–10, 1981.

[Gra91] Philippe Granger. Static Analysis of Linear Congruence Equalities among Va-riables of a Program. In Int. Joint Conf. on Theory and Practice of SoftwareDevelopment (TAPSOFT), pp. 169–192. LNCS 493, Springer, 1991.

[GS07] Thomas Gawlitza, Helmut Seidl. Precise Fixpoint Computation Through Strat-egy Iteration. In European Symposium on Programming (ESOP), pp. 300–315.LNCS 4421, Springer, 2007.

[Hec77] Matthew S. Hecht. Flow Analysis of Computer Programs. North Holland, 1977.[Hei94] Nevin Heintze. Set-based Analysis of ML Programs. SIGPLAN Lisp Pointers,

VII(3):306–317, 1994.[JPS96] Simon L. Peyton Jones, Will Partain, André Santos. Let-floating: Moving Bin-

dings to Give Faster Programs. In Int. Conf. on Functional Programming (ICFP),pp. 1–12, 1996.

[JS98] Simon L. Peyton Jones, André L. M. Santos. A Transformation-Based Optimiserfor Haskell. Sci. Comput. Program., 32(1-3):3–47, 1998.

[Kar76] Michael Karr. Affine Relationships Among Variables of a Program. Acta Infor-matica, 6:133–151, 1976.

[Kil73] Gary A. Kildall. A Unified Approach to Global Program Optimization. In ACMSymp. on Principles of Programming Languages (POPL), pp. 194–206, 1973.

[Kno98] Jens Knoop. Optimal Interprocedural Program Optimization, A New Frameworkand Its Application. LNCS 1428, Springer, 1998.

[KRS94a] Jens Knoop, Oliver Rüthing, Bernhard Steffen. Optimal Code Motion: Theoryand Practice. ACM Trans. Program. Lang. Syst., 16(4):1117–1155, 1994.

[KRS94b] Jens Knoop, Oliver Rüthing, Bernhard Steffen. Partial Dead Code Elimination.In ACM Conf. on Programming Languages Design and Implementation (PLDI),pp. 147–158, 1994.

[KS92] Jens Knoop, Bernhard Steffen. The Interprocedural Coincidence Theorem. In 4thInt. Conf. on Compiler Construction (CC), pp. 125–140. LNCS 541, Springer,1992.

[KTL09] Sudipta Kundu, Zachary Tatlock, Sorin Lerner. Proving Optimizations CorrectUsing Parameterized Program Equivalence. In ACM SIGPLAN Conf. on Pro-gramming Language Design and Implementation (PLDI), 2009.

Page 182: œbersetzerbau: Band 3: Analyse und Transformation

Literaturverzeichnis 173

[KU76] John B. Kam, Jeffrey D. Ullman. Global Data Flow Analysis and Iterative Algo-rithms. Journal of the ACM, 23(1):158–171, 1976.

[KU77] John B. Kam, Jeffrey D. Ullman. Monotone Data Flow Analysis Frameworks.Acta Inf., 7:305–317, 1977.

[Ler09] Xavier Leroy. Formal Verification of a Realistic Compiler. Communications ofthe ACM, 52(7):107–115, 2009.

[LMC03] Sorin Lerner, Todd D. Millstein, Craig Chambers. Automatically Proving theCorrectness of Compiler Optimizations. In ACM SIGPLAN Conf. on Program-ming Language Design and Implementatio (PLDI), pp. 220–231, 2003.

[LMRC05] Sorin Lerner, Todd Millstein, Erika Rice, Craig Chambers. Automated SoundnessProofs for Dataflow Analyses and Transformations via Local Rules. In 32ndACM Symp. on Principles of Programming Languages (POPL), pp. 364–377,2005.

[LPH01] Donglin Liang, Maikel Pennings, Mary Jean Harrold. Extending and EvaluatingFlow-insensitive and Context-insensitive Points-to Analyses for Java. In ACMSIGPLAN-SIGSOFT Workshop on Program Analysis For Software Tools andEngineering (PASTE), pp. 73–79, 2001.

[MAWF98] Florian Martin, Martin Alt, Reinhard Wilhelm, Christian Ferdinand. Analysis ofLoops. In 7th Int. Conf. on Compiler Construction (CC), pp. 80–94. LNCS 1383,Springer, 1998.

[MJ81] Steven S. Muchnick, Neil D. Jones (Hrsg.). Program Flow Analysis: Theory andApplication. Prentice Hall, 1981.

[MOS04] Markus Müller-Olm, Helmut Seidl. Precise Interprocedural Analysis throughLinear Algebra. In 31st ACM Symp. on Principles of Programming Languages(POPL), pp. 330–341, 2004.

[MOS05] Markus Müller-Olm, Helmut Seidl. A Generic Framework for InterproceduralAnalysis of Numerical Properties. In Static Analysis, 12th Int. Symp. (SAS), pp.235–250. LNCS 3672, Springer, 2005.

[MOS07] Markus Müller-Olm, Helmut Seidl. Analysis of Modular Arithmetic. ACMTrans. Program. Lang. Syst., 29(5), 2007.

[Muc97] Steven S. Muchnick. Advanced Compiler Design and Implementation. MorganKaufmann, 1997.

[Myc80] Alan Mycroft. The Theory and Practice of Transforming Call-by-need into Call-by-value. In Symposium on Programming: Fourth ’Colloque International sur laProgrammation’, pp. 269–281. LNCS 83, Springer, 1980.

[NNH99] Flemming Nielson, Hanne Riis Nielson, Chris Hankin. Principles of ProgramAnalysis. Springer, 1999.

[Pai90] Robert Paige. Symbolic Finite Differencing - Part I. In 3rd European Symposiumon Programming (ESOP), pp. 36–56. LNCS 432, Springer, 1990.

[PS77] Robert Paige, Jacob T. Schwartz. Reduction in Strength of High Level Operati-ons. In 4th ACM Symp. on Principles of Programming Languages (POPL), pp.58–71, 1977.

[Ram02] G. Ramalingam. On Loops, Dominators, and Dominance Frontiers. ACM Trans.Program. Lang. Syst. (TOPLAS), 24(5):455–490, 2002.

[SHR+00] Vijay Sundaresan, Laurie Hendren, Chrislain Razafimahefa, Raja Vallée-Rai, Pa-trick Lam, Etienne Gagnon, Charles Godin. Practical Virtual Method Call Reso-lution for Java. In 15th ACM SIGPLAN Conf. on Object-oriented Programming,Systems, Languages, and Applications (OOPSLA), pp. 264–280, 2000.

[Sim08] Axel Simon. Value-Range Analysis of C Programs: Towards Proving the Absenceof Buffer Overflow Vulnerabilities. Springer Verlag, 2008.

Page 183: œbersetzerbau: Band 3: Analyse und Transformation

174 Literaturverzeichnis

[SLGA03] Jeffrey Sheldon, Walter Lee, Ben Greenwald, Saman P. Amarasinghe. StrengthReduction of Integer Division and Modulo Operations. In Languages and Com-pilers for Parallel Computing, 14th Int. Workshop (LCPC). Revised Papers, pp.254–273. LNCS 2624, Springer, 2003.

[SP81] Micha Sharir, Amir Pnueli. Two Approaches to Interprocedural Data Flow Ana-lysis. In Steven S. Muchnick, Neil D. Jones (Hrsg.), Program Flow Analysis:Theory and Application, pp. 189–234. Prentice Hall, 1981.

[SPR90] R. C. Sekar, Shaunak Pawagi, I. V. Ramakrishnan. Small Domains Spell FastStrictness Analysis. In ACM Symp. on Principles o Programming Languages(POPL), pp. 169–183, 1990.

[SRW99] Mooly Sagiv, Thomas W. Reps, Reinhard Wilhelm. Parametric Shape Analysisvia 3-Valued Logic. In 26th ACM Symp. on Principles of Programming Langua-ges (POPL), pp. 105–118, 1999.

[SRW02] Mooly Sagiv, Thomas W. Reps, Reinhard Wilhelm. Parametric Shape Analysisvia 3-Valued Logic. ACM Trans. Program. Lang. Syst. (TOPLAS), 24(3):217–298, 2002.

[SS98] Helmut Seidl, Morten Heine Sørensen. Constraints to Stop Deforestation. Sci.Comput. Program., 32(1-3):73–107, 1998.

[SS99] Jens P. Secher, Morten Heine Sørensen. On Perfect Supercompilation. In 3rd Int.Andrei Ershov Memorial Conference: Perspectives of System Informatics (PSI),pp. 113–127. LNCS 1755, Springer, 1999.

[SS03] Y.N. Srikant, Priti Shankar (Hrsg.). The Compiler Design Handbook: Optimiza-tions and Machine Code Generation. CRC Press, 2003.

[Ste96] Bjarne Steensgaard. Points-to Analysis in Almost Linear Time. In 23rd ACMSymp. on Principles of Programming Languages (POPL), pp. 32–41, 1996.

[TL09] Jean-Baptiste Tristan, Xavier Leroy. Verified validation of Lazy Code Motion. InACM SIGPLAN Conf. on Programming Language Design and Implementation(PLDI), pp. 316–326, 2009.

[TM95] Akihiko Takano, Erik Meijer. Shortcut Deforestation in Calculational Form.In SIGPLAN-SIGARCH-WG2.8 Conf. on Functional Programming Languagesand Computer Architecture (FPCA), pp. 306–313, 1995.

[Wad90] Philip Wadler. Deforestation: Transforming Programs to Eliminate Trees. Theor.Comput. Sci., 73(2):231–248, 1990.

Page 184: œbersetzerbau: Band 3: Analyse und Transformation

Stichwortverzeichnis

Abschluss, 157Abstrakte Interpretation, 47Alias, 69

May-, 69Must-, 69

Aliasanalyse, 68α-Konversion, 142Analyse, 3

flussunabhängige, 75Korrektheit, 76

interprozedurale, 124Points-to, 72

Korrektheit der, 74Analyserahmen

distributiver, 30monotoner, 27

AnsatzCall-String, 134funkionaler, 125

Antisymmetrie, 17Anwendung

partielle, 139Äquivalenzklasse, 77Array-Bounds-Check, 55Aufruf

-keller, 117letzter, 122

Aufrufgraph, 121Ausdruck

verfügbarer, 11Wert-, 149

Ausdrucksauswertung, 10abstrakte, 46

Auswertunggierige, 143, 157partielle, 44verzögerte, 157

verzögerte, 139zögerte, 143

BaumgrammatikNichtterminal einer, 149reguläre, 149

Berechnung, 9Berechnungsfolge

erreichende, 119pegelerhaltende, 118

Berechnungsschritt, 8Beschreibungsrelation, 47β-Reduktion, 142Bottom, 18Bound

Upper, 17Least, 17

C, 3, 139Call

Last, 122Call Stack, 117Code

schleifeninvarianter, 97Constant Folding, 43Constant Propagation, 43Copy Propagation, 40

Datenflussanalyse, VDatenstruktur

Union-Find-, 79Dead Code Elimination, 35Deforestation, 153.NET-Instruktionen, 139

Elementatomares, 29

Page 185: œbersetzerbau: Band 3: Analyse und Transformation

176 Stichwortverzeichnis

größtes, 18kleinstes, 18

Endrekursion, 123evaluation

eager, 143lazy, 143

Fixpunkt, 22größter, 23kleinster, 22Post-, 22

Fixpunktiterationakkumulierende, 24lokale, 90naive, 23rekursive, 87Round-Robin, 24

FORTRAN, VF#, 139Funktion

distributive, 28monotone, 20strikte, 28total distributive, 28

FunktionenFaltung von, 147Inlining von, 144Spezialisierung von, 146

Funktionsabstraktion, 141

Halbordnung, 17HASKELL, 139, 143, 146

Inliningvon Funktionen, 144von Prozeduren, 120

Interpretationabstrakte, VI

Intervallanalyse, 54Intervallarithmetik, 57

JAVA, 3Java Virtual Machine, 139

Kanteneffektabstrakter, 12konkreter, 9

Kellerrahmen, 117Kette

absteigende, 66aufsteigende, 21stabile aufsteigende, 22

Konkretisierung, 48Konstantenfaltung, 43Konstantenpropagation, 43

interprozedurale, 132Kontrollflussgraph, 8

interprozeduraler, 116Korrektheit

der Intervallanalyse, 60der Konstantenpropagation, 47der Transformation DE, 35der Transformation PRE, 97der Transformation RE, 14interprozeduraler Analyse, 127

Kreistrenner, 63

λ-Kalkül, 142Lattice

Atomic, 29Complete, 16, 18

LISP, 146Listenkonstruktor, 146Loop Inversion, 98Loop Separator, 63Lösung, 19

Merge-Over-All-Paths, 27

Mehrfachberechnung, 7Memoisierung, 7Muster, 148

Narrowing, 65-Operator, 67

Nichtterminal einer Baumgrammatik, 149

OCAML, 139, 143Ordnungsrelation, 17

duale, 23

Partial Order, 17Partitionen, 77

Verfeinerung von, 77Pattern Matching, 139Polymorphie, 139Prädominator, 99Programm

wohlgetyptes, 141

Page 186: œbersetzerbau: Band 3: Analyse und Transformation

Stichwortverzeichnis 177

Programmoptimierung, 3Programmpunkt, 8Programmzustand, 9Propagation von Kopien, 40

interprozedurale, 124intraprozedurale, 40

Redundancy Elimination, 13Redundanz, 7Redundanzen

Beseitigung von, 13partielle, 90

Reflexivität, 17Register

virtuelles, 4Registerzuteilung, 4Round-Robin-Iteration, 24

Korrektheit der, 25Rücksprungkante, 100Rückwärtsanalyse, 34

SCALA, 139Schleifenumkehr, 98Schranke

größte untere, 18kleinste obere, 17obere, 17

Seiteneffekt, 143Semantik

denotationelle, 160instrumentierte, 74operationelle, 3, 152

small-step, 8Referenz-, 74

Speicher, 4dynamisch allokierter, 68

Speicherzelleuninitialisierte, 74

Stack Frame, 117Striktheit, 157

totale, 162Wurzel-, 161

Substitution, 142Supergraph

interprozeduraler, 135

Terminierung, 143Top, 18totale Striktheit, 162toter Code

Beseitigung von, 35Transitivität, 17Typinferenz, 139Typsystem, 69

Übersetzunggetrennte, 124

Ungleichungssystem, 15Größe eines, 85

Variableglobale, 32

Variablen, 1-anordnung, 26-belegung, 9

abstrakte, 45-umbenennung, 142Benutzung einer, 32Definition einer, 32echt lebendige, 37echte Benutzung einer, 38lebendige, 32teilweise tote, 102tote, 32

Verbandatomarer, 29flacher, 18Höhe eines vollständigen, 25, 85Teilmengen-, 18vollständiger, 16, 18

Verifikation, VIVorwärtsanalyse, 34

Wertanalyse, 148Widening, 61

-Operator, 62Worklist-Algorithmus, 83Wurzel-Striktheit, 161

Zeiger, 68Zeigerarithmetik, 69Zuweisung

sehr beschäftigte, 91teilweise tote, 102tote, 32verfügbare, 12verzögerbare, 104zwischen Variablen, 40

Zwischendatenstrukturen, 153