Graphersetzungsregelgewinnung aus Hochsprachen und deren

Werbung
Universität Karlsruhe (TH)
Fakultät für Informatik
Institut für Programmstrukturen
und Datenorganisation (IPD)
Lehrstuhl Prof. Dr. Gerhard Goos
Graphersetzungsregelgewinnung
aus Hochsprachen
und deren Anwendung
Diplomarbeit
Andreas Schösser
Betreuer:
Dipl.-Inform. Rubino Geiß
Verantwortlicher Betreuer:
Prof. em. Dr. Dr. h.c. Gerhard Goos
14. September 2007
Hiermit erkläre ich, die vorliegende Arbeit selbstständig verfasst und keine anderen als
die angegebenen Hilfsmittel benutzt zu haben.
Datum, Ort
Unterschrift
Kurzfassung
In dieser Arbeit erweitern wir einen Übersetzer um eine Optimierung, die die Ausnutzung
von reichhaltigen Befehlssätzen, wie sie von modernen Prozessoren angeboten werden,
erlaubt. Die Optimierung findet auf einer übersetzerinternen, graphbasierten Zwischendarstellung statt. Hierbei wird in Zwischensprachengraphen nach Teilgraphen – bestehend aus konventionellen Einzeloperationen – gesucht und diese durch reichhaltige Befehle ersetzt. Zur Graphtransformation setzen wir ein Graphersetzungssystem ein.
Bisher mussten Graphersetzungsregeln manuell geschrieben werden, was sehr zeitaufwändig und fehleranfällig ist. Wir untersuchen deshalb, wie Graphersetzungsregeln
automatisch aus einer Spezifikation, welche das Verhalten von reichhaltigen Befehlen in
einer gängigen Hochsprache beschreibt, gewonnen werden können. Dadurch können neue
reichhaltige Befehle mit wenig Aufwand integriert werden. Wir entwerfen eine geeignete
Spezifikationssprache basierend auf der Hochsprache C und zeigen, wie aus einer solchen Befehls-Spezifikation Graphersetzungsregeln entstehen. Gleichzeitig entwickeln wir
Techniken, um die Trefferwahrscheinlichkeit bei der Mustersuche zu erhöhen und demonstrieren, wie die Auswahl und Anwendung von Graphersetzungsregeln automatisch
gesteuert werden kann.
Schließlich integrieren wir die Optimierung in einen bestehenden Übersetzer und zeigen durch Laufzeittests an realitätsnahen Testprogrammen ihre Leistungsfähigkeit.
i
Inhaltsverzeichnis
1 Einleitung
1.1 Aufgabenstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2 Grundlagen
2.1 Firm und libfirm . . . . . . . . . . . . . . .
2.2 Die Komplexität von reichhaltigen Befehlen .
2.3 Reichhaltige Befehle in Firm . . . . . . . . .
2.4 Transformation von Programmgraphen . . . .
2.5 Zusammenarbeit von Übersetzer und GES . .
2.6 Verwandte Arbeiten . . . . . . . . . . . . . .
2.6.1 Automatische Algorithmenerkennung .
2.6.2 Befehlsauswahl mittels PBQP . . . . .
1
3
4
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
5
6
8
9
10
11
11
13
3 Generierung von Graphersetzungsregeln
3.1 Entwurf der Spezifikationssprache . . . . . . . . . . . . . . . . . .
3.1.1 Entitäten . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.2 Informationen für das Backend . . . . . . . . . . . . . . .
3.1.3 Steuerung der Regelanwendung . . . . . . . . . . . . . . .
3.1.4 Beispiel einer Befehls-Spezifikation . . . . . . . . . . . . .
3.2 Generierung des Mustergraphen . . . . . . . . . . . . . . . . . . .
3.2.1 Analyse des initialen Graphen . . . . . . . . . . . . . . . .
3.2.2 Transformation des initialen Graphen . . . . . . . . . . .
3.2.3 Speicherkante . . . . . . . . . . . . . . . . . . . . . . . . .
3.3 Generierung des Ersetzungsgraphen . . . . . . . . . . . . . . . .
3.4 Regelgenerierung . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.1 Syntax einer Graphersetzungsregel . . . . . . . . . . . . .
3.4.2 Graphmodell . . . . . . . . . . . . . . . . . . . . . . . . .
3.5 Überdeckung mehrerer Grundblöcke . . . . . . . . . . . . . . . .
3.5.1 Phi-Knoten . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.2 Grundblock von Operationen . . . . . . . . . . . . . . . .
3.5.3 Mehrere Register-Stores auf dieselbe Vektor-Komponente
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
15
15
16
17
17
18
19
19
20
21
22
24
24
25
25
25
26
27
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
iii
4 Ersetzungsschritt
4.1 Priorität von Regeln . . . . . . . . . . . . . . .
4.2 Anordnen des reichhaltigen Befehls . . . . . . .
4.2.1 Auffinden der Speichervorgänger . . . .
4.2.2 Grundblock des reichhaltigen Befehls . .
4.3 Analyse einer Passung . . . . . . . . . . . . . .
4.3.1 Speicherabhängigkeit von Musterknoten
4.3.2 Verwendung von Zwischenergebnissen .
4.3.3 Speicherabhängigkeit von Operanden . .
4.3.4 Grundblock von Operanden . . . . . . .
4.3.5 Ausrichtung von Daten . . . . . . . . .
4.4 Entfernen überflüssiger Knoten . . . . . . . . .
4.4.1 Entfernen toten Codes . . . . . . . . . .
4.4.2 Load-Store-Optimierung . . . . . . . . .
4.4.3 Kontrollflussoptimierung . . . . . . . . .
5 Normalisierung und Varianten
5.1 Vortransformationen . . .
5.2 Normalisierung . . . . . .
5.2.1 Vergleiche . . . . .
5.2.2 Adressierungsarten
5.3 Variantenbildung . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
29
29
29
31
33
34
34
34
35
37
37
38
39
39
39
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
41
41
42
42
43
45
6 Algorithmische Steuerung der Regelanwendung
6.1 Überlappung von Passungen . . . . . . . . . .
6.2 Suchbaum . . . . . . . . . . . . . . . . . . . .
6.2.1 Aufbauen des Suchbaums . . . . . . .
6.2.2 Ersetzen der günstigsten Passungen .
6.2.3 Prioritätsklassen . . . . . . . . . . . .
6.3 Musterselektion mittels PBQP . . . . . . . .
6.3.1 PBQP . . . . . . . . . . . . . . . . . .
6.3.2 Änderungen am bisherigen Ansatz . .
6.3.3 Erstellen des PBQP-Graphen . . . . .
6.3.4 Musterauswahl per PBQP am Beispiel
6.3.5 Bewertung . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
47
47
48
49
51
52
53
53
54
55
58
61
.
.
.
.
.
.
.
63
63
65
65
66
67
68
70
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7 Implementierung und Messungen
7.1 Architektur . . . . . . . . . . . . . . . . .
7.2 Messergebnisse . . . . . . . . . . . . . . .
7.2.1 Bewegungsschätzung . . . . . . . .
7.2.2 Skalarprodukt . . . . . . . . . . . .
7.2.3 Reihungselemente begrenzen . . .
7.2.4 Demonstration der Musterauswahl
7.3 Bewertung . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7.4
Weiterführende Ideen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
8 Zusammenfassung und Ausblick
73
1 Einleitung
Reichhaltige Befehle, wie sie von modernen Prozessoren angeboten werden, bleiben von
den verbreiteten Programmiersprachen aufgrund fehlender Sprachkonstrukte, die die
Ausnutzung solcher Befehle erlauben würden, weitgehend unberücksichtigt. Unter den
Begriff reichhaltige Befehle fallen u.A. SIMD-Befehle1 , es sind aber auch noch komplexere Befehle denkbar. Generell verstehen wir unter einem reichhaltigen Befehl ein kleines,
in Hardware gegossenes Programm, das aus mehreren konventionellen Einzelbefehlen
besteht und parallel auf mehreren Daten arbeiten kann. Der Einsatz von reichhaltigen
Befehlen verspricht eine Steigerung der Effizienz im Vergleich zur Ausführung von Einzelbefehlen. Einem Programmierer verbleiben verschiedene Möglichkeiten, um reichhaltige
Befehle auszunutzen:
Programmierung auf Assemblerebene. Mit dieser Methode kann schnell die Übersichtlichkeit und Wartbarkeit der Software verloren gehen.
Übersetzerspezifische Intrinsics sind durch den Übersetzerhersteller definierte Erweiterungen einer Hochsprache. Beim Wechsel der Zielarchitektur oder des Übersetzers
muss ein Programm deshalb, wie auch bei der Assemblerprogrammierung, von
Hand angepasst werden. Bereits vorhandene Programme können nur optimiert
werden, indem der Quellcode manuell umgeschrieben wird.
Eine übersetzerinterne Optimierung ist transparent für den Programmierer. Der Übersetzer entscheidet selbstständig und hardwareabhängig, wann reichhaltige Befehle
einzusetzen sind.
Assemblerprogrammierung und Intrinsics sind für unsere Zwecke nicht geeignet, da
wir den Quellcode portabel halten wollen. Momentan unterstützen die meisten Übersetzer eine oben angesprochene Optimierung nicht vollständig. Einige Übersetzer verwenden sogenannte Vektorisierer, die Vektorbefehle in Schleifen erkennen, Programme
aber nicht außerhalb von Schleifen optimieren und oft bei reichhaltigen Befehlen, deren
Komplexität die von reinen Vektorbefehlen übersteigt, versagen.
In dieser Arbeit stellen wir einen neuartigen Ansatz vor, um einen Übersetzer mit
solch einer Optimierung auszustatten. Optimierungen finden üblicherweise auf der übersetzerinternen Zwischensprache statt. Moderne Zwischensprachen stellen Programme als
1
Single Instruction Multiple Data
1
2
1 Einleitung
Proj P
Load
Proj Is
0x4
Proj M
Add P
Load
Proj Is
Proj P
Optimierung
Vector Load
Proj Vector
VProj 0 Is
Proj M
VProj 1 Is
Proj M
Abbildung 1.1: Optimierung eines Firm Zwischensprachengraphen
gerichteten Graph dar, wobei die Knoten Operationen und die Kanten Datenabhängigkeiten und Kontrollfluss repräsentieren. Da eine Zwischensprache möglichst hardwareunabhängig sein soll, sind in einem Zwischensprachengraphen initial keine Knoten für
hardware-spezifische, reichhaltige Befehle vorhanden. Reichhaltige Befehle können aber
als Teilgraphen, bestehend aus mehreren Einzeloperationen, in Zwischensprachengraphen gefunden werden. Die Aufgabe unserer Optimierung ist es, solche Teilgraphen zu
finden und sie durch neu eingeführte Knoten für reichhaltige Befehle zu ersetzen. Das
Suchen und Ersetzen von Teilgraphen sind Aufgaben, die wir an ein Graphersetzungssystem delegieren können.
Abbildung 1.1 zeigt, wie eine solche Ersetzung in der von uns verwendeten Zwischensprache Firm [27] aussehen könnte. Das linke Programmfragment, das zwei LoadOperationen auf zwei aufeinanderfolgende Speicherstellen ausführt, wird durch das rechte Programmfragment, welches beide Operationen auf einmal durch einen VectorLoadBefehl ausführt, ersetzt. Dadurch wird ein Geschwindigkeitsvorteil erreicht. Beide Programmversionen produzieren dasselbe Ergebnis (fett markiert). Es ist zu beachten, dass
in Firm Datenabhängigkeiten statt Datenfluss verwendet werden. Deshalb sind die
Abhängigkeitskanten rückwärts zu lesen, um die Ausführungsreihenfolge zu erhalten.
Die zur Mustersuche verwendeten Mustergraphen mussten bisher explizit manuell geschrieben oder die Mustersuche sogar ausprogrammiert werden. Diese Methode macht
es sehr aufwändig und fehleranfällig, neue reichhaltige Befehle zu integrieren, da viele
Feinheiten der Zwischensprache beachtet werden mussten. Außerdem ist dazu ein tiefgründiges Fachwissen über die Zwischensprache und das Übersetzer-Backend notwendig,
so dass neue Mustergraphen bisher nur von Experten erstellt werden konnten. Unser
Ansatz überwindet diese Hürde, indem wir Mustergraphen aus einer Spezifikation gewinnen, die das Verhalten von reichhaltigen Befehlen in einer Hochsprache beschreibt.
Dadurch können auch Benutzer ohne Kenntnis der Zwischensprache neue reichhalti-
1.1 Aufgabenstellung
3
ge Befehle spezifizieren. Anstatt die Mustersuche auszuprogrammieren, generieren wir
aus dieser Befehls-Spezifikation automatisch Graphersetzungsregeln für ein Graphersetzungssystem.
1.1 Aufgabenstellung
Die Aufgabe dieser Arbeit ist zu untersuchen, wie solche Graphersetzungsregeln aus einer Befehls-Spezifikation gewonnen und angewendet werden können. Die grundlegende
Idee, Graphersetzungsregeln zur Programmoptimierung einzusetzen, wurde bereits von
Hofmann beschrieben, der ansatzweise die Funktionsfähigkeit der Vorgehensweise zeigt
[15]. Wir übernehmen diesen Grundgedanken, erweitern ihn jedoch mit dem Ziel, eine
lauffähige Optimierung zur automatischen Ausnutzung von reichhaltigen Befehlen auf
der Zwischensprache Firm unter Verwendung eines Graphersetzungssystems zu implementieren und in einen bestehenden Übersetzer zu integrieren.
Wir entwerfen eine Spezifikationssprache basierend auf der Hochsprache C, um das
Verhalten von reichhaltigen Befehlen zu beschreiben. Die Spezifikationssprache soll intuitiv und leicht erweiterbar sein. Außerdem soll es möglich sein, alle Informationen, die
zur automatischen Ausführung der Optimierung nötig sind, zu spezifizieren.
Das Hauptproblem der Implementierung von Hofmann war, dass geringe syntaktische
Abweichungen zwischen Befehls-Spezifikation und dem zu optimierenden Programm dazu führten, dass keine reichhaltigen Befehle mehr gefunden werden konnten. Wir analysieren die Gründe dafür genauer und entwickeln Methoden, um die Trefferwahrscheinlichkeit der Mustersuche zu erhöhen.
Wir untersuchen außerdem, ob eine automatische Integration der neuen reichhaltigen
Befehle in das Übersetzer-Backend möglich ist, um Registerzuteilung zu ermöglichen und
Assemblercode zu erzeugen, was die Implementierung von Hofmann noch nicht leistete.
Die zu entwerfende Spezifikationssprache soll es dem Benutzer ermöglichen, alle dazu
notwendigen, hardware-spezifischen Informationen anzugeben.
Die Regelanwendung war bei der Implementierung von Hofmann noch handgesteuert, d.h. es erfolgte keine automatische Regelauswahl. Hofmann erwähnte zwar schon
die Idee eines Rückrollmechanismus, um bei Bedarf den Graph wieder in den Ausgangszustand zu versetzen oder um mehrere Möglichkeiten der Ersetzung auszuprobieren.
Eine Strategie, um Ersetzungen wieder rückgängig zu machen oder zwischen mehreren
Ersetzungsmöglichkeiten auszuwählen, wird jedoch nicht beschrieben. Wir entwickeln
verschiedene Verfahren, um die Regelauswahl zu automatisieren und die Reihenfolge der
Graphersetzung zu steuern.
Um nicht den Einschränkungen der bestehenden Implementierung unterworfen zu
sein, soll eine komplette Neuimplementierung erfolgen. Die Leistungsfähigkeit unserer
Implementierung soll anhand von Messungen an realitätsnahen Testprogrammen gezeigt
werden.
4
1 Einleitung
1.2 Überblick
In Kapitel 2 werden die Grundlagen der Arbeit vorgestellt und einige notwendige Definitionen getroffen. In Kapitel 3 beschreiben wir, wie aus der Befehls-Spezifikation die
Graphersetzungsregeln entstehen. Wie ein komplexer Befehl in den Zwischensprachengraphen eingefügt wird, nachdem ein Muster gefunden wurde, wird in Kapitel 4 erklärt.
In Kapitel 5 werden Techniken vorgestellt, um die Trefferwahrscheinlichkeit bei der Mustersuche zu erhöhen. Kapitel 6 widmet sich dem Thema, kostengesteuert die billigste
Ersetzung durchzuführen, falls es mehrere Ersetzungsmöglichkeiten geben sollte. In Kapitel 7 werden Details unserer Implementierung offengelegt und die Integration in einen
bestehenden Übersetzer besprochen. Außerdem stellen wir hier unsere Messergebnisse
vor. Kapitel 8 fasst schließlich die Ergebnisse dieser Arbeit zusammen.
2 Grundlagen
In diesem Kapitel gehen wir auf die grundlegenden Ideen ein, auf die diese Arbeit aufbaut. Wir beschrieben die von uns verwendete Zwischensprache und geben Beispiele
für reichhaltige Befehle an. Desweiteren erläutern wir die grobe Architektur unserer
Optimierung und stellen verwandte Arbeiten vor, die ähnliche Ziele wie diese Arbeit
verfolgen.
2.1 Firm und libfirm
Die von uns implementierte Optimierung arbeitet auf der Zwischensprache Firm [27].
Es handelt sich dabei um eine graphbasierte Programmdarstellung in SSA-Form (Single
Static Assignment1 ) [8, 6]. Operationen werden als Knoten eines Programmgraphen dargestellt. Jede Operation verweist durch Datenabhängigkeitskanten auf ihre Operanden.
Die Verwendung von Datenabhängigkeiten statt des Datenflusses ist eine Besonderheit
von Firm. In Anlehnung daran wird in Firm der Steuerfluss umgekehrt dargestellt: Jeder
Grundblock verweist durch Steuerflusskanten auf seine Vorgänger. Ein spezieller Kantentyp ist die Speicherabhängigkeitskante, durch die Speicheroperationen, die möglicherweise
auf denselben Speicherbereich zugreifen, serialisiert werden. In allen Abbildungen dieser
Arbeit verwenden wir Datenabhängigkeiten statt des Datenflusses.
Viele der Knotentypen von Firm wie z.B. Load-, Store-, Mul- oder Add-Knoten sind
selbsterklärend. An dieser Stelle beschreiben wir einige für diese Arbeit wichtige spezielle Knotentypen detaillierter: Da manche Firm-Knoten ein Tupel von Ergebnissen
bereit stellen, verfügt die Zwischensprache über Proj -Knoten, die ein bestimmtes Element des Ergebnistupels auswählen. Proj -Knoten existieren nur virtuell und erzeugen
keinen zusätzlichen Code. Eine Spezialität von SSA-Programmen sind Phi -Operationen.
Phi -Operationen stehen für die Vereinigung alternativer Datenflusspfade und definieren
einen neuen Wert, indem sie genau einen der alternativen Werte gemäß des Steuerflusses auswählen. Statt mehrerer Verweise haben Verwender dieser Werte also nur noch
einen Verweis auf den von der Phi -Operation generierten Wert. Phi -Knoten dienen also
der Aufrechterhaltung der SSA-Eigenschaft, dass eine Verwendung genau eine Definition haben muss und werden vor der Codeerzeugung wieder abgebaut. Schlussendlich sei
noch der Knotentyp Sync erwähnt, der eingesetzt wird, falls mehrere parallele Speicherabhängigkeitsketten zu einer vereinigt werden sollen.
1
deutsch: Statische Einmalzuweisung
5
6
2 Grundlagen
In Firm sind auch die Typen der Werte des Programms zugänglich. Der primitive
Typ des Ergebnisses einer jeden Operation ist an deren Knoten durch ein sogenanntes
Mode-Attribut annotiert. So steht beispielsweise Is für Signed Integer, T für Tupel oder
M für Memory. Dadurch ist auch implizit bekannt, welche Bitbreite ein Ergebniswert
hat.
Firm stellt den Anspruch, von allen nicht essentiellen Abhängigkeiten zu abstrahieren [12] und so die Befehlsanordnung und Befehlsauswahl nicht implizit vorzugeben.
Erst das Übersetzer-Backend soll die Befehle in den Grenzen, die durch den Steuerfluss, Speicherabhängigkeit und Datenabhängigkeit gegeben sind, anordnen. Allerdings
heißt das nicht, dass für unterschiedliche Anordnungen der Befehle im Quellprogramm
immer derselbe Zwischensprachengraph entsteht. Beispielsweise könnten für einen arithmetischen Ausdruck die beiden semantisch äquivalenten aber syntaktisch verschiedenen
Bäume (a + b) + c und a + (b + c) aufgebaut werden. Wichtig ist jedoch, dass es von der
Programmiersprache abhängt, ob wir arithmetische Ausdrücke derart umordnen dürfen.
Ein Beispiel eines Firm-Programms zeigt Abbildung 2.1. Der Graph entspricht der
Zwischendarstellung des folgenden C Programms, welches zwei Vektoren komponentenweise addiert und uns in den nächsten Abschnitten als Beispiel dienen wird:
void VectorAdd(int *a, int *b, int *c)
{
c[0] = a[0] + b[0];
c[1] = a[1] + b[1];
}
libfirm [21] ist eine leistungsfähige Implementierung der Zwischensprache Firm. Es
handelt sich um eine Bibliothek, die die nötigen Funktionen bereitstellt, um Zwischensprachengraphen aufzubauen, zu verwalten und zu manipulieren.
2.2 Die Komplexität von reichhaltigen Befehlen
Moderne Prozessoren bieten reichhaltige Befehlssätze an, deren Befehle sich nicht mehr
eindeutig nach Flynn [11] kategorisieren lassen: Die klassische Einteilung in SISD (Single Instruction Single Data) und SIMD (Single Instruction Multiple Data) reicht nicht
mehr aus, da reichhaltige Befehle viele verschiedene Operationen parallel ausführen.
Generell arbeitet ein reichhaltiger Befehl ein und dasselbe kleine Hardware-Programm
– bestehend aus mehreren Einzeloperationen und möglicherweise mit verzweigtem Steuerfluss – parallel auf mehreren Daten ab. Vielen reichhaltigen Befehlen ist gemein, dass
sie zusätzlich zu skalaren Daten auch Vektordaten verarbeiten können.
Als Beispiel dient uns hier der SSE2-Befehlssatz2 der Intel IA32 Architektur [18].
Dieser Befehlssatz stellt SIMD-Befehle zur Verfügung, um komplette Vektoren aus dem
Speicher zu laden oder zu speichern (movdqu), und um arithmetische Operationen auf
2
Streaming SIMD Extensions
2.2 Die Komplexität von reichhaltigen Befehlen
7
Abbildung 2.1: Firm-Graph einer Funktion, die zwei Vektoren komponentenweise addiert. Der markierte Teilgraph lädt einen Vektor aus dem Speicher.
8
2 Grundlagen
Anwendung
Matrix Multiplikation zweier 6x6 Matrizen
Matrix Inversion 4x4
Wiener Filter (Reduktion von Rauschen)
Maximum-/Minimum-Suche
Kosinus Transformation
3D Transformation
Upsampling
Speedup
2,1
4,03
n/a
n/a
1,70
2,01
n/a
Befehlssatz
SSE
SSE
SSE
SSE
SSE, SSE2
SSE2
SSE
Quelle
AP-930
AP-928
AP-807
AP-804
AP-945,922
AP-939
AP-822
Tabelle 2.1: Speedup durch Ausnutzen reichhaltiger Befehle
Vektorelementen durchzuführen (addps, subps, mulps). Als typisches Beispiel eines reichhaltigen Befehls, der über die reine Vektorverarbeitung hinausgeht und verzweigten
Steuerfluss enthält, sei der psadbw -Befehl erwähnt, der die Summe der absoluten Differenzen (SAD) über 16 Vektorkomponenten gleichzeitig berechnen kann. Das folgende
C-Programm könnte also durch diesen reichhaltigen Befehl ersetzt werden:
unsigned char a[16], b[16];
int result = 0, i;
...
for(i = 0; i < 16; i++) {
if(a[i] > b[i])
result += a[i] - b[i];
else
result += b[i] - a[i];
}
Viele Befehle des SSE-Befehlssatzes arbeiten auf dem XMM-Registersatz. Dabei handelt es sich um spezielle, 128 Bit breite Register. Die Interpretation des Registerinhaltes
hängt vom jeweiligen Befehl ab. Beispielsweise kann der Inhalt eines XMM-Registers
als Vektor zu vier Integer Komponenten vom Typ Integer (32 Bit) ausgelegt werden.
Register dieser Art bezeichnen wir allgemein als Spezialregister.
Reichhaltige Befehle werden ausgenutzt, um einen Geschwindigkeitsvorteil gegenüber
dem Ausführen von Einzelbefehlen zu erreichen. Große Erfolge wurden dabei im Multimediabereich verzeichnet: Durch von Hand optimierte Programme wurde eine hohe
Geschwindigkeitssteigerung erreicht. In Tabelle 2.1 sind einige ausgewählte Beispiele aus
den Intel Application Notes [16] mit den erzielten Speedups (soweit von Intel angegeben)
dargestellt.
2.3 Reichhaltige Befehle in Firm
Reichhaltige Befehle haben initial keinen entsprechenden Knotentyp in Firm, können
aber als Teilgraphen, bestehend aus mehreren Einzeloperationen, in Firm-Graphen ge-
2.4 Transformation von Programmgraphen
9
funden werden. In Abbildung 2.1 ist ein solcher Teilgraph, mit der Semantik eines VektorLadebefehls, als dunkel markierte Knoten zu sehen. Die Knoten Load[Is] 595:28 und
Load[Is] 985:19 laden zwei Integerwerte an zwei aufeinanderfolgenden Speicherstellen.
Die geladenen Werte entsprechen also den Komponenten eines Vektors. Die restlichen
Knoten des Teilgraphen dienen der Adressberechnung.
Dieser Teilgraph könnte durch einen speziellen, hardwarespezifischen Knoten mit derselben Semantik ersetzt werden, der vom Backend direkt in den entsprechenden Assemblerbefehl umgesetzt wird. Beim Ausführen des Programms hätte der Prozessor weniger
Befehle zu dekodieren, die Adressrechnung würde nur einmal erfolgen und das Programm
würde von der Parallelverarbeitung des neuen Befehls profitieren.
Firm erlaubt es zwar, zur Laufzeit neue Knotentypen einzuführen. Es ist aber
zunächst unvereinbar mit Firm, neue hardwarespezifische Knoten einzufügen, da es sich
um eine Zwischensprache handelt, die möglichst hardwareunabhängig sein soll. Hier hilft
uns die Implementierung unseres aktuellen Backends für die IA32 Architektur weiter [28].
Im Gegensatz zu herkömmlichen Backend-Implementierungen arbeitet dieses Backend
ebenfalls graphbasiert und erzeugt aus dem hardware-unabhängigen Zwischensprachengraphen einen hardware-spezifischen Backendgraphen. Da unsere Optimierung in einer
späten Phase angesiedelt ist, kann das Einfügen hardware-spezifischer Knoten als Teil
des Backends angesehen werden.
2.4 Transformation von Programmgraphen
Um einen Firm-Graph durch Einfügen von reichhaltigen Befehlen zu optimieren, muss
der ursprüngliche Graph transformiert werden. Die Transformation besteht darin, zu optimierende Teilgraphen von Programmgraphen aufzufinden und sie durch eine optimierte
Version zu ersetzen. Dies sind Aufgaben, auf die zu erledigen Graphersetzungssysteme
spezialisiert sind und wir deshalb an ein solches delegieren können.
Definition 1. Für uns ist ein Graphersetzungssystem (GES) ein Tupel
Graphersetzungssystem := (C, G)
aus Graphersetzungsregeln C und einem Arbeitsgraphen G. Eine Graphersetzungsregel p
ist eine abstrakte Darstellung einer Graphtransformation und hat die Form
r
p:L−
→R
Eine Graphersetzungsregel mit SPO-Semantik [9] besteht aus dem Mustergraph L, dem
Ersetzungsgraph R und einem Homomorphismus r zwischen L und R. L beschreibt den
Teilgraphen, der im Arbeitsgraphen gefunden werden soll. Wird ein Teilgraph gefunden,
bezeichnen wir diesen als Passung. Nach erfolgreichem Finden einer Passung legen r
und R nun fest, welche Knoten oder Kanten aus dem Arbeitsgraphen entfernt, welche
neu hinzukommen oder welche retypisiert werden sollen. Weitere Details sind in [13] zu
finden.
10
2 Grundlagen
Der Einsatz eines Graphersetzungssystems entbindet uns von der Aufgabe, die Mustersuche und die Programmtransformation auszuprogrammieren. Stattdessen generieren
wir Regeln für ein Graphersetzungssystem, was weniger aufwändig und weniger fehleranfällig ist.
Für diese Arbeit setzen wir das Graphersetzungssystem GrGen [13] ein. GrGen wurde am Institut für Programmstrukturen und Datenorganisation entwickelt und arbeitet
sehr effizient. Es besitzt eine einheitliche und umfangreiche Syntax für Graphersetzungsregeln. GrGen führt Syntax- und Semantikprüfungen von Graphersetzungsregeln durch,
wodurch Fehler frühzeitig erkannt werden können. Es verfügt über eine leicht erweiterbare Spezifikation des verwendeten Graphmodells, was für diese Arbeit wichtig ist, da
für die Optimierung neue Knotentypen und Knotenattribute eingeführt werden müssen.
GrGen ist optimiert, gesuchte Muster schnell zu finden, indem es Arbeitsgraphen analysiert, bevor es mit der Suche beginnt. Auch diese Eigenschaft kommt uns zugute, denn
eine Optimierung soll die Übersetzungszeit nicht übermäßig verlängern.
Der modulare Aufbau von GrGen erlaubt es, verschiedene Backends zu verwenden.
Ein Backend für die Graphverwaltung libfirm existiert bereits und wurde von Batz
implementiert [3]. Dadurch kann die Graphersetzung direkt auf der übersetzerinternen
Zwischendarstellung stattfinden, so dass die Zwischensprachengraphen nicht erst in ein
geeignetes Format exportiert werden müssen. Falls die Optimierung mit einer anderen
Firm-Implementierung durchgeführt werden soll, ist es so nicht nötig, den Regelgenerator neu zu schreiben. Es besteht die Möglichkeit, ein anderes, dafür vorgesehenes
GrGen-Backend zu verwenden.
2.5 Zusammenarbeit von Übersetzer und GES
Wir demonstrieren nun, wie Übersetzer und Graphersetzungssystem zusammenarbeiten.
Der grobe Ablauf der Optimierung ist in Abbildung 2.2 zu sehen. Die Optimierung
besteht aus zwei Schritten:
Im ersten Schritt (dunkelgrau markiert) werden die zur Optimierung benötigten
Graphersetzungsregeln generiert. Dazu wird die Befehls-Spezifikation der reichhaltigen
Befehle an den Übersetzer übergeben. Das Übersetzer-Frontend wandelt die BefehlsSpezifikation in Zwischensprachengraphen um, welche vom Regelgenerator in Graphersetzungsregeln übersetzt und in einer Regeldatenbank gespeichert werden. Der Regelgenerator ist eine Erweiterung des Übersetzers und erzeugt zusätzlich zu den Graphersetzungsregeln automatisch auch die Spezifikation der neu hinzugekommenen Befehle
für das Backend. Dieser erste Schritt muss nur dann durchgeführt werden, wenn neue
reichhaltige Befehle hinzukommen. Ist die Regeldatenbank einmal aufgebaut, kann diese
zur Optimierung von Programmen genutzt werden.
Die eigentliche Optimierung geschieht im zweiten (weiß markierten) Schritt. Das zu
optimierende Quellprogramm wird dem Übersetzer übergeben, welcher durch das Fron-
2.6 Verwandte Arbeiten
11
C Spezifikation
reichhaltiger Befehl
Quellprogramm
Frontend
Zwischensprache
Übersetzer
Regelgenerator
Regel−
Datenbank
GES
Backend
Regelgenerierungsschritt
Optimierter Assemblercode
Optimierungsschritt
Beide Schritte
Abbildung 2.2: Grober Ablauf der Optimierung.
tend Zwischensprachengraphen erzeugt. Auf diesen Zwischensprachengraphen arbeitet
nun das Graphersetzungssystem GES, welches Bestandteil des Übersetzers ist [3] und
sich der erzeugten Graphersetzungsregeln aus der Regeldatenbank bedient. Anschließend
erzeugt das Backend aus dem optimierten Zwischensprachengraphen unter Zuhilfenahme der in der Regeldatenbank vorhandenen, hardware-spezifischen Zusatzinformationen
optimierten Assemblercode.
2.6 Verwandte Arbeiten
Im Folgenden beschreiben wir Arbeiten, die ein ähnliches Ziel verfolgen oder deren Methoden auf das von uns zu lösende Problem angewandt werden können.
2.6.1 Automatische Algorithmenerkennung
Metzger und Wen [23] beschreiben ausführlich einen Ansatz, um komplette Algorithmen
im sogenannten Computational Kernel eines Programms aufzufinden. Unter Computational Kernel wird der Teil des Programms verstanden, der einen Großteil der Rechenzeit
benötigt und deshalb optimiert werden soll. Die Optimierung besteht darin, langsame
12
2 Grundlagen
Programmteile durch einen Aufruf einer schnelleren, optimierten Bibliotheksfunktion
mit derselben Semantik zu ersetzen.
Die Optimierung findet auf einer Zwischensprache in Baumform, die für den Convex
Application Compiler [1] entworfen wurde, statt. Die verwendeten Datenstrukturen sind
• Der Control Tree, der die Anweisungen (engl. Statements) sowie den Steuerfluss
des Programms inklusive Schleifen enthält. Ausdrücke werden durch Expression
Trees repräsentiert.
• Ein separater Graph zur Darstellung des Datenflusses zwischen Statements.
• Der i-val Tree, der Schleifenvariablen sowie deren Abhängigkeiten darstellt.
• Parameterlisten von Unterprogrammen.
Die Idee besteht darin, den Computational Kernel auf Zwischensprachenebene
auf vordefinierte Muster aus einer Musterdatenbank abzusuchen. Um diese Mustersuche zu beschleunigen, werden Muster und die Zwischendarstellung des Computational Kernel auf eine einheitliche Form (die sog. Canonical Form) gebracht. D.h. die
Zwischensprachen-Bäume werden umgeordnet, indem Knoten durch eine Nummer kodiert und dann sortiert werden. Dieses Vorgehen soll die Anzahl der Freiheitsgrade bei
der Mustersuche einschränken und die Mustersuche darauf reduzieren, die Bäume Knoten für Knoten zu vergleichen.
Metzger und Wen beschreiben außerdem, wie Gruppen von Anweisungen aus einem
Programm extrahiert werden, um sie mit Mustern aus der Musterdatenbank zu vergleichen. Dazu werden Eigenschaften der Anweisungsgruppe wie Knotentypen und die
Topologie des Datenabhängigkeitsgraphen herangezogen. Für den Fall, dass verschiedene
zu ersetzende Anweisungsgruppen gefunden wurden, wird beschrieben, wie eine Auswahl
getroffen werden kann, um den größtmöglichen Kostenvorteil zu erhalten.
Im Vergleich mit unserem Ansatz besteht dieselbe Grundidee, eine Datenbank mit
vordefinierten Mustern zu unterhalten, die im Zwischensprachengraphen des zu optimierenden Programms gesucht werden. Metzger und Wen erwähnen sogar schon die Idee,
die Muster automatisch zu erzeugen, indem man einen Algorithmus in einer Hochsprache spezifiziert. Dadurch kann selbst ein Benutzer des optimierenden Übersetzers neue
Muster hinzufügen, ohne selbst über die interne Zwischensprache Bescheid wissen zu
müssen. Auch der Ansatz, Muster und zu optimierendes Programm u.A. durch Standardoptimierungen näher an eine einheitliche Form zu bringen, ist in beiden Arbeiten
ähnlich.
Unterschiedlich ist die Art der verwendeten Datenstrukturen auf Zwischensprachenebene. Firm hat Graphstruktur im Unterschied zur Baumstruktur mit Anweisungslisten
der oben erwähnten Zwischensprache. Daten- und Kontrollfluss, sowie Ausdrücke, Anweisungen und Funktionsparameter sind in unserem Fall in einem gemeinsamen Graphen enthalten. Da Firm-Programme in SSA Form sind, spielen Variablennamen auf
2.6 Verwandte Arbeiten
13
Zwischensprachenebene keine Rolle. Im Unterschied zu Metzger und Wen müssen wir
das Zusammenspiel von Variablen nicht erst durch Umordnen von Anweisungslisten und
Umbenennen prüfen. Alle möglichen Anweisungsanordnungen sind durch den Datenfluss
in Firm gegeben. Metzger und Wen stellen den Anspruch, komplette Algorithmen inklusive Schleifen in einem Programm wiederzufinden, während wir DAGs (Directed Acyclic
Graph) in Programmgraphen auffinden wollen, die dem Verhalten reichhaltiger Befehle
entsprechen. Unsere Optimierung ist mehr Backend orientiert, da wir gefundene Muster nicht durch einen Funktionsaufruf, sondern durch schnelle Assemblerbefehle ersetzen
wollen. Dadurch haben wir auch auf Spezifikationsebene mit Hardwareeigenschaften wie
die benutzen Registerklassen etc. zu tun. Unsere Optimierung muss teilweise in das
Compilerbackend integriert werden. Wir benutzen ein modernes GES, um Muster in
Zwischensprachen aufzufinden. Schließlich kann sich unsere Arbeit auf eine Implementierung stützen, während Metzger und Wen nur den Normalisierungsprozess, nicht aber
Mustergenerierung, Mustersuche und Ersetzungsschritt implementiert haben.
2.6.2 Befehlsauswahl mittels PBQP
Jakschitsch verwendet einen PBQP-Löser (Partitioned Boolean Quadratic Problem), um
die kostengünstigste Auswahl von Assemblerbefehlen für ein Firm-Programm zu treffen
[19]. Ein PBQP lässt sich bildlich als Graph darstellen, an dessen Knoten kostenbehaftete Alternativen existieren (dargestellt durch Kostenvektoren). An den Kanten werden
Übergangskosten zwischen Alternativen der beiden verbundenen Knoten als Kostenmatrizen annotiert. Das Problem der Befehlsauswahl wurde erstmals von Eckstein, König
und Scholz auf ein PBQP abgebildet [10] und von Jakschitsch erweitert. Wir gehen
später noch genauer auf dieses Befehlsauswahlverfahren ein und zeigen, wie PBQP zur
kostengesteuerten Auswahl von Ersetzungen verwendet werden kann.
14
2 Grundlagen
3 Generierung von Graphersetzungsregeln
Die zur Optimierung verwendeten Graphersetzungsregeln können aufgrund der Komplexität von reichhaltigen Befehlen und der Zwischensprache sehr groß werden. Um die
Regeln nicht von Hand schreiben zu müssen, werden sie ausgehend von einer BefehlsSpezifikation in der Hochsprache C generiert. Das erleichtert das Hinzufügen von neuen
reichhaltigen Befehlen erheblich. In diesem Kapitel entwerfen wir eine Spezifikationssprache basierend auf C und beschreiben die Transformation der Befehls-Spezifikation
in Graphersetzungsregeln. Dabei gehen wir auf die notwendigen Informationen ein, die
die Spezifikation beinhalten muss, um eine Optimierung durchführen und ausführbaren
Code generieren zu können. Wir beschreiben außerdem, wie aus dem initialen Zwischensprachengraphen Muster- und Ersetzungsgraphen entstehen.
3.1 Entwurf der Spezifikationssprache
Wir verändern und erweitern die Spezifikationssprache, wie sie von Hofmann [15] beschrieben wurde, um sie vielseitiger und mächtiger zu gestalten. Die alte Art der Spezifikation war nicht ausreichend, da sie es nicht erlaubte, neben dem reinen Verhalten
weitere Eigenschaften eines reichhaltigen Befehls anzugeben. So konnte z.B. nicht festgelegt werden, in welchen Registerklassen ein reichhaltiger Befehl seine Operanden erwartet. Eine automatische Registerzuteilung war so nicht möglich. Außerdem war die
Spezifikation nicht intuitiv genug. Speicher- und Registerzugriffe wurden während der
Verhaltensbeschreibung unterschiedlich behandelt, was zu Verwirrung bei der Spezifikation führte. Diese Probleme beleuchten wir im Folgenden genauer. Zunächst stellen wir
fest, dass außer dem reinen Verhalten eines reichhaltigen Befehls weitere Informationen
vom Benutzer benötigt werden. Da wären z.B. die vom Befehl verwendeten Entitäten,
die Operanden oder Ergebnis des Befehls darstellen, oder der zu emittierende Assemblercode. Um dies in einer einheitlichen Form spezifizieren zu können, führen wir eine
neue Schreibweise ein: Wir gewinnen die gewünschten Informationen aus dem Aufruf
einer vordefinierten Funktion. Beispielsweise definiert der Aufruf
float *MyOp = Operand_0(attributes);
die Variable MyOp, mit deren Hilfe sich nun das Verhalten des Befehls beschreiben lässt.
Durch die Funktionsparameter attributes lassen sich der Variablen weitere Attribute zuordnen, auf die wir im nächsten Abschnitt zu sprechen kommen. Dieser Ansatz ist leicht
erweiterbar, da sich so nicht nur die Entitäten, sondern beliebige weitere Eigenschaften
eines reichhaltigen Befehls spezifizieren lassen.
15
16
3 Generierung von Graphersetzungsregeln
3.1.1 Entitäten
Jeder reichhaltige Befehl arbeitet auf bestimmten Entitäten wie Registern oder Speicherbereichen, die wir in der Befehls-Spezifikation durch Variablen modellieren. Wir müssen
die Möglichkeit bieten, solche Variablen zu definieren und erlauben, die Variablen mit
Attributen zu versehen. Durch Auslesen der Attributwerte müssen folgende Fragen beantwortet werden können:
1. Nimmt die Variable einen Operanden oder das Ergebnis des reichhaltigen Befehls
auf?
2. Repräsentiert die Variable einen Vektor oder einen skalaren Wert?
3. Befindet sich der Wert im Hauptspeicher oder in einem Register?
4. In welcher Registerklasse wird der Wert übergeben?
Eine Variable wird durch einen Funktionsaufruf Operand n() oder Result() definiert.
Diese Funktionen haben einen Zeiger als Ergebnis, mit dessen Hilfe das Verhalten des
reichhaltigen Befehls beschrieben werden kann. Die Attribute der Variablen werden als
Funktionsparameter angegeben:
type *MyOp = Operand_n(char *kind, char *location, char *register_class);
oder
type *MyRes = Result(char *kind, char *location, char *register_class);
Die oben geforderten Informationen gewinnen wir wie folgt:
1. Der Name der Funktion ist bedeutungstragend. Operand n bedeutet, dass die Variable MyOp den n. Quelloperanden des reichhaltigen Befehls repräsentiert. Result
bedeutet, dass die Variable MyRes den Zieloperanden des reichhaltigen Befehls darstellt. Der primitive Typ type und der Name der Variablen kann frei vom Benutzer
festgelegt werden.
2. Der Funktionsparameter kind darf die Werte ”vector” oder ”scalar” annehmen
und gibt an, ob die Variable einen Vektor oder einen skalaren Wert repräsentiert.
3. Der Parameter location legt durch die Werte ”register” oder ”memory” fest, ob
sich der Wert der Variablen in einem Register oder im Speicher befindet.
4. Die Registerklasse wird durch den Parameter register class angegeben und entspricht dem Namen einer im Backend deklarierten Registerklasse. Beispielsweise
kommen hier Werte wie ”xmm” für die XMM Registerklasse oder ”gp” für die Allgemeinregisterklasse (engl. General Purpose) in Frage. Werte im Speicher erfordern
ebenfalls die Angabe einer Registerklasse, da die Speicheradresse im Register übergeben wird.
3.1 Entwurf der Spezifikationssprache
17
3.1.2 Informationen für das Backend
Um Assemblercode ausgeben zu können, werden einige Informationen über den reichhaltigen Befehl an das Backend weitergereicht. In Anlehnung an die Schreibweise zur
Spezifikation der Entitäten, werden die Informationen für das Backend ebenfalls durch
den Aufruf vordefinierter Funktionen spezifiziert:
Der zu emittierende Assemblercode wird als Parameter der Funktion
Emit(char *assembler_template);
angegeben. Der Parameter assembler template besteht aus dem Namen des zu emittierende Assemblerbefehls sowie den Platzhaltern ”%Sn ” für den n. Quelloperanden
und ”%Dn ” für den n. Zieloperanden. Die Platzhalter werden vom Backend durch die
tatsächlich zugeteilten Register ersetzt. Es ist auch möglich, mehrere Assemblerbefehle
– getrennt durch einen Zeilenumbruch – anzugeben.
Emit(".movdqu %S0, %D0");
Außerdem müssen wir dem Backend Bescheid geben, falls ein reichhaltiger Befehl weitere
Register außer seinem Zielregister überschreibt. Das passiert beispielsweise beim Umkopieren von einzelnen Vektorkomponenten aus XMM Registern in Allgemeinregister mit
SSE2 Befehlen. Durch den Aufruf der Funktion
Destroys(char *destroyed_register);
kann dieser Umstand spezifiziert werden. Der Parameter destroyed register kann die
Werte ”in rn ” annehmen. Damit ist gemeint, dass der Befehl zusätzlich das Register
überschreibt, in dem der n. Quelloperand übergeben wurde.
3.1.3 Steuerung der Regelanwendung
Wie wir später sehen werden, ist die Reihenfolge der Anwendung der generierten Graphersetzungsregeln wichtig. Dazu bieten wir dem Benutzer die Möglichkeit, jeden reichhaltigen Befehl einer bestimmten Prioritätsklasse zuzuordnen. Um dies zu spezifizieren,
ist der Aufruf der Funktion
Priority(int priority_class);
notwendig, wobei der Parameter priority class die Nummer der Prioritätsklasse
angibt. Regeln in einer niedrigeren Prioritätsklasse werden früher ausgeführt als Regeln
in einer höheren Prioritätsklasse.
Durch den Aufruf der Funktion
CostSavings(int savings);
kann festgelegt werden, welche Kostenersparnis ein reichhaltiger Befehl im Vergleich
zur Ausführung von Einzelbefehlen hat. Die spezifizierten Kosten werden später bei der
Musterauswahl (Kapitel 6) eingesetzt.
18
3 Generierung von Graphersetzungsregeln
#include <rich_instructions_opt.h>
void addps(void)
{
// Definitionsteil
float *op0 = Operand_0("vector", "memory", "gp");
float *op1 = Operand_1("vector", "register", "xmm");
float *result = Result("in_r1");
Emit("addps %S0, %S1");
Priority(3);
CostSavings(15);
// Verhalten des reichhaltigen Befehls
result[0] = op0[0] + op1[0];
result[1] = op0[1] + op1[1];
result[2] = op0[2] + op1[2];
result[3] = op0[3] + op1[3];
}
Abbildung 3.1: Spezifikation des Befehls addps
3.1.4 Beispiel einer Befehls-Spezifikation
Nachdem wir die Spezifikationsmöglichkeiten detailliert besprochen haben, präsentieren
wir nun eine Beispielspezifikation. Die Spezifikation von mehreren reichhaltigen Befehlen
besteht aus mehreren Funktionen, wobei jede Funktion genau einen Befehl beschreibt.
Jede Funktion erhält einen beliebigen, aber eindeutigen Namen. So können, falls nötig,
für einen Befehl mehrere Verhaltensbeschreibungen angegeben werden.
Als Beispiel zeigt Abbildung 3.1 eine Spezifikation des Befehls addps des SSE2 Befehlssatzes, der zwei Vektoren zu jeweils vier Komponenten addiert. Die Komponenten
sind vom Typ Fließkomma mit einfacher Genauigkeit (Float). Zu Beginn des Funktionsrumpfes werden die beiden Operanden op0 und op1 definiert. In beiden Fällen handelt
es sich um Vektoren (im Gegensatz zu Skalaren), wobei sich op0 im Speicher befindet
und op1 in Registern des Registersatzes xmm. Die einzelnen Vektorkomponenten haben
den Datentyp float. Desweiteren legt der Aufruf der Funktion Result fest, dass der Befehl sein Ergebnis im Register ”in r1” speichert, d.h. in dem Register, in dem auch der
Quelloperand op1 liegt. Alternativ könnte der Befehl sein Ergebnis im Speicher ablegen.
Als nächstes wird der zu produzierende Assemblercode als Parameter der Funktion Emit
angegeben. Schließlich wird durch Priority(3) dem Befehl die Prioritätsklasse 3 und
durch CostSavings(15) die Kosteneinsparung 15 zugewiesen, wodurch die Reihenfolge,
in welcher die Graphersetzungsregeln zur Anwendung kommen, beeinflusst wird.
Nach diesen einleitenden Definitionen wird das Verhalten des reichhaltigen Befehls
unter Verwendung der im Definitionsteil angelegten Variablen beschrieben. Auf Variablen, die einen Registerinhalt repräsentieren, kann dabei wie auf normale Variablen oder
3.2 Generierung des Mustergraphen
19
Reihungen im Speicher zugegriffen werden. In diesem Beispiel werden zwei Vektoren
komponentenweise addiert und das Ergebnis im festgelegten Zielregister abgespeichert.
3.2 Generierung des Mustergraphen
Da nun bekannt ist, wie reichhaltige Befehle in C spezifiziert werden können, beschreiben wir jetzt, wie aus der Befehls-Spezifikation der Mustergraph generiert wird. Dazu
müssen wir zunächst die Befehls-Spezifikation in einen Zwischensprachengraphen umwandeln. Einen initialen Zwischensprachengraphen erhalten wir, indem wir die BefehlsSpezifikation des reichhaltigen Befehls dem unmodifizierten Übersetzer-Frontend übergeben. Das ist möglich, denn für die Spezifikation wurden nur Sprachelemente der Programmiersprache C verwendet, so dass das Frontend nicht speziell angepasst werden
muss. Der vom Frontend gelieferte Graph ist jedoch nicht direkt als Mustergraph geeignet. Zum Einen enthält er Knoten, die im Mustergraph nicht erwünscht sind (z.B.
Start- und End-Knoten) und zum Anderen muss der initiale Graph teilweise transformiert und normalisiert werden. Um diese Transformation durchzuführen, müssen die
Informationen, die der Benutzer auf Hochsprachenebene spezifiziert hat, im initialen
Graph wiedergefunden werden. Dazu analysieren wir den initialen Graphen.
3.2.1 Analyse des initialen Graphen
Insbesondere Load- und Store-Knoten haben im initialen Graphen eine vom Arbeitsgraphen, in dem wir die Muster finden wollen, etwas abweichende Bedeutung. Im Arbeitsgraphen stellen Load- und Store-Knoten Speicherzugriffe dar, im initialen Graphen
werden dadurch auch Registerzugriffe modelliert, abhängig davon, auf welche Variable
ein Speicherbefehl zugreift. Greift eine Load- bzw. Store-Operation im Mustergraphen
auf das Abbild eines Registers zu, bezeichnen wir diese als Register Load bzw. Register
Store.
Aufgrund der Grapheigenschaft von Firm können wir die Bedeutung von Speicheroperationen mit geringem Aufwand zurückgewinnen, indem wir die Datenabhängigkeitskanten ausgehend vom betreffenden Load- oder Store-Knoten absuchen. Für jedes Load
und Store existiert ein Datenflusspfad zur Definition der Variablen, auf die zugegriffen
wird. An dieser Stelle können wir die benötigten Informationen auslesen. Abbildung 3.2
zeigt ein generisches Muster, das im initialen Graphen vorzufinden ist: Über die Adresskante des Load- oder Store-Knotens ist ein optionaler Add-Knoten und über über einen
Proj -Knoten direkt ein Call-Knoten erreichbar, der die Definition einer Variablen darstellt. Die Vorgänger des Call-Knotens sind die Funktionsparameter der aufgerufenen
Funktion und stellen somit direkt die Attribute dar, die der Benutzer der entsprechenden Variablen zugeordnet hat. Durch diese Attribute können wir entscheiden, ob es sich
um eine Register- oder Speicheroperation handelt. Um bei der Mustertransformation
(siehe Abschnitt 3.2.2) schnell auf diese Attribute zurgreifen zu können, annotieren wir
20
3 Generierung von Graphersetzungsregeln
SymC
...
SymC
Call
Proj P
Const
Add
Load
Abbildung 3.2: Generisches Muster, um Analyseinformationen über Load und Store
Knoten zu gewinnen.
sie an jedem Load- und Store-Knoten.
Auf ähnliche Weise werden auch die restlichen, im Definitionsteil festgelegten Informationen, extrahiert. Der zu emittierende Code, die Prioritätsklasse des Befehls oder die
zusätzlich überschriebenen Register können aus den Parametern derjenigen Call-Knoten
extrahiert werden, die dem jeweiligen Funktionsaufruf entsprechen.
3.2.2 Transformation des initialen Graphen
Da wir jetzt zwischen Register- und Speicheroperationen unterscheiden können, kann
mit den gewonnenen Informationen der initiale Graph zum Mustergraph transformiert
werden. Zunächst kümmern wir uns um Register Load- und Register Store-Knoten.
Wenn eine Register Load-Operation mit einem gewissen Offset auf die Basis eines
Registers zugreift, hat dies die Bedeutung, dass eine einzelne Vektorkomponente aus einem Spezialregister geladen wird. Um solche Zugriffe auf Vektorkomponenten im Graph
explizit sichtbar zu machen, ersetzen wir Register Load-Knoten durch die in von Hofmann [15, Abschnitt 4.4] eingeführten VProj -Knoten (Projektion einer Komponente aus
einem Vektor). Damit steht ein Knotentyp zur Verfügung, um eine Vektorkomponente
eines Vektorregisters auszuwählen. Die Nummer der Vektorkomponente wird durch ein
Indexattribut VProj Number angegeben. Das Indexattribut berechnen wir durch
VProj Number =
Offset
SizeOfMode
(3.1)
wobei mit Offset der Versatz zur Basis des Vektors gemeint ist und mit SizeOfMode
die Breite des Modes der zu ladenden Daten in Byte. Der Ansatz, VProj -Knoten mit
in den Mustergraph aufzunehmen, setzt vorraus, dass VProj -Knoten ebenfalls im Arbeitsgraphen vorkommen. Sie werden durch vorangegangene Graphersetzungen in den
Arbeitsgraphen eingefügt.
3.2 Generierung des Mustergraphen
21
Register Store-Operationen legen ein Endergebnis des reichhaltigen Befehls in einem
(Teil-)Register ab. Da dieser Vorgang im Arbeitsgraphen nicht explizit vorkommt, nehmen wir Register Store-Knoten nicht in den Mustergraph mit auf; sie werden zunächst
ignoriert. Die Knoten, die Endergebnisse des reichhaltigen Befehls darstellen, kommen
im Arbeitsgraphen jedoch vor und können im Mustergraphen als Vorgänger des Register Store-Knotens gefunden werden kann. Dieser Vorgängerknoten wird erst später bei
der Generierung des Ersetzungsgraphen interessant, da sein Ergebnis nun durch den
reichhaltigen Befehl berechnet werden soll (Siehe Abschnitt 3.3).
Ebenfalls nicht mit in den Mustergraph mit aufgenommen werden alle Knoten, die
nicht zur Verhaltensbeschreibung des reichhaltigen Befehls gehören. Dazu gehören die
Knoten des Definitionsteils sowie Start- und End-Knoten. Dazu beschneiden wir den
initialen Graphen um diese Knoten.
3.2.3 Speicherkante
Ein Nachteil der Implementierung von Hofmann [15] war, dass geringe Abweichungen von
Spezifikation und zu optimierendem Programm dazu führten, dass reichhaltige Befehle
nicht gefunden wurden. Spezifizierte beispielsweise der Benutzer das Verhalten eines
Vektorlade-Befehls folgendermaßen:
res[0] = a[0];
res[1] = a[1];
und wurden die Werte im realen Programm aber in umgekehrter Reihenfolge geladen:
x = a[1];
y = a[0];
so konnte die Vektor-Ladeoperation, obwohl vorhanden, im unteren Programmstück bisher nicht erkannt werden.
Ein weiteres Problem ist, dass in realen Programmen die Speicheroperationen einer Passung durch nicht zur Passung gehörende Speicheroperationen unterbrochen werden kann. Um das zu demonstrieren greifen wir das Beispiel aus Abbildung 2.1 noch
einmal auf: Zwischen den zur dunkel markierten Passung gehörenden Knoten Proj[M]
962:27 und Load[Is] 987:13 befinden sich entlang der Speicherkante mehrere Knoten,
die nicht zur Passung gehören: Load[Is] 970:25, Proj[M] 961:29, Store[Is] 980:19, Proj[Is]
981:18. Der einzufügende reichhaltige Befehl würde die Einzelbefehle der Passung aber
auf einmal ausführen. Wir müssen also prüfen, ob wir die dazwischenliegenden Knoten so umordnen dürfen, dass die Operationen der Passung bezüglich der Speicherkante
zusammenhängend sind.
Beide Probleme hängen mit der Speicherkante zusammen. In beiden Fällen wurden
Speicheroperationen serialisiert, obwohl eigentlich keine Speicherabhängigkeit vorhanden
ist. Das liegt daran, dass mangels einer entsprechenden Transformation alle Speicheroperationen so serialisiert wurden, wie sie im Quellprogramm vorkommen.
22
3 Generierung von Graphersetzungsregeln
Um auch mit diesen Tatsachen zurecht zu kommen, nehmen wir die Speicherkante
zwischen den Speicheroperationen nicht mit in den Mustergraph auf. Der große Vorteil
ist, dass auch Programme, die die Ladebefehle in einer anderen Reihenfolge als spezifiziert durchführen, optimiert, sowie Passungen, die bezüglich der Speicherkante ineinander verzahnt“ sind, ersetzt werden können. Wir erkaufen uns das jedoch dadurch, dass
”
wir nach erfolgreicher Mustersuche noch herausfinden müssen, ob eine Passung wirklich
ersetzt werden darf. Darauf gehen wir im Detail in Kapitel 4 ein.
3.3 Generierung des Ersetzungsgraphen
Zusätzlich zum Mustergraphen besteht eine Graphersetzungsregel aus dem Ersetzungsgraphen, der das Ergebnis der Graphtransformation beschreibt. Zunächst ist klar, dass
der Ersetzungsgraph den Knoten des neu einzufügenden reichhaltigen Befehls enthalten
muss. In diesem Abschnitt klären wir die weiterführenden Fragen, mit welchen Operanden der neue Befehl verbunden wird, wie seine Endergebnisse bekannt gemacht werden
und welche Knoten und Kanten aus dem Arbeitsgraphen gelöscht werden können.
Auf den ersten Blick erscheint es offensichtlich, dass alle Einzeloperationen einer gefundenen Passung gelöscht werden dürfen, da diese jetzt implizit durch den reichhaltigen
Befehl durchgeführt und nicht mehr gebraucht werden. Bei näherer Betrachtung erkennen wir aber, dass innere Knoten1 möglicherweise Zwischenergebnisse produzieren, die
der reichhaltige Befehl nicht explizit bereit stellt. Finden diese Zwischenergebnisse im
Arbeitsgraphen außerhalb einer Passung Verwendung, würde der Verwender durch das
sofortige Löschen Operanden verlieren, was wiederum einen unzulässigen Firm-Graphen
zur Folge hätte. Die Einzeloperationen dürfen also nicht sofort gelöscht werden, da wir
den Arbeitsgraphen zum Zeitpunkt der Regelgenerierung noch nicht kennen. Der Ersetzungsgraph enthält somit ebenfalls alle Knoten und Kanten des Mustergraphen. Überflüssige Knoten werden erst später bei der Eliminierung toten Codes entfernt (siehe
Abschnitt 4.4).
Eine Ausnahme bilden die Knoten, die die Endergebnisse der Berechnung liefern.
Diese Knoten werden nicht mehr gebraucht, da die Verwender der bisherigen Ergebnisse
nun die Ergebnisse des reichhaltigen Befehls erhalten sollen. Dazu müssten die Eingangskanten der Verwender zum reichhaltigen Befehl umverdrahtet werden. Die Anzahl der
Verwender ist aber vom Arbeitsgraphen abhängig und uns deshalb zum Zeitpunkt der
Regelgenerierung noch nicht bekannt. Dadurch ist es schwierig, diese Transformation in
einer einzigen Graphersetzungsregel auszudrücken.
Um diese Schwierigkeit zu vermeiden, erfolgt die Ersetzung nach dem Verfahren, wie
es in in Abbildung 3.3 dargestellt ist: Die linke Seite der Abbildung zeigt eine gefundene
Passung eines reichhaltigen Befehls. Die n Operanden der Berechnung sind durch die
Knoten Op 1 bis Op n, die m Endergebnisse durch die Knoten Res 1 bis Res m darge1
innere Knoten = Knoten, die weder einen Operanden noch ein Endergebnis darstellen des reichhaltigen
Befehls darstellen
3.3 Generierung des Ersetzungsgraphen
Op 1
Op n
23
Op 1
Op n
Reichh. Befehl
innere Knoten
innere Knoten
Proj Data
Res 1
Res m
Gefundene Passung
VProj 1 [Res 1]
Proj M
VProj m [Res m]
Ergebnis der Graphersetzung
Abbildung 3.3: Grundsätzliches Vorgehen zum Erstellen des Ersetzungsgraphen
stellt. Die Endergebnisse lassen sich zu einem m-komponentigen Vektor zusammenfassen.
Außerdem sind die inneren Knoten der Passung abstrakt zusammengefasst. Nun soll der
reichhaltige Befehl in das Programm eingefügt werden, was im rechten Teilbild zu sehen
ist:
Die neu hinzugekommenen Knoten sind der Knoten des reichhaltigen Befehls selbst
sowie der Knoten Proj Data, der das Gesamtergebnis des Befehls darstellt, und Proj M
für die Speicherabhängigkeit. Die Verwender der Endergebnisse sind nicht bekannt und
können so auch nicht in die Transformation mit einbezogen werden. Stattdessen werden
die bisherigen Ergebnisknoten zum Typ VProj k umtypisiert und mit dem Gesamtergebnis des reichhaltigen Befehls verbunden. Ihre ursprünglichen Eingangskanten werden
gelöscht. Die VProj k -Knoten wählen nun die k. Komponente des vom reichhaltigen
Befehl gelieferten Ergebnisvektors aus. Auf diese Weise zeigen alle Verwender der bisherigen Ergebnisse durch ihre Eingangskanten auf die VProj k Knoten und verwenden
so die Ergebnisse des reichhaltigen Befehls. Durch die VProj -Knoten vermerken wir außerdem explizit im Graph, dass ein Wert eine Vektorkomponente eines Spezialregisters
darstellt. Sollte also ein Einzelbefehl das Ergebnis weiterverwenden, muss zusätzlicher
Konvertierungscode eingefügt werden. Liefert der reichhaltige Befehl einen skalaren Wert
als Ergebnis, welcher von Einzeloperationen direkt verarbeitet werden kann, typisieren
wir den Ergebnisknoten zum Typ Proj statt VProj um.
Die Operanden des reichhaltigen Befehls sind diejenigen Knoten, die im Mustergraphen keine Vorgänger besitzen. Der reichhaltige Befehl wird also über seine Eingangskanten mit diesen Operanden verbunden. Die Kantenposition entspricht der vom Benutzer spezifizierten Operandennummer (siehe Abschnitt 3.1.1) und kann somit aus der
Befehls-Spezifikation gewonnen werden.
Es fehlt noch die endgültige Einreihung in die Speicherabhängigkeitskette des Ar-
24
3 Generierung von Graphersetzungsregeln
beitsgraphen, die wir in diesem Schritt aber noch nicht durchführen können. Dazu eine
Analyse des Arbeitsgraphen notwendig ist, was zur Optimierungszeit erfolgt. Wir beschreiben dieses Vorgehen in Kapitel 4.2.
Die inneren Knoten der Passung sind nach wie vor enthalten, wurden aber vom
restlichen Programm abgetrennt, weil die umtypisierten Ergebnisknoten neu verbunden wurden. Die inneren Knoten verlieren so möglicherweise alle Verwender und werden
dann später automatisch durch die Eliminierung toten Codes entfernt. Für Store-Knoten
funktioniert dieser Automatismus nicht mehr, da Store-Knoten kein Datenergebnis und
somit auch keine Verwender der Daten haben, die sie verlieren könnten. Wir weisen das
Graphersetzungssystem explizit an, diese Knoten zu entfernen. Das ist korrekt, da die
betreffenden Speicherstellen nun durch den reichhaltigen Befehl beschrieben werden.
3.4 Regelgenerierung
Nachdem bekannt ist, welche Gestalt Muster- und Ersetzungsgraph haben, wenden wir
uns der Frage zu, wie daraus GrGen-Graphersetzungsregeln generiert werden. Dabei
nutzen wir besondere Eigenschaften von GrGen, die wir im Folgenden beschreiben.
3.4.1 Syntax einer Graphersetzungsregel
Eine Graphersetzungsregel besteht zum Einen aus einem pattern { }-Block, der die
Knoten- und Kantenstruktur des Mustergraphen aufnimmt. Knotendefinitionen haben
die Form:
KnotenName : KnotenTyp;
Kantendefinitionen haben die Form
QuellknotenName - KantenName : KantenTyp -> ZielknotenName;
Knoten und Kanten bezeichnen wir allgemein als Graphelemente. Um Attribute von
Graphelementen zu vergleichen, erlaubt GrGen sogenannte Bedingungen innerhalb des
pattern Teils, eingeschlossen in einen if { }-Block. Darin befinden sich Boolesche Ausdrücke, wobei Attribute von Graphelementen als Variablen verwendet werden dürfen.
Zum Anderen besteht eine Graphersetzungsregel aus dem modify { }-Block. Dieser beschreibt den Ersetzungsgraph oder genauer die Unterschiede von Muster- und
Ersetzungsgraph. Dadurch können Knoten bzw. Kanten hinzugefügt, gelöscht oder umtypisiert werden. Das Löschen erfolgt durch
delete(GraphelementName, ...);
Umtypisierte Graphelemente erhalten die Werte ihrer Attribute, falls diese in einem
gemeinsamen Obertyp vorkommen. Umtypisieren kann aber auch quer zur Typhierarchie
erfolgen. Die Syntax, um Graphelemente umzutypisieren, lautet
3.5 Überdeckung mehrerer Grundblöcke
25
NeuerGraphelementName : NeuerTyp<AlterGraphelementName>;
Schließlich können im eval { }-Block Attribute von Graphelementen des Ersetzungsgraphen auf einen bestimmten Wert gesetzt werden. Zu deren Berechnung sind arithmetische Ausdrücke erlaubt.
3.4.2 Graphmodell
Zur Regelgenerierung nutzen wir aus, dass GrGen erlaubt, ein hierarchisches Graphmodell zu definieren. So fassen wir beispielsweise die Knotentypen Sel und Proj, die
möglicherweise eine Basisadresse eines Vektors darstellen, zu einem Obertyp VectorBase
zusammen. Typen werden definiert durch
node class TypName { AttributName : Typ, ... };
edge class TypName { AttributName : Typ, ... };
Untertypen von Knoten oder Kanten werden definiert durch
node class UntertypName extends TypName { AttributName : Typ, ... };
edge class UntertypName extends TypName { AttributName : Typ, ... };
Mehr Details über die Syntax und Semantik von GrGen Sprachkonstrukten können
im GrGen.NET Benutzerhandbuch [4] nachgelesen werden.
3.5 Überdeckung mehrerer Grundblöcke
Erstreckt sich die Verhaltensbeschreibung eines reichhaltigen Befehls über mehrere
Grundblöcke, sind weitere Feinheiten zu beachten, die wir im Folgenden untersuchen.
3.5.1 Phi-Knoten
Im Fall von verzweigtem Steuerfluss enthält der Mustergraph Phi -Knoten. Phi -Knoten
stehen für die Vereinigung alternativer Datenflüsse und wählen gemäß des Steuerflusses einen bestimmten Wert aus mehreren Datenflussvorgängern aus. Um zu vermerken,
welche Daten auf welchem Steuerflusspfad berechnet werden, muss die Kantennummer
einer Datenflusskante am Phi -Knoten mit der Kantennummer der entsprechenden Steuerflusskante am zugehörigen Grundblock übereinstimmen.
Um dies während der Mustersuche zu überprüfen, dürfen wir nicht einfach die absoluten Kantennummern des Mustergraphen mit den Kantennummern des Arbeitsgraphen vergleichen. Es wäre möglich, im Arbeitsgraphen zwei beliebige Kanten eines Phi Knotens zu vertauschen, ohne die Semantik des Programms zu ändern, wenn gleichzeitig
die Steuerflusskanten mit denselben Nummern am Grundblock vertauscht werden. Dann
26
3 Generierung von Graphersetzungsregeln
würden aber aber die absoluten Kantennummern von Mustergraph und Arbeitsgraph
nicht mehr übereinstimmen.
Aus diesem Grund lassen wir erst beliebige Kantennummern an Phi -Knoten und
Grundblöcken bei der Mustersuche zu. Ungültige Passungen filtern wir durch Bedingungen in der Graphersetzungsregel heraus. Dazu führen wir ein Positions-Attribut für
Kanten ein. Die Kantennummern von Datenflusskanten der Phi -Knoten und Steuerflusskanten der zugehörigen Grundblöcke, die im Mustergraph gleich sind, werden im
Arbeitsgraphen paarweise verglichen. Tritt eine Ungleichheit auf, wird die Passung aussortiert.
Ein Nachteil von Bedingungen ist, dass die darin enthaltenen Informationen nicht
in die Suchplanung mit einbezogen werden. Andererseits werden Bedingungen so früh
wie möglich berechnet (in diesem Fall, sobald beide zu vergleichenden Kanten gefunden
wurden), so dass die Suche möglicherwiese schon in einem frühen Stadium abgebrochen
werden kann.
3.5.2 Grundblock von Operationen
Wenn ein Knoten in den Mustergraphen aufgenommen wird, müssen wir auch seinen
Grundblock mit aufnehmen. Ohne Grundblockinformation könnten beispielsweise zwei
Ladebefehle, die sich auf unterschiedlichen Steuerflusspfaden befinden, fälschlicherweise
als Passung eines Vektorlade-Befehls aufgefasst werden.
In manchen Situationen ist diese Vorgehensweise aber zu restriktiv, weil Befehle,
die im Mustergraphen aus einem Grundblock bestehen, im Arbeitsgraphen in mehrere
Blöcke zerfallen können. Das hängt von der Anordnung der Einzelbefehle im Quellcode
oder von vorausgegangenen Optimierungen ab, die Knoten möglicherweise umgeordnet
haben. Wir weichen deshalb die Bedingung auf, dass jeder Knoten genau einem Grundblock zugeordnet sein muss. Stattdessen dürfen die Knoten, die sich im Mustergraphen in
einem Grundblock befinden, im Arbeitsgraphen auf mehrere Grundblöcke verteilt sein.
Diese Grundblöcke müssen sich dann aber paarweise dominieren bzw. nachdominieren.
Das bedeutet, Knoten die sich im Mustergraph auf demselben Steuerflusspfad befunden haben, können im Arbeitsgraphen nicht auf verzweigte Steuerflusspfade ausgelagert
sein. Für zwei Knoten a und b, die sich im Mustergraphen in einem Grundblock befinden,
muss also im Arbeitsgraphen eine der beiden folgenden Bedingungen gelten:
(a.block dom b.block ∧ b.block ndom a.block)
(3.2)
(b.block dom a.block ∧ a.block ndom b.block)
(3.3)
Um diese schwächere Bedingung per Graphersetzungsregel zu prüfen, können wir
wieder Bedingungen einsetzen. Allerdings setzt das vorraus, dass es Knotenattribute
gibt, die die Dominator-/Nachdominator-Beziehung von Knoten modellieren.
Eine Ausnahme bilden Phi -Knoten, für die ja die Eingangskanten mit den Steuerflusskanten ihres Grundblocks abgeglichen werden müssen und deshalb an einen bestimmten
3.5 Überdeckung mehrerer Grundblöcke
27
Value 1
Value 1
RegStore
Value 2
Value 3
RegStore
RegStore
Value 2
Value 3
Phi
(a) Mustergraph mit mehreren Grundböcken
(b) Zu erkennendes Muster im Arbeitsgraph
Abbildung 3.4: Parallele Register Stores
Grundblock gebunden sind. Für diese Knoten passen wir die Grundblöcke exakt. Dadurch stellen wir sicher, dass Datenfluss und Steuerfluss im Arbeitsgraphen genau so
verknüpft sind wie im Mustergraphen.
Für einige spezielle Knotentypen passen wir den Grundblock überhaupt nicht. Dazu
zählen Konstanten, da sie sich im Firm-Graphen immer in Startblock befinden.
3.5.3 Mehrere Register-Stores auf dieselbe Vektor-Komponente
Ein reichhaltiger Befehl, der verzweigten Steuerfluss enthält, schreibt möglicherweise auf
den verschiedenen Steuerflusspfaden verschiedene Werte in eine Vektorkomponente eines
Vektorregisters. In diesem Fall weichen Mustergraph und Arbeitsgraph stark voneinander
ab, wie Abbildung 3.4 zeigt. Grundblöcke sind grau hinterlegt, Steuerflusskanten grau
und Datenflusskanten schwarz eingezeichnet. Abbildung 3.4(a) stellt den Mustergraphen
dar. Auf drei verschiedenen Steuerpfaden werden die Werte Value 1, Value 2 und Value 3
berechnet und in dieselbe Vektorkomponente geschrieben, dargestellt durch die RegStoreKnoten. Dem gegenüber steht der Arbeitsgraph aus Abbildung 3.4(b). RegStore-Knoten
kommen im Arbeitsgraph nicht vor, stattdessen werden die drei verschiedenen Werte
durch einen Phi -Knoten vereinigt. Dieser Phi -Knoten und seine inzidenten Kanten sind
28
3 Generierung von Graphersetzungsregeln
im Mustergraphen initial nicht enthalten. Wir benötigen also eine Methode, um den
Mustergraphen dem Arbeitsgraphen anzupassen.
Unsere Implementierung setzt dazu die aktuelle Load-Store-Optimierung der libfirm
ein. Diese führt die gewünschte Transformation durch, wenn sie, wie in Abbildung 3.4(a)
der Fall, Store-Operationen auf dieselbe Adresse erkennt. Sie ersetzt die verschiedenen
Store-Knoten durch einen einzigen Store-Knoten im Nachfolgerblock und fügt einen Phi Knoten ein. Wenn pro Grundblock mehrere verschiedene Vektorkomponenten beschrieben werden, funktioniert dieses Verfahren nicht mehr, da durch die serialisierten Speicheroperationen aktuell wichtige Analyseinformationen der Load-Store-Optimierung verborgen bleiben. Die aktuelle Implementierung hat deshalb Schwierigkeiten, solche reichhaltigen Befehle zu erkennen. Abhilfe würde eine vorgeschaltete Optimierung schaffen, die
Speicheroperationen wenn möglich parallelisiert, wodurch die Load-Store-Optimierung
wieder den von uns benötigten Graph erzeugen würde. Dies ist ein prinzipiell lösbares
Problem, gehört aber nicht zu den Aufgaben dieser Arbeit. Trotz dieser Einschränkung
konnten mit unserer Implementierung alle uns bekannten reichhaltigen Befehle spezifiziert werden.
4 Ersetzungsschritt
In diesem Kapitel erklären wir, wie die generierten Graphersetzungsregeln angewendet
werden und warum die Reihenfolge der Anwendung wichtig ist. Wir beschreiben, wie eine
gefundene Passung auf Konsistenz geprüft und der reichhaltige Befehl in die Speicherabhängigkeitskette eingereiht wird. Abschließend erläutern wir noch, wie überflüssige
Knoten aus dem Graph entfernt werden.
4.1 Priorität von Regeln
Durch die Einführung der VProj -Knoten werden Graphersetzungsregeln voneinander
abhängig. Betrachten wir beispielsweise einen VectorLoad und einen VectorAdd Befehl,
der seine Operanden in einem Vektorregister erwartet. Im Mustergraphen des VectorAddBefehls befinden sich deshalb VProj -Knoten, die seine Operanden darstellen. Diese
VProj -Knoten werden aber erst durch die Graphersetzungsregel des VectorLoad-Befehls
in den Arbeitsgraphen eingefügt. Deswegen muss die VectorLoad-Graphersetzungsregel
vor der VectorAdd-Graphersetzungsregel ausgeführt werden.
Dazu führen wir Prioritätsklassen ein. Graphersetzungsregeln mit höherer Prioritätsklasse hängen von Regeln mit niedriger Prioritätsklasse ab. An dieser Stelle sei erwähnt,
dass Prioritätsklassen zwar den Suchaufwand verringern, jedoch für einige Abhängigkeiten nicht ausreichend sind. Wir erörtern dieses Thema in Kapitel 6 genauer.
Eine speziell ausgezeichnete Prioritätsklasse ist die Klasse CleanUp. Muster in dieser
Klasse werden zuletzt gesucht und enthalten typischerweise Regeln, um im Arbeitsgraphen übrig gebliebene VProj Knoten in Allgemeinregister umzukopieren.
4.2 Anordnen des reichhaltigen Befehls
Bisher haben wir nicht darüber gesprochen, an welcher Stelle ein reichhaltiger Befehl
im Programmfluss angeordnet werden soll. Befehlsanordnung ist zwar die Aufgabe des
Übersetzer-Backends, jedoch ist in Firm-Programmen durch die Speicherabhängigkeitskette eine Ausführungsreihenfolge von Speicheroperationen vorgegeben. Ein reichhaltiger
Befehl, der den Speicher benutzt, muss in die Speicherabhängigkeitskette eingereiht werden. Wir müssen dafür sorgen, dass dabei keine Verklemmungen entstehen. Von einer
Verklemmung sprechen wir, wenn ein Befehl so angeordnet wird, dass zum Zeitpunkt
seiner Ausführung seine Operanden noch nicht zur Verfügung stehen.
29
30
4 Ersetzungsschritt
Load 1
Proj Is
VectorLoad
Proj M
E1
Proj M
E2
Store
Load 1
Proj M
Proj Is
Load 2
Proj Is
Store
Proj M
VectorLoad
E1
Proj M
E2
(a) Verklemmung: Store kann das Ergebnis E1
des reichhaltigen Befehls nicht verwenden,
da es zu spaet berechnet wird.
Proj M
Load 2
Proj Is
Proj M
(b) Korrekte Anordnung: Store kann das Ergebnis E1
des reichhaltigen Befehls verwenden, Load 1
und Load 2 können entfallen
Abbildung 4.1: Mögliche Anordnungen des reichhaltigen Befehls. Dunkel gefärbte Knoten gehören zum Such- bzw. Ersetzungsmuster. Gestrichelte Elemente
können nach der Regelanwendung entfallen.
Eine mögliche Verklemmung ist in Abbildung 4.1(a) dargestellt1 . Hier ist grau markiert die Passung eines Vektor-Ladebefehls inklusive dem bereits eingefügten reichhaltigen Befehl VectorLoad zu sehen. Die Passung wird entlang der fett gezeichneten Speicherabhängigkeitskette durch einen Store-Knoten unterbrochen. Diese Store-Operation
verwendete ursprünglich den von Load 1 geladenen Wert, soll aber stattdessen nun den
äquivalenten Wert E1 verwenden, der durch den reichhaltigen Befehl VectorLoad von
derselben Speicherstelle geladen wird. Der Knoten VectorLoad wurde jedoch hinter der
Passung und insbesondere hinter der Store-Operation angeordnet. Zur Ausführungszeit
der Store-Operation steht der benötigte Wert E1 also noch gar nicht zur Verfügung. Da
der VectorLoad-Befehl zu spät angeordnet wurde, entsteht somit eine Verklemmung.
Um solche Verklemmungen zu vermeiden, führen wir den reichhaltigen Befehl
möglichst früh aus. Abbildung 4.1(b) zeigt diesen Fall: Der reichhaltige Befehl VectorLoad muss so angeordnet werden, dass er vor allen Speicheroperationen der zugehörigen
1
Adressberechnung, Operanden der Load -Knoten sowie des reichhaltigen Befehls wurden der Übersicht
halber weggelassen.
4.2 Anordnen des reichhaltigen Befehls
31
Passung ausgeführt wird. Folglich wird in unserem Beispiel der Knoten VectorLoad vor
dem Load 1 -Knoten eingereiht. So kann der Store-Befehl das Teilergebnis E1 des reichhaltigen Befehls verwenden und die Ladebefehle Load 1 und Load 2 können komplett
entfallen.
Wie im Beispiel zu sehen ist, hängt die Stelle, an der ein reichhaltiger Befehl angeordnet werden soll, vom Verlauf der Speicherabhängigkeitskette durch die Passung ab.
Wir haben aber Informationen über die Speicherkante im Mustergraphen weggelassen,
um nicht zu restriktiv zu sein. Deshalb kann ein reichhaltiger Befehl nicht durch eine
vorgenerierte Graphersetzungsregel in die Speicherabhängigkeitskette eingereiht werden.
Dazu analysieren wir den Arbeitsgraphen separat und fügen die Speicherabhängigkeit
wenn nötig nach der erfolgreichen Anwendung einer Graphersetzungsregel hinzu.
4.2.1 Auffinden der Speichervorgänger
Wir müssen also eine geeignete Stelle im Programmgraphen finden, um den reichhaltigen
Befehl möglichst früh anzuordnen. Es stellt sich die Frage, ob es eindeutig eine solche
Stelle gibt. Der Befehl soll vor allen Speicheroperationen seiner Passung angeordnet werden. Es muss aber im Allgemeinen keine eindeutige erste Speicheroperation einer Passung
geben, weil in Firm auch parallele Speicherabhängigkeitsketten zugelassen sind. Speicheroperationen auf parallelen Speicherpfaden greifen auf disjunkte Speicherbereiche zu2 .
Insbesondere kann es somit mehrere Pfade der Speicherabhängigkeit durch ein Passung
geben. Wir haben also im Allgemeinen keinen eindeutigen Knoten, vor dem der reichhaltige Befehl anzuordnen wäre. Es gibt vielmehr drei Fälle der Speicherabhängigkeit
einer Passung vom restlichen Graph zu unterscheiden, die wir im Folgenden beschreiben.
Zunächst benötigen wir jedoch eine exakte Definition des Begriffs des Speichervorgängers
einer Passung:
Definition 2. Wir bezeichnen einen Knoten n als Speichervorgänger einer Passung p,
falls ein direkter Nachfolger m von n zu p gehört und es über die Speicherkante einen
Weg vom Start-Knoten nach n gibt, der keinen zu p gehörenden Knoten überstreicht. m
bezeichnen wir als eine erste Speicheroperation.
Als Beispiel betrachten wir eine Passung des Befehls VectorLoad, der zwei aufeinanderfolgende Speicherstellen lädt. Die Passung besteht aus 4 Knoten, nämlich den beiden
Ladeoperationen Load 1 und Load 2 sowie ihren jeweiligen Proj M -Knoten.
Abbildung 4.2(a) zeigt den Fall, dass alle Speicheroperationen dieser Passung – hellgrau hinterlegt – komplett serialisiert sind. Hier gibt es eine eindeutige erste Speicheroperation Load 1, die nach Definition Teil der Passung ist. Folglich existiert auch ein
eindeutiger Speichervorgänger: der dunkelgrau hinterlegte Proj M -Knoten außerhalb der
Passung. Der reichhaltige Befehl kann direkt vor dem Load 1 -Knoten eingefügt werden.
2
Eine vorausgegangene Optimierung könnte Speicheroperationen parallelisiert haben, nachdem sie herausgefunden hat, dass zwei Load -Knoten auf disjunkte Speicherbereiche zugreifen.
32
4 Ersetzungsschritt
Proj M
Proj M
?
Load 1
Load 1
Load 2
Proj M
...
Proj M
Proj M
Proj M
Load 1
Proj M
Proj M
Load 2
Load 2
Proj M
Proj M
(a) Eindeutiger Speichervorgänger
eine erste Speicheroperation
(b) Eindeutiger Speichervorgänger
mehrere erste Speicher−
operationen
(c) Mehrere Speichervorgänger
mehrere erste Speichero−
operationen
Abbildung 4.2: Drei mögliche Speicherabhängigkeiten. Die Musterknoten sind hellgrau
markiert, die Speichervorgänger dunkelgrau.
In Abbildung 4.2(b) sind die Speicheroperationen Load 1 und Load 2 parallelisiert
worden. Die Passung hat nun mehrere erste Speicheroperationen Load 1 und Load 2,
jedoch ebenfalls einen eindeutigen Speichervorgänger Proj M außerhalb der Passung. In
diesem Fall ordnen wir den reichhaltigen Befehl direkt hinter dem Speichervorgänger
ein.
Abbildung 4.2(c) zeigt eine problematische Situation: Zusätzlich zu den Operationen
Load 1 und Load 2 sind weitere Speicheroperationen außerhalb der Passung parallelisiert
worden. Es gibt nun mehrere, dunkelgrau markierte Speichervorgänger außerhalb der
Passung, so dass der reichhaltige Befehl nicht mehr eindeutig angeordnet werden kann.
Wir müssen in diesem Fall eine künstliche Sync-Operation einfügen, die die beiden Flüsse
der Speicherabhängigkeit miteinander vereint. Dadurch entsteht wieder ein eindeutiger
Speichervorgänger, den wir wie in Fall (b) behandeln können. Diesen Fall deckt unsere
Implementierung momentan noch nicht ab.
Das Bestimmen der Speichervorgänger einer Passung geschieht zwischen Mustersuche und Ersetzung, indem wir eine Implementierung des Algorithmus 1 ausführen. Initial
übergeben wir dem Algorithmus den Start-Knoten als Parameter. Dieser Algorithmus
führt dann eine Tiefensuche auf dem Zwischensprachengraphen entlang der Speicherkante beginnend beim Start-Knoten durch. Trifft die Suche auf einen Knoten der Passung,
wird der Vorgänger dieses Knotens, der außerhalb der Passung liegt, der Menge M der
Speichervorgänger hinzugefügt, und die Suche entlang dieses Pfades abgebrochen. Nach
dem Terminieren des Algorithmus enthält M die Menge aller Speichervorgänger. Unsere
Implementierung setzt momentan |M | = 1 vorraus. Nach dem Ausführen einer Graphersetzungsregel wird der reichhaltige Befehl direkt nach dem gefundenen Speichervorgänger
4.2 Anordnen des reichhaltigen Befehls
33
in die Speicherabhängigkeitskette eingefügt.
Algorithmus 1 Suche der Speichervorgänger
procedure SucheSpeichervorgaenger(n)
if Knoten n schon besucht then
return
end if
Markiere n als besucht
if Knoten n gehört zur Passung then
Markiere Vorgänger von n als Speichervorgänger
return
end if
if mode(n) != Memory then
for all Nachfolger m von n mit mode(m) == Memory do
Speichervorgaenger(m)
end for
end if
if mode(n) == Memory then
for all Nachfolger m von n do
Speichervorgaenger(m)
end for
end if
end procedure
4.2.2 Grundblock des reichhaltigen Befehls
Den Grundblock, in den ein reichhaltiger Befehl angeordnet werden soll, bestimmen
wir ebenfalls erst, nachdem eine Passung gefunden wurde. Wir können den richtigen
Grundblock nicht per Graphersetzungsregel bestimmen, da eine Passung, die im Mustergraphen aus nur einem Grundblock besteht, im Arbeitsgraphen auf mehrere Blöcke
verteilt sein kann (siehe Abschnitt 3.5.2). Zum Zeitpunkt der Regelgenerierung kennen
wir diese Verteilung noch nicht. Wir haben zwei Fälle zu beachten:
1. Der reichhaltige Befehl benutzt den Speicher. In diesem Fall wurde der Befehl
schon in die Speicherabhängigkeitskette eingereiht und damit ist der Grundblock
schon fest vorgegeben. Würde der Befehl in einem anderen Grundblock angeordnet
werden, entstünden Verklemmungen.
2. Der Befehl arbeitet nur auf Registern. Falls der reichhaltige Befehl keinen Speicher
benutzt bietet sich mehr Spielraum. Wir suchen dazu den Grundblock eines inneren
Knotens der Passung, der alle anderen Blöcke der Passung dominiert und ordnen
den reichhaltigen Befehl in diesen ein.
34
4 Ersetzungsschritt
4.3 Analyse einer Passung
In den folgenden Abschnitten beschreiben wir die Fälle, in denen der reichhaltige Befehl
nicht eingesetzt werden darf, obwohl eine Passung gefunden wurde. Vor dem Ersetzen
einer Passung müssen wir deshalb durch geeignete Analysen prüfen, ob wir den Ersetzungsschritt überhaupt durchführen dürfen.
4.3.1 Speicherabhängigkeit von Musterknoten
Die erste Analyse, die wir durchführen, betrifft den Verlauf der Speicherkante durch
eine Passung. Da wir im Mustergraph die Speicherkanten weggelassen haben, kennen
wir diesen Verlauf nicht. Möglicherweise befinden sich zwischen den Knoten der Passung
entlang der Speicherkante Knoten, die nicht zur Passung gehören (siehe Abschnitt 3.2.3).
Um das herauszufinden, suchen wir, ausgehend von jeder Speicheroperation n einer
Passung, die Speicherkante nach oben hin ab. Wir können die Suche abbrechen, wenn
wir auf einen Speichervorgänger der Passung treffen. Dadurch arbeitet die Suche nur
lokal im Bereich der Passung. Stößt die Suche auf einen fremden Knoten m, müssen
wir überprüfen, ob beide Knoten n und m auf denselben Speicherbereich S zugreifen
und ob einer der beiden Knoten n oder m den Speicher verändert. Ist das der Fall,
darf die Ersetzung nicht durchgeführt werden, da wir wie in Abschnitt 4.2 besprochen
den reichhaltigen Befehl vor allen Speicheroperationen der Passung und somit auch vor
dem fremden Knoten m anordnen. Dadurch hätten wir die Reihenfolge, in der auf den
Speicherbereich S zugegriffen wird verändert. Da es sich zusätzlich bei einem der Zugriffe
um einen Schreibzugriff handelt, hätten wir die Semantik des Programms verändert.
4.3.2 Verwendung von Zwischenergebnissen
Eine weitere Analyse ist notwendig, wenn in einer Passung gleichzeitig Load- und StoreOperationen enthalten sind. Die Store-Operation schreibt möglicherweise eine Speicherstelle, die zuvor von der Load-Operation geladen wurde. Eine wichtige Folge der in 4.2
beschriebenen Befehlsanordnung ist aber, dass ein reichhaltiger Befehl eine Speicherstelle, die er liest, nur dann beschreiben darf, wenn der Ladebefehl nicht ein Endergebnis
der Passung ist und keine weiteren Verwender außerhalb der Passung besitzt.
Ein Beispiel dazu ist in in Abbildung 4.3 dargestellt. Links ist grau hinterlegt die
Passung eines reichhaltigen Befehls namens LoadStore vor der Ersetzung zu sehen. Der
Befehl lädt einen Wert aus dem Speicher, ändert ihn durch eine beliebige Operation
und speichert den Wert an dieselbe Speicherstelle zurück. Die Speicherkanten sind fett
eingezeichnet. Der Knoten Load hat einen Verwender außerhalb der Passung. Deshalb
muss der Load-Knoten auch nach der Regelanwendung stehen bleiben. Diese Situation
ist auf der rechten Seite der Abbildung zu sehen. Der reichhaltige Befehl LoadStore
wurde nach unserer Befehlsanordnungsstrategie vor dem Load-Knoten eingereiht. Da
LoadStore nun implizit die Store-Operation ausführt, die im Ursprungsprogramm nach
4.3 Analyse einer Passung
35
Proj P
Proj P
Regel−
anwendung
LoadStore
Load 1
Proj M
Proj Is
Proj M
Load 1
Verwendung
...
Store
Proj Is
Verwendung
Abbildung 4.3: Nach der Ersetzung liest ein übrig gebliebenes Load einen fehlerhaften
Wert. Grau markierte Knoten gehören zum Such- bzw. Ersetzungsmuster.
der Load-Operation ausgeführt wird, haben wir die Semantik des Programms verändert:
Die stehen gebliebene Load-Operation liest fälschlicherweise den bereits veränderten
Speicher.
Kritisch sind also Passungen mit lesenden und schreibenden Speicheroperationen. Für
diese Passungen prüfen wir, ob eine Load-Operation, die nicht das Endergebnis des reichhaltigen Befehls liefert, weitere Verwender hat und schließen solche Passungen von der
Ersetzung aus. Diese Prüfung könnte innerhalb der Graphersetzungsregel durch NACs3
geschehen. In unserer Implementierung ist die Prüfung momentan ausprogrammiert.
4.3.3 Speicherabhängigkeit von Operanden
Wenn die Operanden eines reichhaltigen Befehls durch die Speicherkante voneinander
abhängen, kann dies möglicherweise zu Verklemmungen führen. Ein Beispiel dazu ist in
Abbildung 4.4 dargestellt.
Abbildung 4.4 (a) zeigt das Ausgangsprogramm, welches zwei Vektoren a und b aus
dem Speicher lädt und komponentenweise multipliziert. Der einfacheren Darstellung wegen wurden Proj -Knoten weggelassen; von Adressberechnungen wurde durch Angabe
der Vektorkomponenten im Namen der Load-Knoten abstrahiert: Load 1a lädt die erste Komponente des Vektors a. Die Speicherabhängigkeit ist durch gestrichelte Kanten
dargestellt. Außerdem seien Graphersetzungsregeln für zwei Befehle in unserer Daten3
Negativ Application Condition, deutsch: Negative Anwendungsbedingung
36
4 Ersetzungsschritt
Load 1a
Load 1a
VectorMul
Load 1b
Load 1b
Load 2a
VectorLoad
Load 2b
Mul
Mul
(a)
VProj 0
Mul
VectorLoad
VProj 1
Mul
(b)
(c)
Abbildung 4.4: Es entsteht eine Verklemmung, da Speicherabhängigkeiten von Operanden existieren.
bank: Für einen VectorLoad-Befehl, der einen Vektor aus dem Speicher lädt, und für
einen VectorMul-Befehl, der zwei Vektoren komponentenweise multipliziert. VectorMul
kann einen seiner beiden Operanden direkt aus dem Speicher laden, der zweite muss
in Registern vorliegen und explizit geladen werden. Dies geschieht, indem wir das grau
hinterlegte Muster durch einen VectorLoad-Befehl ersetzen.
In Abbildung 4.4(b) wurde der VectorLoad-Befehl bereits eingesetzt und in die Speicherabhängigkeitskette eingereiht. Nun kann die grau hinterlegte Passung des VectorMul-Befehls ersetzt werden.
Abbildung 4.4(c) stellt das Ergebnis dieser Ersetzung dar. Nach unserer Strategie
wurde der VectorMul-Befehl vor dem VectorLoad-Befehl eingereiht. Dieses Vorgehen
erzeugt ein unzulässiges Programm, da der VectorMul-Befehl die Ergebnisse des VectorLoad-Befehls benötigt, diese aber noch gar nicht berechnet sind. Diese Ersetzung darf
also nicht angewendet und muss zurückgewiesen werden.
Der Grund für diese Verklemmung ist in Abbildung 4.4(b) zu sehen: Die beiden Operanden VProj 0 und VProj 1 hängen über den Knoten VectorLoad und die Speicherkante
von der zur VectorMul-Operation gehörenden Speicheroperation Load1a ab. In solchen
Fällen wird der reichhaltige Befehl immer oberhalb seines Operanden angeordnet, was
zur Verklemmung führt.
Wir benötigen also eine Analyse, die prüft, ob die Operanden des reichhaltigen Befehls direkt oder indirekt über die Speicherkante voneinander abhängen. Dazu starten
wir von jedem Operanden aus eine Suche im Arbeitsgraphen. Trifft die Suche auf eine
Speicheroperation, suchen wir die von dort ausgehende Speicherabhängigkeitskette nach
oben hin ab. Treffen wir dabei auf einen beliebigen zur Passung gehörigen Knoten, wird
die Passung zurückgewiesen, da eine Speicherabhängigkeit von Operanden festgestellt
4.3 Analyse einer Passung
37
wurde. Die Suche kann abgebrochen werden, wenn ein Grundblock erreicht wird, der
höher als alle von der Passung überdeckten Blöcke in der Dominanzrelation steht.
An dieser Stelle sei noch anzumerken, dass die Verklemmung in Abbildung 4.4 zu
verhindern gewesen wäre, indem Load 1a und Load 2a statt Load 1b und Load 2b als
Passung für den VectorLoad-Befehl gewählt worden wäre. In Kapitel 6 beschreiben wir
Verfahren, wie die bessere der beiden Musterkombinationen ausgewählt werden kann.
4.3.4 Grundblock von Operanden
Wir müssen sicherstellen, dass die Operanden des reichhaltigen Befehls zur Verfügung
stehen, wenn er ausgeführt wird. Dies ist vor allem dann wichtig, wenn sich die Passung
des Befehls über mehrere Grundblöcke erstreckt. Die Operanden müssen sich im selben
Block oder in einem Dominatorblock des Blocks befinden, in den der reichhaltige Befehl
eingeordnet werden soll. Da wir die Knoten, die als Operanden des reichhaltigen Befehls
dienen, bereits zum Zeitpunkt der Passung kennen und den Block des reichhaltigen
Befehls schon bestimmt haben, können wir diese Bedingung für jeden Operanden einzeln
nachprüfen.
4.3.5 Ausrichtung von Daten
Manche reichhaltige Befehle können nur auf Daten im Speicher zugreifen, wenn diese speziell ausgerichtet sind. Beispielsweise kann der movdqa-Befehl aus dem SSE-Befehlssatz
nur Daten laden, wenn deren Basisadresse durch 16 teilbar ist. Ist dies nicht der Fall,
erzeugt der Befehl eine Ausnahme4 . Bevor wir einen solchen Befehl einsetzen, muss garantiert sein, dass die Ausrichtung5 der Daten im Speicher stimmt [7].
Dazu ist zum Einen eine Erweiterung der Spezifikationssprache notwendig, so dass
der Benutzer die Art der Ausrichtung angeben kann. Zum Anderen muss der Übersetzer bzw. die Programmiersprache die Möglichkeit bieten, Daten auszurichten. Einige
Lösungsansätze dazu stammen von Intel. Intel schlägt zur Ausrichtung von Daten die
Anwendung spezieller Intrinsics vor [7]. Der Intel eigene C-Übersetzer ICC kennt das
Intrinsic
__declspec(align(16)) float buffer[400];
um die Reihung6 buffer an 16-Byte-Grenzen auszurichten. Ist buffer eine lokale Variable, richtet der Übersetzer gleichzeitig die Prozedurschachtel mit aus. In C kann ein
Programmierer Daten auch manuell ausrichten, indem er mehr Speicher als benötigt reserviert und die erste, mit der benötigten Ausrichtung verträglichen Adresse innerhalb
des reservierten Speicherbereichs zur Platzierung seiner Daten heranzieht:
4
engl. Exception
engl. Alignment
6
engl. Array
5
38
4 Ersetzungsschritt
float *p_buffer;
float buffer[400 + 15];
p_buffer = (float*)(((unsigned) buffer + 15) & ~0xf);
Intrinsics haben den Vorteil, dass dem Übersetzer explizit bekannt gemacht wird, dass
Daten ausgerichtet sind. Werden die Daten manuell ausgerichtet, muss diese Information zuerst durch eine geeigneten Analyse gewonnen werden. Unsere Motivation für eine
übersetzerinterne Optimierung war allerdings, dass durch sie keine Intrinsics verwendet
werden müssen und so einfach der Übersetzer oder die Architektur gewechselt werden
kann. In diesem Fall könnten diese Intrinsics, zumindest in der Programmiersprache
C, durch Präprozessor-Makros automatisch aus dem Programm entfernt werden, wenn
das Programm von einem anderen Übersetzer übersetzt wird. Da in diesem Fall keine
reichhaltigen sondern Einzelbefehle ausgeführt werden, müssen die Daten möglicherweise
nicht ausgerichtet sein.
Obwohl Reihungen korrekt ausgerichtet sind, können Daten von nicht erlaubten
Adressen geladen werden, da wir zunächst beliebige Offsets zu Basisadressen von Reihungen erlauben, beispielsweise durch einen Schleifenzähler i:
a = buffer[i];
Wir müssen also zusätzlich garantieren, dass sich der Schleifenzähler so verhält, dass
nur von erlaubten Basisadressen geladen wird. Dies wurde z.B. von Pryanishnikov, Krall
und Horspool [24], [25] untersucht. Eventuell müssen Techniken wie z.B. Schleifenschälen
[20] angewandt werden. Es kann sich möglicherweise auch lohnen, vor jedem Speicherzugriff zur Laufzeit zu prüfen, ob die Daten korrekt ausgerichtet sind und dann dynamisch
zwischen reichhaltigen Befehlen und Einzelbefehlen umzuschalten.
Dies ist jedoch nicht Aufgabe dieser Arbeit. Unsere Implementierung enthält diese Techniken noch nicht, stattdessen profitieren wir von Befehlen wie z.B. lddqu des
SSE3-Befehlssatzes, um auch nicht-ausgerichtete Daten schnell zu laden. Gibt es solche
Befehle nicht, müssen die bisherigen Überlegungen in Zukunft in die Implementierung
mit einbezogen werden.
4.4 Entfernen überflüssiger Knoten
Durch die Regelanwendung fallen die inneren Knoten einer Passung nicht weg, da sie
im Ersetzungsgraph ebenfalls vorhanden sind. Haben diese Knoten aber keine Verwender außerhalb der Passung, sollen sie entfernt werden, da ihre Ergebnisse nicht mehr
gebraucht werden. Dazu verwenden wir drei aus dem Übersetzerbau bekannte [2] und
bereits in unseren Übersetzer eingebaute Optimierungen, die erst dann ausgeführt werden, nachdem alle möglichen Graphersetzungsregeln ausgeführt und die entsprechenden
reichhaltigen Befehle eingesetzt worden sind.
4.4 Entfernen überflüssiger Knoten
39
4.4.1 Entfernen toten Codes
Durch das Retypisieren der Ergebnisknoten des reichhaltigen Befehls zu VProj -Knoten
verlieren die ehemaligen Operanden der Ergebnisknoten ihre Verwender. Da in Firm Datenabhängigkeiten statt Datenfluss dargestellt wird, sind diese verwaisten“ Vorgänger”
knoten automatisch nicht mehr sichtbar und werden so bei der Codeauswahl nicht mehr
berücksichtigt.
4.4.2 Load-Store-Optimierung
Für Load-Knoten funktioniert dieses Verfahren nicht mehr, da diese mehrere Ergebniskanten haben. Fällt durch das oben beschriebene Verfahren der Verwender des Datenergebnisses weg, ist der Load-Knoten immer noch durch die Speicherkante sichtbar.
Die Load-Store-Optimierung erkennt Load-Knoten ohne Verwender und entfernt diese.
Store-Knoten wurden bereits während der Regelanwendung entfernt, da sie kein Datenergebnis produzieren, das außerhalb der Passung verwendet werden könnte.
4.4.3 Kontrollflussoptimierung
Enthält ein Grundblock durch die obigen Optimierungen keine Knoten mehr, entfernt
die Kontrollflussoptimierung diese leeren Blöcke. Das ist häufig der Fall, wenn sich ein
Muster über mehrere Grundblöcke erstreckt. In diesem Fall kann die Kontrollflussoptimierung auch nicht mehr benötigte Verzweigungsoperationen entfernen.
40
4 Ersetzungsschritt
5 Normalisierung und Varianten
In diesem Kapitel erläutern wir, welche Techniken zum Einsatz kommen, um syntaktische Abweichungen bei semantischer Gleichheit von Muster- und Arbeitsgraph möglichst
gering zu halten. Da es im Allgemeinen unentscheidbar ist, ob zwei syntaktisch verschiedenen Programm semantisch gleich sind, sind wir nicht in der Lage, jeden Teilgraph
zu finden, der die Semantik des Mustergraphen besitzt. Wir können jedoch die Trefferwahrscheinlichkeit bei der Mustersuche erhöhen, indem wir durch normalisierende
Transformationen Muster- und Arbeitsgraph einander annähern und Spezialfälle abdecken, indem wir ausgehend von einem Mustergraphen automatisch mehrere Varianten
generieren. Zusätzlich wenden wir aus dem Übersetzerbau bekannte Optimierungen an,
die wir im folgenden Abschnitt beschreiben.
5.1 Vortransformationen
Unsere Optimierung wird in einen Übersetzer integriert, der bereits viele andere Optimierungen durchführt, die unsere eigene Optimierung positiv oder negativ beeinflussen
können. Einige bereits vorhandene Optimierungen haben eine normalisierende Auswirkung auf Muster- und Arbeitsgraph, andere bewirken genau das Gegenteil. Das liegt
daran, dass dabei der Mustergraph nur musterlokal optimiert wird, der Arbeitsgraph
aber musterübergreifend. Wir müssen also genau überlegen, wann unsere Optimierung
zwischengeschaltet wird.
In der Praxis hat sich gezeigt, dass die folgenden aus dem Gebiet des Übersetzerbaus bekannte Graphtransformationen auf den Zwischensprachen- und Mustergraphen
angewendet, Muster- und Arbeitsgraph einander annähern:
Lowering. Unsere Optimierung wird ausgeführt, nachdem das sogenannte Lowering des
Firm-Graphen durchgeführt wurde. Das bedeutet, dass abstrakte Konstrukte für
Zugriffe auf Arrayelemente durch Zeigerarithmetik ersetzt wurden. Auf diese Weise
können wir direkte Arrayzugriffe und Zeigerarithmetik im Quellprogramm gleichermaßen handhaben.
Kritische Kanten entfernen. Das Entfernen kritischer Kanten entbindet uns von einer
Normalisierung in speziellen Fällen von verzweigtem Steuerfluss.
Schleifen ausrollen. Es wäre möglich, dass Vektorbefehle deshalb nicht gefunden werden, weil sich der Zugriff auf Vektorkomponenten hinter verschiedenen Schleifeni-
41
42
5 Normalisierung und Varianten
terationen verbirgt. Das Ausrollen von Schleifen, kann solche verborgenen Befehle
aufdecken. Die Anzahl der ausgerollten Iterationen kann beispielsweise von den in
der Schleife verwendeten Datentypen und der maximalen Breite der Vektorregister
abhängig gemacht werden.
Load-Store-Optimierung. Eine Optimierung, die die Informationen einer Alias-Analyse
dazu verwendet, um unnötige Load- oder Store-Knoten zu entfernen. Momentan
wird die Load-Store-Optimierung erweitert, um Speicherbefehle zu parallelisieren,
wenn keine Abhängigkeit zwischen ihnen besteht. Eine gute Alias-Analyse ist wichtig für die Funktionsfähigkeit unserer Optimierung. Wir verwenden einen sogenannten, in unseren Übersetzer eingebauten Memory Disambiguator [14].
If-Konversion. Durch die If-Konversion [22] fallen viele Grundblöcke weg, was die Größe
von Mustergraphen verringert. Allerdings kann die If-Konversion den Registerdruck erhöhen, was negative Auswirkungen auf die Laufzeit hat. Da die aktuellen
Implementierung der If-Konversion nicht erlaubt, die Darstellung wieder abzubauen, vermeiden wir sie in unserer Implementierung
Code Placement. Ein für uns kritische Optimierung, die in vielen Fällen eine normalisierende Wirkung hat, da sie mehrere Konvertierungsknoten (Knotentyp Conv ) zu
einem Knoten vereinigen kann. Allerdings kann sie Operationen auch verschieben,
was eine erfolgreiche Mustersuche schwieriger macht.
Folgende Optimierungen hatten negative Auswirkung auf unsere eigene Optimierung:
Strength reduction. Die Adressberechnung erfolgt nach Anwendung der Strength Reduction iterativ, die Mustersuche kann dadurch die einzelnen Vektorkomponenten
nicht mehr unterscheiden.
5.2 Normalisierung
Zusätzlich zu den Standardoptimierungen verwenden wir spezielle normalisierende
Transformationen. Hofmann beschreibt eine Normalisierung arithmetischer Ausdrücke
[15, Abschnitt 4.6]. Wir greifen diese Idee auf und wenden sie auf die Ausdrücke zur
Adressberechnung verschiedener Adressierungsarten an. Diese und weitere Techniken
beschreiben wir in den folgenden Abschnitten.
5.2.1 Vergleiche
Vorausgegangene Optimierungen haben möglicherweise die Operanden von Vergleichsoperationen vertauscht. Da wir aber eine möglichst große Ähnlichkeit von Muster- und
Arbeitsgraph herstellen möchten, wandeln wir alle Vergleichsoperationen Ist-Größer
bzw. Ist-Größer-Gleich in Ist-Kleiner bzw. Ist-Kleiner-Gleich um.
5.2 Normalisierung
(a) Basisadresse + Const
43
(b) Basisadresse + Vektor−Basisadresse
+ Const
(c) Basisadresse + Arraybreite * Zeile
+ Vektor−Spalte + Const
Abbildung 5.1: Mögliche Positionen eines Vektors (grau markiert) im Speicher.
5.2.2 Adressierungsarten
Wir fordern, dass ein Zugriff auf Reihungselemente mit konstantem Offset in der BefehlsSpezifikation – z.B. a[0], a[1] – auf verschiedene direkte und indirekte Zugriffe im zu
optimierenden Programm passt – z.B. a[i+1], a[i][j+1], a[b[i]+j+1]. Der Grund
ist, dass wir zum Zeitpunkt der Spezifikation die Lage von Vektoren im Speicher nicht
kennen können und deshalb davon abstrahieren. Oft sind Vektoren in mehrdimensionale
Reihungen eingebettet oder ihre Basisadresse hängt von verschiedenen Schleifenzählern
ab, was z.B. ein Ergebnis des Schleifenausrollens sein kann.
Abbildung 5.1 zeigt mögliche Positionen eines 4-dimensionalen Vektors innerhalb einer Reihung a und die entsprechende Berechnung der Speicheradresse einer Vektorkomponente (unter der Annahme, dass Reihungen zeilenweise im Speicher abgelegt werden).
Wir diskutieren diese drei Fälle im Folgenden genauer:
1. Fall (a) zeigt eine eindimensionale Reihung, die Basisadresse des Vektors entspricht
der Basisadresse der Reihung. Dies entspricht der Situation zum Zeitpunkt der
Spezifikation. Ein lesender Zugriff auf die ersten beiden Vektorkomponenten sähe
folgendermaßen aus:
LOAD(Basis + 0), LOAD(Basis + 1)
(5.1)
2. Befindet sich der Vektor nicht am Beginn der eindimensionalen Reihung, wie Fall
(b) zeigt, sondern an einer zur Übersetzungszeit noch nicht bestimmbaren Stelle
a[j], so lautet der Zugriff auf die ersten beiden Komponenten:
LOAD(Basis + j + 0), LOAD(Basis + j + 1)
(5.2)
Bei j könnte es sich beispielsweise um einen Schleifenzähler handeln. Der Unterschied zu (a) ist, dass j als zusätzlicher Summand in beiden Ausdrücken zur
Adressberechnungen auftaucht.
3. Greift das zu optimierende Programm auf einen in ein mehrdimensionales Array
eingebetteten Vektor zu, was Fall (c) zeigt, kommt noch einen zusätzlicher Summand hinzu. Angenommen der Vektor beginne an Position a[i][j], so lautet der
44
5 Normalisierung und Varianten
Zugriff auf die ersten beiden Komponenten:
LOAD(Basis + i ∗ Breite + j + 0), LOAD(Basis + i ∗ Breite + j + 1)
(5.3)
4. Ein in Abbildung 5.1 nicht dargestellter Fall ist der indirekte Zugriff über eine
Verweistabelle. Jedes Reihungselement einer eindimensionalen Reihung verweist
wieder auf eine Reihung, womit sich mehrdimensionale Reihungen modellieren lassen. In C ist dies beispielsweise eine Möglichkeit, um Reihungen dynamischer Größe
an eine Funktion zu übergeben. Der Zugriff auf eine solches Reihung erfolgt nicht
durch die oben genannten Formeln sondern durch kaskadierte Load Befehle. Die
erste Vektorkomponente ließe sich folgendermaßen laden:
LOAD(LOAD(Basis + i) + j + 1)
(5.4)
Da das innere Load nur die Basisadresse einer zweiten eindimensionalen Reihung
lädt, lässt sich dies auf Fall (b) herunterbrechen.
Es ist wünschenswert, all diese Zugriffsarten mit nur einer Spezifikation abzudecken.
Dazu transformieren wir sowohl den Muster- als auch den Arbeitsgraphen.
Normalisierung
Wir erkennen zum Einen, dass sich die Ausdrücke zur Adressberechnung aller beschriebenen Adressierungsarten aus einer Summe unterschiedlich vieler Summanden zusammensetzen. Der Zugriff auf verschiedene Vektorkomponenten unterscheidet sich nur durch
eine Konstante, alle anderen Summanden des Ausdrucks werden zur Berechnung der
Basisadresse verwendet. Um den Zugriff auf Vektorkomponenten zu erkennen, müssen
wir also überprüfen, ob alle Summanden der Adressberechnungsausdrücke bis auf die
Konstante übereinstimmen.
Im Firm-Graphen bestehen Ausdrücke zur Adressberechnung aus einer Kaskade von
Add-Knoten. Da die Addition kommutativ und assoziativ ist, können die Knoten der
Add-Kaskade von einer vorausgegangenen Optimierung (z.B. Code-Placement) beliebig
vertauscht worden sein, wenn die Programmiersprache dies erlaubt. Für die Graphersetzung wünschen wir uns aber eine einheitliche Darstellung.
Deshalb falten wir die Add-Kaskaden zu einem MultiAdd-Knoten zusammen. Ein
MultiAdd-Knoten stellt eine (nicht hardwarenahe) Additionsoperation dar, die mehrere
Summanden auf einmal verarbeiten kann. Durch die vorausgegangene CSE (Common
Subexpression Elimination1 ) ist sichergestellt, dass die Kanten verschiedener MultiAddKnoten auf denselben Knoten zeigen, falls diese denselben Wert transportieren. Um Zugriffe auf verschiedene Vektorkomponenten zu erkennen, ist die Bedingung an MultiAddKnoten, dass alle Kanten bis auf eine auf dieselbe Knotenmenge zeigen. Die verbleibende
Kante muss auf eine Konstante zeigen.
1
deutsch: Eliminierung gemeinsamer Teilausdrücke
5.3 Variantenbildung
45
Auf- und Abbau der Normalisierung
Für alle Speicheroperationen s des Programms können wir überprüfen, ob die Adressberechnung einen oder mehrere Add-Knoten enthält. Die Vorgänger der gefundenen AddKnoten sammeln wir in einer Menge M . Dann erzeugen wir einen neuen MultiAddKnoten, der die Knoten aus M als Vorgänger erhält und die neue Adresse der Speicheroperation s bildet. Wichtig ist, die abgekoppelte Add-Kaskade nicht zu zerstören,
sondern für den Abbau der Normalisierung an geeigneter Stelle zu sichern.
Speicheroperationen, die direkt auf eine Basisadresse einer Reihung zugreifen, haben
initial keinen arithmetischen Ausdruck zur Adressberechnung. In diesem Fall fügen wir
künstlich den Knoten MultiAdd(Basisadresse, 0) als Adresse ein. Dies hat den Vorteil,
dass wir auch Zugriffe auf Vektoren finden können, die an einem zur Übersetzungszeit
bekannten, konstanten Offset ungleich 0 zu einer Basisadresse beginnen. Dies können wir
erreichen, indem wir in Bedingungen der Graphersetzungsregel nicht den absoluten Wert
der Konstanten prüfen, sondern das Größenverhältnis der Konstanten untereinander.
Um die Normalisierung abzubauen, wird der MultiAdd-Knoten an allen Speicheroperationen, die nach der Optimierung noch übrig geblieben sind, wieder durch die
ursprüngliche Adresse ausgetauscht. Dies hat den Vorteil, dass vorausgegangen Optimierungen, die die Adressrechnung bereits optimiert haben, nicht noch einmal laufen
müssen.
5.3 Variantenbildung
Die Einführung von MultiAdd-Knoten allein reicht noch nicht aus, um Passungen mit
verschiedenen Adressierungsarten mit nur einer Befehls-Spezifikation zu finden. Der Unterschied zwischen den entsprechenden Ausdrücken zur Adressberechnung liegt in der unterschiedlichen Anzahl der Summanden, d.h. der unterschiedlichen Anzahl der Vorgänger
der MultiAdd-Knoten.
Daher generieren wir für jede Adressierungsart eine Variante. Die Varianten unterscheiden sich durch die unterschiedliche Anzahl der Vorgänger ihrer MultiAdd-Knoten,
wie Abbildung 5.2 zeigt. Fall (a) stellt die Variante 0 dar, wie sie nach der Normalisierung
des Mustergraphen entsteht. Fall (b) stellt Variante 1 dar, im Vergleich zu Variante 0
wurden die MultiAdd-Knoten den Vorgänger i erweitert. Im Allgemeinen enthalten pro
neuer Variante alle MultiAdd-Knoten, die Adresse innerhalb eines Vektors darstellen,
einen neuen gemeinsamen Vorgänger. Greift ein Befehl also 2 Vektoren zu, kommen pro
Variante insgesamt 2 neue Knoten hinzu. Die Anzahl der generierten Varianten kann
durch eine zuvor definierte Höchstgrenze festgelegt werden.
Bei der Mustersuche ist es wichtig, die Anzahl der Vorgänger eines MultiAdd-Knoten
im Muster mit der Anzahl der Vorgänger seiner Passung im Arbeitsgraphen abzugleichen. Ansonsten könnten im Arbeitsgraphen weitere Vorgänger vorhanden sein, was einer
46
5 Normalisierung und Varianten
0x0
Base
0x4
MultiAdd
MultiAdd
Load
Load
(a) Variante 0
0x0
i
0x4
Base
MultiAdd
MultiAdd
Load
Load
b) Variante 1
Abbildung 5.2: Automatisch generierte Varianten des Mustergraphen eines Vektor-LadeBefehls.
anderen Adressierungsart und somit nicht der Semantik des Mustergraphen entspricht.
6 Algorithmische Steuerung der
Regelanwendung
Bisher haben wir nur die Anwendung einer Graphersetzungsregel diskutiert. Enthält
unsere Regeldatenbank mehrere Regeln, gilt es zu entscheiden, welche Regeln in welcher
Reihenfolge anzuwenden sind und wann es sinnvoll ist, den Ersetzungsschritt durchzuführen. Beispielsweise kann es vorkommen, dass sich zwei oder mehrere Passungen
überlappen. Dann ist nicht sofort klar, welche Passung zur Ersetzung freigegeben werden soll. In diesem Kapitel präsentieren wir zwei verschiedene Möglichkeiten, um die
Regelanwendung zu steuern und die kostengünstigste Kombination von Passungen zur
Ersetzung auszuwählen.
6.1 Überlappung von Passungen
Wir sprechen von einer Überlappung zweier Passungen, wenn mindestens ein innerer
Knoten einer Passung ebenfalls von der anderen Passung überdeckt wird. Zum Einen
können sich mehrere Passungen eines Mustergraphen überlappen. Um das herauszufinden, weisen wir das GES bei der Anwendung einer Regel an, gleichzeitig alle im Graph
vorhandenen Passungen eines Mustergraphen zu finden und auszugeben. Wir dürfen jetzt
den Ersetzungsschritt allerdings noch nicht durchführen, da sich zum Anderen auch Passungen verschiedener Mustergraphen überlappen können. Deshalb wenden wir vor dem
Ersetzen zuerst noch weitere Regeln an, um weitere Passungen zu finden. Wir können
dann für alle gefundenen Passungen durch Vergleich der Knotenmengen herausfinden,
ob eine Überlappung vorliegt.
Wenn sich zwei beliebige Passungen p1 und p2 überlappen, ist nicht sofort klar, welche
der beiden Passungen ersetzt werden soll. Jede Ersetzung könnte das Auffinden weiterer
Passungen – wir nennen diese Folgepassungen – nach sich ziehen, die aber zu diesem
Zeitpunkt noch nicht bekannt sind (siehe Abschnitt 4.1). Wir gehen zunächst davon
aus, dass nur eine der beiden Passungen ersetzt werden darf1 , weil das Ersetzen der
Passung p1 durch Umtypisieren Knoten verändert, welche möglicherweise gleichzeitig p2
angehören. Das würde zu unvorhergesehenen Ergebnissen beim Ersetzen von p2 führen.
Um die Problematik klarer zu machen, geben wir ein konkretes Beispiel an: Wir gehen
davon aus, dass unsere Regeldatenbank genau zwei Regeln für die Befehle VectorLoad
1
In Abschnitt 6.3 heben wir diese Einschränkung auf.
47
48
6 Algorithmische Steuerung der Regelanwendung
VectorLoad 1
VectorLoad 2
VectorSub 1
Abbildung 6.1: Überlappung von Passungen der Befehle VectorMul und VectorLoad.
und VectorSub enthält. Der VectorLoad-Befehl lädt einen Vektor aus dem Speicher in
ein Spezialregister. Der VectorSub-Befehl subtrahiert zwei Vektoren komponentenweise
und kann einen der beiden Vektoren direkt aus dem Speicher laden. Den zweiten Vektor
erwartet er in einem Spezialregister. Abbildung 6.1 stellt abstrakt einen Arbeitsgraphen
dar, in dem drei Passungen der beiden Befehle zu finden sind. VectorLoad hat die beiden
Passungen VectorLoad 1 und VectorLoad 2, VectorSub hat die Passung VectorSub 1,
welche sich mit VectorLoad 2 überlappt. Wir erkennen, dass es für dieses Beispiel sinnvoll
ist, VectorLoad 1 und VectorSub 1 zu ersetzen, und die Passung VectorLoad 2 von
der Ersetzung auszuschließen, da ihre Funktionalität schon durch den VectorSub-Befehl
abgedeckt ist.
An diesem Beispiel werden zwei Probleme deutlich: Erstens kann die Passung
VectorSub 1 erst dann gefunden werden, wenn VectorLoad 1 schon ersetzt wurde, da
VectorSub 1 dessen Ergebnisse verwendet. Wir müssen also zuerst nach VectorLoadBefehlen suchen. Wird eine VectorLoad-Passung gefunden, können wir noch nicht wissen,
ob sie zu ersetzen ist, da wir nach einer Passung für VectorSub noch gar nicht gesucht
haben. Wir müssten also mit dem Ersetzen warten, bis auch die Passung VectorSub 1
gefunden wurde. Allerdings kann wie bereits beschrieben, VectorSub 1 nicht ohne einen
bereits eingefügten VectorLoad-Befehl gefunden werden. Zur Lösung dieses Problems
geben wir in den Abschnitten 6.2 und 6.3 zwei unterschiedliche Verfahren an.
Zweitens ist, um die günstigste“ Kombination von Ersetzungen zu finden, ein Kos”
tenmodell erforderlich. Dazu müssen einerseits die Kosten der Einzelbefehle bekannt
sein, und andererseits die Kosten eines reichhaltigen Befehls, woraus statisch die Kostenersparnis des reichhaltigen Befehls berechnet werden kann. Kennen wir die Kostenersparnis jedes reichhaltigen Befehls, ist es möglich, die Kombination von Ersetzungen
auszuwählen, die nach dem Modell die größte Kostenersparnis ergibt.
6.2 Suchbaum
Unser Ziel ist es also, für jede gefundene Passung kostengesteuert zu entscheiden, ob die
Passung ersetzt werden soll oder nicht. Um dies zu erreichen, könnten wir eine gefun-
6.2 Suchbaum
49
dene Passung p zunächst nur testweise ersetzen und prüfen, welche Folgepassungen in
diesem Fall gefunden werden. Zugleich merken wir uns die Gesamtkostenersparnis K1 ,
die durch das Ersetzen der Folgepassungen entsteht. Dann wäre ein Rückrollmechanismus von Nöten, um den Graph in den Zustand zurückzuversetzen, in dem er sich vor
der Ersetzung von p befand. Nun könnten wir testweise p nicht ersetzen, um zu prüfen,
welche Folgepassungen in diesem Fall gefunden werden und wiederum deren Gesamtkostenersparnis K2 zu berechnen. Dann wäre ein weiteres Rückrollen erforderlich. Wir
könnten p dann persistent ersetzten, falls K1 > K2 oder die Ersetzung von p zurückweisen, falls K1 < K2 .
Da das Rückrollen von Graphersetzungen aufwändig ist und bei diesem Verfahren pro
gefundener Passung zwei Mal durchgeführt werden muss, verfolgen wir eine etwas abweichende Strategie: Bei genauerem Hinsehen erkennen wir, dass die VProj -Knoten dafür
verantwortlich sind, dass Folgepassungen erst nach dem Ersetzen einer Passung gefunden
werden (siehe Abschnitt 4.1). Um Folgepassungen von p zu finden, müssen wir p nicht
ersetzen, sondern können einfach die VProj -Knoten künstlich in den Graph einfügen,
die bei der Ersetzung von p entstehen würden. Dadurch wird der Graph nur um diese künstlichen Knoten angereichert, alle anderen Knoten bleiben zunächst unverändert
erhalten. Der Rückrollmechanismus beschränkt sich darauf, die künstlich eingefügten
VProj -Knoten nach dem Suchen wieder zu löschen, was mit mit geringem Aufwand
durchgeführt werden kann. Dieses Vorgehen ändert zwar nicht die Komplexitätsklasse
des Algorithmus, lässt sich jedoch deutlich performanter implementieren.
Unsere Strategie sieht also folgendermaßen aus: Wir trennen die Mustersuche komplett von der Musterersetzung ab. Für eine gefundene Passung p prüfen wir einerseits,
welche Folgepassungen gefunden werden, wenn p nicht ersetzt wird. Danach prüfen
wir, welche Folgepassungen gefunden werden, wenn p ersetzt wird, indem wir künstlich
VProj -Knoten einfügen. Nachdem alle Folgepassungen gefunden wurden, entfernen wir
die VProj -Knoten wieder, um den Arbeitsgraph in den Ursprungszustand zu versetzen.
Keine der gefundenen Passungen wird ersetzt, wir merken uns stattdessen alle gefundenen (Folge-)Passungen und deren Kostenersparnisse in einer geeigneten Datenstruktur,
die wir Suchbaum nennen.
6.2.1 Aufbauen des Suchbaums
Die Struktur eines Suchbaums ist beispielhaft in in Abbildung 6.2 dargestellt, welche
einem möglichen Suchbaum des Beispiels aus Abbildung 6.1 entspricht. Ein Suchbaum
ist ein Binärbaum, dessen Knoten die gefundenen Passungen repräsentieren. Die Wurzel
des Suchbaums stellt die erste gefundene Passung p1 dar. Ausgehend von einem Knoten
p zeigt die Kante vom Typ 1 auf den Unterbaum, der die Folgepassungen enthält, die
entstehen, wenn p ersetzt wird. Dementsprechend zeigt die Kante vom Typ 0 auf den
Unterbaum, der die Folgepassungen enthält, die entstehen, wenn p nicht ersetzt wird.
Außerdem ist an jedem Knoten die Kostenersparnis Ersparnis annotiert, die wir erhalten würden, wenn der entsprechende reichhaltige Befehl eingesetzt würde. Zusätzlich
50
6 Algorithmische Steuerung der Regelanwendung
VectorLoad 2
Ersparnis: 5
Gesamtersparnis: 15
0
VectorLoad 1
Ersparnis: 5
Gesamtersparnis: 15
1
VectorSub 1
Ersparnis: 10
Gesamtersparnis: 10
1
0
0
1
VectorLoad 1
Ersparnis: 5
Gesamtersparnis: 5
0
1
1
0
Passung wird ersetzt
Passung wird nicht ersetzt
Keine Passung gefunden
Abbildung 6.2: Beispiel eines Suchbaumes
berechnen wir für jeden Knoten die Gesamtersparnis, die sich aus der Gesamtersparnis
der beiden Unterknoten Gesamtersparnis 0 und Gesamptersparnis 1 ergibt.
Gesamtersparnis = MAX(Gesamtersparnis 0 , Gesamtersparnis 1 + Ersparnis)
(6.1)
Anhand dieser Informationen kann ist im Ersetzungsschritt an jedem Knoten des
Suchbaums die nächste zu ersetzende Passung bekannt. Wir erklären aber zuerst, wie
ein solcher Suchbaum aufgebaut werden kann. Der Aufbau erfolgt nach Algorithmus 2.
Die rekursive Funktion SuchbaumAufbau bekommt die Menge R der Graphersetzungsregeln, die Menge I der zu ignorierenden Passungen und die Menge P der von zu ersetzenden Passungen überdeckten Knoten übergeben.
Die Funktion ruft zunächst FindePassung auf, die eine Passung p zu einer Regel
r aus R auswählt. p muss gültig sein nach den Kriterien aus Abschnitt 4.3. Außerdem
prüfen wir, ob p nicht in I enthalten ist, d.h. ob p nicht schon gepasst wurde. Das kann
vorkommen, denn wir suchen ja auch Folgepassungen für den Fall, dass p nicht ersetzt
wird. In diesem Fall wird der Arbeitsgraph nicht verändert und bei der Suche nach
Folgepassungen wird p möglicherweise erneut gefunden. Schließlich darf sich p nicht mit
Passungen überlappen, die bereits zum Ersetzen ausgewählt worden sind. Dazu prüfen
wir, ob sich von p überdeckte Knoten schon in P befinden. Sind all diese Kriterien erfüllt,
liefert FindePassung p zurück.
Falls FindePassung eine Passung p zurückliefert, baut die Funktion SuchbaumAufbau einen neuen Suchbaumknoten K auf. Zunächst wird p zur Menge I hinzugefügt, um
zu verhindern, dass p in einem Unterbaum von K erneut gefunden wird. Der rekursive
Aufruf von SuchbaumAufbau findet die Folgepassungen von p im Fall, dass p nicht
ersetzt wird und baut den zugehörigen Unterbaum auf. Dann werden die künstlichen
VProj -Knoten in den Graphen eingefügt, um ein Ersetzen von p zu simulieren. Dazu
6.2 Suchbaum
51
müssen zusätzlich die Eingangskanten der potentiellen Verwender der VProj -Knoten
temporär auf die erzeugten VProj -Knoten umgebogen werden. Da wir nun annehmen,
dass p ersetzt wird, müssen wir verhindern, dass sich Folgepassungen mit p überlappen.
Deswegen tragen wir die inneren Knoten von p in die Menge P ein. Jetzt baut ein weiterer
rekursiver Aufruf von SuchbaumAufbau den Unterbaum von K für den Fall, dass p ersetzt wird, auf. Sind beide Unterbäume aufgebaut, wird der Arbeitsgraph in den Zustand
versetzt, den er bei Eintritt in die Funktion hatte. D.h. die VProj -Knoten werden wieder entfernt und die inzidenten Kanten auf ihre ursprünglichen Knoten zurückgebogen.
Außerdem entfernen wir p aus I und aus P . Schließlich muss noch die Gesamtkostenersparnis an K, die nach Formel (6.1) berechnet wird, annotiert werden.
Algorithmus 2 Aufbau des Suchbaums
procedure SuchbaumAufbau(R, I, P)
p := FindePassung(R, I, P)
if Kein p gefunden then
return
end if
Füge p in I ein
K.notreplaced := SuchbaumAufbau(R)
Füge VProj Knoten ein
Füge p in P ein
K.replaced := SuchbaumAufbau(R)
Entferne p aus P
Entferne p aus I
Entferne VProj Knoten
Berechne Kosten für K
return K
end procedure
procedure FindePassung(R, I, P)
Finde gültige Passung p für ein r aus R, p ∈
/ I ∧p*P
return p
end procedure
6.2.2 Ersetzen der günstigsten Passungen
Im Suchbaum befinden sich nun alle Passungen, die im Arbeitsgraphen gefunden werden können. Beginnend bei der Wurzel durchlaufen wir den günstigsten Pfad durch
den Baum. An jedem Knoten können wir direkt entscheiden, welcher Unterknoten als
nächstes besucht und ob die Passung p ersetzt werden soll, indem wir die Gesamtkosten Gesamtersparnis 0 und Gesamtersparnis1 der Unterknoten vergleichen. Der Fall
Gesamtersparnis 1 < Gesamtersparnis 0 bedeutet, dass es günstiger ist p nicht zu ersetzen. Wir ignorieren in diesem Fall p und fahren mit dem Unterbaum 0 fort. Falls
52
6 Algorithmische Steuerung der Regelanwendung
VectorLoad 1
VectorLoad 2
VectorSub 1
Prioritätsklasse 0
VectorAdd
Prioritätsklasse 1
Abbildung 6.3: Überlappungsproblem mit mehreren Prioritätsklassen
Gesamtersparnis1 > Gesamtersparnis 0 ist es günstiger, p zu ersetzen. Wir ersetzen in
diesem Fall p wie in Kapitel 4 beschrieben und fahren mit dem Unterbaum 1 fort.
An dieser Stelle sei noch angemerkt, dass bei diesem Verfahren eine weitere Feinheit
zu beachten ist: Die im Suchbaum enthaltenen Passungen enthalten u.A. Verweise auf
die künstlich eingefügten VProj -Knoten, die aber wieder entfernt wurden. Während des
Suchbaum-Aufbaus merken wir uns deshalb zusätzlich für jeden VProj -Knoten, welche
Ersetzung den VProj -Knoten erzeugen würden. Während des Ersetzungsschritts greifen
wir auf diese Information zurück und ersetzen Verweise auf künstliche VProj -Knoten
durch Verweise auf die persistenten VProj -Knoten.
Auf diese Weise ersetzen wir nur die Passungen, die nach unserem Kostenmodell
das günstigste Programm erzeugen. Alle anderen Passungen werden unersetzt wieder
freigegeben.
6.2.3 Prioritätsklassen
Wie schon diverse Backend-Implementierungen gezeigt haben [5], kann das Ausprobieren
aller Möglichkeiten, wie es bei diesem Verfahren geschieht, sehr zeitaufwändig werden.
Pro gefundener Passung kann sich der Suchbaum im schlechtesten Fall exponentiell vergrößern. Außerdem werden in unterschiedlichen Teilbäumen möglicherweise oft dieselben
Passungen gefunden.
Um Suchbäume klein zu halten, erstellen wir einen Suchbaum pro Prioritätsklasse.
Dieser Ansatz ist dann günstig, wenn Mustergraphen höherer Prioritätsklasse nur dann
gefunden werden können, wenn Mustergraphen niedriger Prioritätsklassen bereits gefunden und ersetzt wurden. Typischerweise ist das bei Befehlssätzen so, bei denen Ladeoperationen, arithmetische Operationen und Speicheroperationen strikt getrennt sind. Es
gibt jedoch auch prioritätsklassenübergreifende Abhängigkeiten, die mit diesem Ansatz
nicht erkannt werden können.
6.3 Musterselektion mittels PBQP
53
Ziehen wir dazu nocheinmal das Beispiel aus Anschnitt 6.1 heran. Zusätzlich sei
der Befehl VectorAdd in der Regeldatenbank, der zwei Vektoren in Registern erwartet und diese komponentenweise addiert. Alle Befehle, die aus dem Speicher laden, seien
der Prioritätsklasse 0 zugeteilt, Befehle, die rein auf Registern arbeiten, der Prioritätsklasse 1. Abbildung 6.3 zeigt das entstehende Problem: Die beiden Befehle VectorLoad
und VectorSub wurden in Prioritätsklasse 0 eingeteilt, was dazu führt, dass VectorLoad 1
und VectorSub 1 ersetzt werden, VectorLoad 2 wird nicht ersetzt, gestrichelt gezeichnet.
Erst jetzt arbeiten wir die Befehle in Prioritätsklasse 1 ab. Der Befehl VectorAdd kann
nicht erkannt werden, da VectorLoad 2 zurückgewiesen wurde. Wir können durch die
Einführung von Prioritätsklassen möglicherweise klassenübergreifende Zusammenhänge
nicht erkennen. Unsere Tests haben jedoch gezeigt, dass trotz Verwendung von Prioritätsklassen in vielen Fällen gute Ergebnisse erzielt werden können.
6.3 Musterselektion mittels PBQP
Um dem Problem von zu großen Suchbäumen und zu langen Laufzeiten der Musterauswahl Sorge zu tragen, machen wir in diesem Abschnitt einen Exkurs zu einem alternativen Verfahren der Musterauswahl. Unser Ziel ist es, die Musterauswahl auf ein
PBQP2 abzubilden. Wir beschreiben in den folgenden Abschnitten das PBQP genauer
und erörtern, welche Änderungen an unserem bisherigen Ansatz nötig sind. Schließlich
zeigen wir am Beispiel, wie der neue Ansatz funktioniert.
6.3.1 PBQP
Ausgangslage eines PBQPs ist ein ungerichteter Graph, an dessen Knoten unterschiedlich viele, kostenbehaftete Alternativen existieren. Ziel eines PBQP-Lösers ist es, an
jedem Knoten des Graphen genau eine Alternative auszuwählen, so dass die Gesamtkosten minimiert werden. Zusätzlich zu den Kosten der einzelnen Alternativen, die als
Kostenvektor an jedem Knoten annotiert sind, gibt es sogenannte Übergangskosten, die
an den Kanten des Graphen stehen. Die Übergangskosten beschreiben, welche zusätzlichen Kosten entstehen, wenn an den beiden durch eine Kante verbundenen Knoten
die Alternativen Ai und Aj ausgewählt werden. Alle Übergangskosten an einer Kante
werden deshalb als Matrix Mij dargestellt. Einen Beispielgraph zeigt Abbildung 6.4. Die
Kosten der Alternativen sind an jedem Knoten n als Vektoren cn , die Übergangskosten
an jeder Kante zwischen den Knoten n und m als Matrix C (n,m) eingezeichnet.
PBQP wurde erstmals von Eckstein und Scholz [10] zur Befehlsauswahl eingesetzt und
von Jakschitsch [19] erweitert. Dazu wird die Knoten- und Kantenstruktur des SSAGraphen übernommen, um den PBQP-Graphen zu erhalten. Wir geben ein ähnliches
Verfahren an, um das Problem der Auswahl verschiedener Passungen auf ein PBQP
abzubilden.
2
Partitioned Boolean Quadratic Problem
54
6 Algorithmische Steuerung der Regelanwendung
c 3 = (10, 20)
C(1, 3) = ( 3 4 )
c1 = (15)
3
1
C(4, 3) = ( 3 4 )
4
c4 = (40)
( )
C(2, 3) = 7 8
48
C(1, 2) = ( 2 3 )
2
c2 = (15, 20)
Abbildung 6.4: Ein PBQP als Graph aufgefasst.
6.3.2 Änderungen am bisherigen Ansatz
Die Abbildung der Musterauswahl auf ein PBQP verlangt, dass alle möglichen Passungen im Arbeitsgraph gleichzeitig gefunden werden können, da wir im Vorraus bereits alle
Alternativen kennen müssen. Das Finden einer Passung darf nicht von vorangegangen
Ersetzungsschritten abhängig sein. Nach dem bisherigen Ansatz hindern uns daran die
expliziten VProj -Knoten, die Passungen voneinander abhängig machen. Wie benötigen
einen neuen Ansatz, durch den VProj -Knoten nicht mehr explizit im Graph dargestellt
werden. VProj -Knoten in bisherigen Mustergraphen bedeuten, dass der reichhaltige Befehl Vektordaten als Eingabe erwartet. Wir schaffen nun die expliziten VProj -Knoten in
Mustergraphen ab; alle VProj -Knoten werden durch allgemeine IR Knoten ersetzt. Dadurch werden möglicherweise zunächst auch Passungen gefunden, deren Operanden keine
Vektorkomponenten repräsentieren. Dies kann man in einem zweiten Schritt überprüfen
und eventuell Konvertierungen vornehmen oder die Passung verwerfen.
Der große Vorteil dieses Verfahrens ist, dass die erfolgreiche Mustersuche nicht mehr
davon abhängt, dass bereits VProj Knoten in den Graph eingefügt worden sind. Alle
Muster können unabhängig voneinander und ohne eine Ersetzung durchgeführt zu haben, gefunden werden. Die Prioritätsklassen fallen weg. Mittels PBQP soll nun die kostengünstigste Kombination von Ersetzungen herausgefunden werden. Dies ist insbesondere dann interessant, wenn sich Muster überlappen. Bisher hatten wir nur die Möglichkeit, genau ein Muster aus der Menge der sich überlappenden Muster auszuwählen. Da
nun VProj -Knoten wegfallen und deshalb auch kein Umtypisieren von Knoten mehr
stattfindet, ist es möglicherweise auch geschickt, mehrere der sich überlappenden Passungen gleichzeitig zur Ersetzung freizugeben. Das führt zwar dazu, dass gemeinsame
Teilausdrücke mehrfach berechnet werden, kann jedoch trotzdem einen Kostenvorteil
bringen, wenn die Ergebnisse aller ausgewählten Passungen durch weitere reichhaltige
Befehle verarbeitet werden können.
Der neue Ansatz bringt auch Änderungen beim Ersetzungsschritt mit sich. Wir finden
nun im Arbeitsgraphen nicht mehr explizit die Information vor, welche Vektorkompo-
6.3 Musterselektion mittels PBQP
55
nente ein Knoten darstellt. Diese Information muss extra gespeichert werden. Da wir
nun mehrfache Verwendung von gemeinsamen Teilausdrücken erlauben, ist Retypisieren nicht mehr erlaubt. Die Ergebnisse des reichhaltigen Befehls können deshalb nicht
mehr durch eine Graphersetzungsregel mit ihren Verwendern verbunden werden, dies
muss manuell oder mit Hilfe mehrerer kleinerer, manuell gesteuerter Regeln geschehen.
Dadurch werden die Fähigkeiten des Graphersetzungssystems weniger stark ausgenutzt.
Die Hauptaufgabe des GES besteht bei diesem Ansatz in der Mustersuche.
6.3.3 Erstellen des PBQP-Graphen
In den folgenden Abschnitten beschreiben wir, wie der PBQP-Graph entsteht und
erläutern, wie die Knoten- sowie Kantenkosten berechnet werden. Den initialen Graphen erhalten wir, indem wir die Knoten- und Kantenstruktur des SSA-Graphen übernehmen und alle gerichtete Kanten in ungerichtete umwandeln. Dann müssen an jedem
Knoten die Alternativen und deren Kosten sowie an jeder Kante die Übergangskosten
eingetragen werden.
Alternativen
Jeder Knoten erhält initial die Alternative ”nicht überdeckt“, dargestellt durch das Symbol ⊗. Danach setzen wir das GES ein, um nach Abschnitt 6.3.2 veränderte Mustergraphen zu suchen. Die n gefundenen Passungen werden nach dem Schema p1 bis pn
durchnummeriert. Wird ein Knoten im SSA-Graphen von der Passung pi überdeckt, so
erhält seine Entsprechung im PBQP-Graph die Alternative Ai := {pi }.
Nach diesem Schritt hat ein Knoten n + 1 Alternativen, falls er von n Passungen
überdeckt wird. Wir möchten nun den Umstand modellieren, dass auch mehrere überlappende Passungen gleichzeitig ersetzt werden können. Der PBQP Ansatz gestattet
es nicht, mehrere Alternativen gleichzeitig auszuwählen. Um mehrere sich überlappende Passungen zur Ersetzung freigeben zu können, gehen wir vor, wie von Jakschitsch
[19, Kapitel 5] beschrieben: Wir erzeugen weitere Alternativen, die mehrere Passungen
gleichzeitig darstellen. Dazu bestimmen wir an jedem Knoten die Potenzmenge P der
Menge der überdeckenden Passungen und fügen die neu hinzugekommenen Kombinationen als neue Alternativen hinzu. Wird beispielsweise ein Knoten von Passung p1 und
p2 überdeckt, so sind die Alternativen A := ({⊗}, {p1 }, {p2 }, {p1 , p2 }). Das kann sich
dann lohnen, wenn die Ergebnisse aller sich überlappenden Passungen durch reichhaltige Befehle weiterverwendet und so die zusätzlichen Kosten wieder eingespart werden
können.
Überlappende Passungen pn und pm treten dann in Konflikt, wenn in beiden Passungen Speicheroperationen vorkommen und mindestens eine davon den Speicher verändert.
In einem solchen Fall muss die simultane Auswahl der Passungen verhindert werden, indem die Alternativen entfernt werden, in denen pn und pm gemeinsam vorkommen.
56
6 Algorithmische Steuerung der Regelanwendung
Knotenkosten
Die an jedem Knoten existierende Alternative ⊗ erhält im Kostenvektor die Kosten des
dem Knoten entsprechenden Einzelbefehls, da bei deren Auswahl ein Knoten nicht von
einer Passung überdeckt wird und somit erhalten bleibt. Die Gesamtkosten einer Passung
pm entsprechen den Kosten des einzufügenden reichhaltigen Befehls. Diese Gesamtkosten werden über die Knoten der Passung verteilt. Wird eine Alternative aus mehreren
Passungen gebildet, summieren sich deren Kosten auf. An jedem Knoten k entstehen
also für die Alternative Ai die Kosten
X Gesamtkosten(pm )
(6.2)
Kosten(k, Ai ) =
Anzahl Knoten(pm )
∀pm ∈Ai
Diese Kostenverteilung auf alle Knoten einer Passung ist mit PBQP vereinbar: Da
ein PBQP-Löser die Gesamtkosten des Graphen minimiert, werden die Einzelkosten
der Knoten aufsummiert. Die verteilten Kosten an den Knoten einer Passung ergeben
also aufsummiert wieder die Kosten der reichhaltigen Befehls. Damit sind wir von der
Entscheidung befreit, an welchem Knoten einer Passung die Kosten zu annotieren sind
(vgl. Jakschitsch [19, Abschnitt 4.5.4]).
Kantenkosten
Ein PBQP-Löser wählt an jedem Knoten des Arbeitsgraphen genau eine Alternative
aus. Wir fordern zusätzlich, dass falls an einem beliebigen Knoten einer Passung p eine
Alternative gewählt wird, die p enthält, so müssen auch an allen anderen Knoten der
Passung Alternativen gewählt werden, die p enthalten. Eine Passung darf also immer
nur komplett oder gar nicht ausgewählt werden. Durch die Kantenkosten stellen wir
sicher, dass diese Forderung erfüllt wird. Aus diesem Grund stellen wir die folgenden
Bedingungen an den Übergang von Alternative Ai an Knoten X zur Alternative Aj
an Knoten Y auf. Man beachte, dass ein PBQP ungerichtet ist und die Bedingungen
deshalb für beide Richtungen einer Kante geprüft werden müssen. Die Folgerung aus
diesen Bedingungen geben wir im Anschluss an.
1. Ist ein px ∈ Ai aber px ∈
/ Aj , darf px in keinem Al an Knoten Y vorkommen.
Ansonsten ist der Übergang nicht erlaubt, d.h. die Übergangskosten sind ∞.
2. Der Übergang von Ai 6= {⊗} nach Aj = {⊗} ist nur erlaubt, wenn an Y kein px
aus Ai enthalten ist. Eventuell müssen Konvertierungskosten veranschlagt werden,
falls X ein Ergebnisknoten oder Erhaltungskosten, falls X ein innerer Knoten des
Musters ist (siehe nächster Abschnitt).
Erlaubte Übergänge erhalten die Kosten 0. Eventuell notwendige Konvertierungskosten werden zusätzlich aufgeschlagen. Nicht erlaubte Übergänge erhalten die Kosten ∞.
Als Folge aus diesen Regeln ergibt sich:
6.3 Musterselektion mittels PBQP
1.
57
a) Ist an Knoten X die Passung px ausgewählt, und wird Knoten Y ebenfalls
von px überdeckt, muss an Knoten Y ebenfalls px ausgewählt werden.
b) Ist an Knoten X die Passung px nicht ausgewählt, und wird Knoten Y ebenfalls von px überdeckt, darf an Knoten Y px nicht ausgewählt werden.
2.
a) Ein reichhaltiger Befehl darf nur eingefügt werden, wenn es möglich ist, seine
Operanden so zu konvertieren, dass er sie verarbeiten kann (z.B. durch Packen
von einzelnen Werten in Spezialregister).
b) Ergebnisse eines reichhaltigen Befehls müssen eventuell konvertiert werden,
z.B. durch Umkopieren von Komponenten aus Spezialregistern in Allgemeinregister. Ist das nicht möglich, darf der reichhaltige Befehl nicht eingesetzt
werden.
c) Übergänge zwischen zwei Knoten mit der Alternative ⊗ sind immer möglich.
Konvertierungskosten
Trotz der Symmetrie eines PBQP müssen wir bei der Berechnung von Konvertierungskosten zusätzlich die Richtung der entsprechenden SSA-Kante beachten. Wenn beim
Übergang von Knoten X nach Knoten Y in Richtung der Kante im SSA-Graphen eine Passung hinzukommt“, handelt es sich bei Knoten Y um einen Operanden eines
”
reichhaltigen Befehls. Diese Übergangskosten dürfen nur 0 sein, wenn der reichhaltige
Befehl den Wert des Knotens Y direkt verarbeiten kann. Das bedeutet, der reichhaltige Befehl kann skalare Werte direkt verarbeiten oder es handelt sich bei Y um eine
Vektorkomponente, die durch einen voranstehenden reichhaltigen Befehl bereit gestellt
wird. Ansonsten muss Knoten X erhalten bleiben und es müssen Konvertierungskosten
veranschlagt werden. Kann nicht konvertiert werden, ist der Übergang nicht erlaubt,
d.h. Übergangskosten sind unendlich oder die entsprechende Alternative wird gelöscht.
Fällt dagegen beim Übergang von Knoten X nach Knoten Y in Richtung der Kante
im SSA-Graphen eine Passung weg, muss geprüft werden, ob der Wert an Knoten X
direkt von anderen reichhaltigen Befehlen weiterverwendet werden kann oder ob Konvertierungsoperationen für Einzelbefehle notwendig sind.
Durch die fehlenden VProj -Knoten wird also eine Abhängigkeitsanalyse notwendig,
die prüft, welche Befehle Ergebnisse von Befehlen Passungen weiterverwenden. Die Ergebnisse dieser Analyse können dazu verwendet werden, die Passungen in der korrekten
Reihenfolge zu ersetzen, so dass die Operanden von neu eingefügte reichhaltige Befehle
direkt mit ihren Vorgängern verbunden werden können.
Unabhängige Teilmuster
Durch die in diesem Abschnitt beschriebene Änderung der Mustergraphen, können diese
in unabhängige Teilmuster zerfallen. Bisher wurden die Berechnungen, die auf einzelnen Vektorkomponenten stattfinden, durch die VProj -Knoten und deren gemeinsame
58
6 Algorithmische Steuerung der Regelanwendung
p1 (VectorLoad)
p4 (V
Basis 1
Load 1
Load 2
erarb
eite
Load 3
+
4)
Load 4
+
p2 (HorizontalAdd)
Teuer
Basis 2
Store 1
p3 (VectorStore)
Store 2
Abbildung 6.5: Ein Firm-Graph mit Passungen einiger reichhaltiger Befehle
Vorgänger aneinander gekoppelt. Da die VProj -Knoten nun weggefallen sind, ist das
nicht mehr der Fall. Zwischen den Teilmustern gibt es keine Kante, wodurch wir auch
keine Übergangskosten angeben können. Hier funktioniert der PBQP-Ansatz nicht mehr,
da wir allein durch die Übergangskosten garantieren, dass eine Passung px an allen Knoten, die sie überdeckt, konsistent ausgewählt wird. Wir können diese Teilmuster aber
durch künstliche, nicht im SSA-Graph vorhandene Kanten, verbinden. Für die künstliche Kante gelten dieselben Regeln zur Übergangskostenberechnung wie für normale
Datenflusskanten.
6.3.4 Musterauswahl per PBQP am Beispiel
Abbildung 6.5 zeigt einen vereinfachten SSA-Graphen eines Programms, das vier aufeinanderfolgende Speicherstellen lädt, jeweils zwei der geladenen Werte addiert und
die Ergebnisse der Addition wieder abspeichert. Statt der Einzelbefehle könnten auch
die reichhaltigen Befehle VectorLoad (4 Komponenten), HorizontalAdd und VectorStore
(2 Komponenten) eingesetzt werden. Die zugehörigen Passungen p1 , p2 und p3 sind in
der Abbildung in unterschiedlichen Grautönen hinterlegt. Zusätzlich gibt es eine teure
Einzeloperation Teuer, die die 4. Vektorkomponente ebenfalls verarbeitet. Das Laden der
4. Vektorkomponente und Ausführen der teuren Operation könnte auch von einem billigeren reichhaltigen Befehl Verarbeite 4 erledigt werden; seine Passung p4 ist dunkelgrau
hinterlegt.
p4 überlappt sich mit p1 und p2 3 . Es stellt sich nun die Frage, ob es günstiger ist,
die 4. Vektorkomponente durch den reichhaltigen Befehl Verarbeite 4 erneut laden und
3
p1 und p2 sowie p2 und p3 überlappen sich nur scheinbar. Im Bereich deren Überlappung“ befinden
”
sich lediglich Operanden bzw. Ergebnisknoten.
6.3 Musterselektion mittels PBQP
Befehl
Vector Load
Vector Add
Vector Store
Verarbeite 4
Passung
p1
p2
p3
p4
Kosten
10
2
3
6
59
Knotenanzahl
5
6
5
3
Kosten pro Knoten
2
0,33
0,6
2
Tabelle 6.1: Über die Knoten einer Passung verteilte Kosten von reichhaltigen Befehlen.
verarbeiten zu lassen, oder falls möglich das Ergebnis des VectorLoad Befehls zu konvertieren und durch die Einzeloperation Teuer verarbeiten zu lassen. Möglicherweise ist es
auch günstiger, VectorLoad und die davon abhängigen Befehle gar nicht zu verwenden.
Die Lösung hängt von den Kosten der Einzel- und reichhaltigen Befehle ab und soll
von einem PBQP-Löser gefunden werden. Dazu berechnen wir wie in Abschnitt 6.3.3
beschrieben die Knoten- und Kantenkosten des PBQP-Graphs:
Alle Einzelbefehle haben die Kosten 3, mit Ausnahme der Operation Teuer, der wir
die Kosten 5 zuweisen. Die Kosten der reichhaltigen Befehle sind in Tabelle 6.1 zusammengefasst. Wichtig sind hierbei die Kosten pro Knoten, die wir im nächsten Schritt an
die einzelnen Alternativen der Knoten zuweisen.
Dazu geben wir beispielhaft für den Knoten Load 1 die Alternativen sowie deren
Kosten an. Load 1 wird durch die Passungen p1 sowie p2 gleichzeitig überdeckt. Um zu
modellieren, dass an Knoten Load 1 die Möglichkeiten bestehen (1) keine der beiden
Passungen zu ersetzen, (2) nur p1 zu ersetzen, (3) nur p2 zu ersetzen oder (4) p1 und p2
zu ersetzen, erhält er die Alternativen A := ({⊗}, {p1 }, {p2 }, {p1 , p2 }). Die Kosten der
Alternative {⊗} entsprechen den Kosten des Einzelbefehls, also 3. Die Alternativen {p1 }
und {p2 } erhalten die Kosten 2 und 0, 33, wie aus Tabelle 6.1 auszulesen ist. Schließlich
addieren sich die Kosten von p1 und p2 , um die Kosten der Alternative {p1 , p2 } zu
erhalten, folglich 2, 33. In Tabelle 6.2 sind die Alternativen der weiteren Knoten inklusive
deren Kosten dargestellt.
Es fehlen jetzt noch die Übergangskosten an den Kanten, die wir mit Hilfe der Regeln
aus Abschnitt 6.3.3 aufstellen. Wir ziehen als Beispiel die Kante Basis 1 → Load 1 heran.
Die komplette Übergangsmatrix ist in Tabelle 6.3 (a) dargestellt. In den Zeilen sind die
Alternativen an Knoten Basis 1, in den Spalten die an Knoten Load 1 aufgetragen. Wir
besprechen die Matrix nun zeilenweise: (1) Alternative ⊗ an Basis 1 darf nicht nach p1
oder p1 p2 übergehen. Ansonsten könnte p1 unvollständig ausgewählt werden. Sie darf
auch nicht nach p2 übergehen, da p2 nicht die Ergebnisse der Einzelbefehle verarbeiten
und daher nicht ohne p1 ersetzt werden kann. (2) Alternative p1 an Basis 1 darf nicht
nach ⊗ oder p2 übergehen, da p1 ansonsten unvollständig ausgewählt werden könnte.
(3) Wird an Basis 1 nur p4 ausgewählt, dürfen p1 und p1 p2 nicht gewählt werden, sonst
wäre möglicherweise p1 unvollständig. p2 darf nicht gewählt werden, da sie nicht ohne
p1 ersetzt werden kann. (4) In Zeile 4 greifen dieselben Regeln wie in Zeile 2.
Eine weitere Übergangsmatrix für den Übergang Load 4 → T euer ist in Tabelle 6.3 (b)
60
6 Algorithmische Steuerung der Regelanwendung
Knoten
Base
Load 1
...
Load 4
+1
...
Store 1
Teuer
Alternativen
(⊗, p1 , p4 , p1 p4 )
(⊗, p1 , p2 , p1 p2 )
...
(⊗, p1 , p2 , p4 , p1 p2 , p1 p4 , p2 p4 , p1 p2 p4 )
(⊗, p2 , p3 , p2 p3 )
...
(⊗, p3 )
(⊗, p4 )
Kosten
(3, 2, 2, 4)
(3, 2, 0.33, 2.33)
...
(3, 2, 0.33, 2, 2.33, 4, 2.33, 4.33)
(3, 0.33, 30, 0.99)
...
(3, 0.66)
(20, 2)
Tabelle 6.2: Kosten der Alternativen pro Knoten.
angegeben. Zu bemerken sind hier die Kosten 100 beim Übergang von Alternative p1
nach p4 und von p1 p2 nach p4 . Das sind Konvertierungskosten, die anfallen, da in diesen
Fällen das Ergebnis des VectorLoad-Befehls so konvertiert werden muss, damit es von
der Operation Teuer verarbeitet werden kann.
Alle weiteren Übergangsmatrizen wurden ebenfalls nach diesen Regeln erstellt. Als
nächstes übergeben wir den PBQP-Graph inklusive aller Alternativen und deren Kosten,
sowie der Übergangskosten an einen PBQP-Löser.
Ergebnisse
Wir setzen einen PBQP-Löser ein, um die Alternativen mit den minimalen Gesamtkosten
berechnen zu lassen. Das Ergebnis des Experiments ist, dass mit oben festgelegten Kosten
alle Passungen p1 , p2 , p3 und p4 zur Ersetzung freigegeben werden. Das ist korrekt, denn
die Kosten der reichhaltigen Befehle VectorLoad, HorizontalAdd und VectorStore sind
geringer als die Kosten der Einzelbefehle. Es lohnt sich in diesem Fall, auch p4 zu ersetzen,
obwohl die vierte Vektorkomponente doppelt geladen wird. Der Grund ist, dass die durch
die Ersetzung von p1 , p2 und p3 gewonnenen Einsparungen groß genug sind, um dies zu
kompensieren und zusätzlich sind die Kosten zur Konvertierung des Ergebnisses des
VectorLoad-Befehls für die Operation Teuer sehr hoch. Die Gesamtkosten der Auswahl
belaufen sich auf 21.
Wir übergeben das PBQP nun nochmals dem PBQP-Löser, allerdings lassen wir jetzt
eine günstige Konvertierung des Ergebnisses des VectorLoad-Befehls zu, d.h. wir ersetzen
die Werte 100 aus Tabelle 6.3 (b) durch 0. Nun fallen für die Operation Teuer nur die
Kosten 5 an, während für die Operation Verarbeite 4 die Kosten 6 anfallen würden. Es
ist in diesem Fall also günstiger, die Operation Teuer auszuführen, da die Konvertierung
ihres Operanden keine Kosten verursacht, statt durch den Befehl Verarbeite 4 die vierte
Vektorkomponente ein zweites Mal zu laden. Folglich werden nur p1 , p2 und p3 zur
Ersetzung freigegeben, p4 zurückgewiesen. Die Gesamtkosten sind damit 20.
Wir verändern die Kosten des PBQP nun abermals, so dass der VectorStore-Befehl
6.3 Musterselektion mittels PBQP
(a)
Basis1→Load1
⊗
p1
p4
p1 p4
⊗
0
∞
0
∞
61
(b)
p1
∞
0
∞
0
p2
∞
∞
∞
∞
p1 p2
∞
0
∞
0
Load4→Teuer
⊗
p1
p2
p4
p1 p2
p1 p4
p2 p4
p1 p2 p4
⊗
0
100
∞
∞
100
∞
∞
∞
p4
∞
∞
∞
0
∞
0
0
0
Tabelle 6.3: Übergangskosten ausgewählter Kanten.
sehr teuer ist, indem wir ihre Kosten auf 100 erhöhen. Außerdem sei eine Konvertierung
des Ergebnisses des VectorAdd-Befehls, so dass sie durch einzelne Store-Operationen
verarbeitet werden können, ebenfalls teuer. Dann wählt der PBQP-Löser korrekt keine
Passung zur Ersetzung aus. p1 , p2 und p3 lohnen sich nun nicht mehr. Es lohnt sich
ebenfalls nicht, p4 zu ersetzen, da in diesem Fall die Operation Load 4 trotzdem stehen
bleiben müsste. Die doppelte Ausführung von Load 4 lohnt sich aber in diesem Fall
nicht, da die Einsparungen durch den Befehl Verarbeite 4 zu gering sind. Also wird auch
p4 nicht zur Ersetzung freigegeben. Die Gesamtkosten sind in diesem Fall 35.
6.3.5 Bewertung
Wir haben an einem Beispiel gezeigt, wie die Musterauswahl auf ein PBQP abgebildet
werden kann. Der große Vorteil ist, dass zur Lösung des im Grunde NP-vollständigen
Problems der Musterauswahl ein generischen PBQP-Löser eingesetzt werden kann, der
heuristische Verfahren zur Lösung verwendet. Auch wenn viele Passungen im Arbeitsgraph gefunden werden, arbeitet der PBQP-Löser aufgrund der Charakteristik unserer
Graphen noch effizient [19].
Durch die generischeren Mustergraphen werden deutlich mehr Muster gefunden als
beim bisherigen Ansatz. Viele der gefundenen Passungen müssen wieder aussortiert werden, jedoch ist es auch möglich zu prüfen, ob Werte aus verschiedenen Einzelregistern
in ein Vektorregister gepackt werden können. Bisher werden die Vektorregister nur über
Vektorlade-Befehle befüllt.
Dieses Verfahren bringt jedoch einige Änderungen an unserem ursprünglichen Verfahren mit sich. Es gibt keine VProj -Knoten mehr, die explizit im Graph die Information
darstellen, ob ein Wert eine Vektorkomponente darstellt. Wir müssen also erst durch
geeignete Analysen herausfinden, welche Knoten potentielle Ergebnisknoten von reichhaltigen Befehlen darstellen und welche anderen reichhaltigen Befehle diese Ergebnisse
verwenden können. Aus Zeitgründen beinhaltet unsere Implementierung deshalb nur das
62
6 Algorithmische Steuerung der Regelanwendung
in Abschnitt 6.2 beschriebene, suchbaumbasierte Verfahren.
7 Implementierung und Messungen
In diesem Kapitel gehen wir auf den Aufbau unserer Implementierung ein, mit deren Hilfe
wir Messungen durchgeführt haben. Die Messergebnisse und deren Bewertung stellen wir
zum Abschluss des Kapitels vor.
7.1 Architektur
Unsere Implementierung ist modular aufgebaut. Sie lässt sich grob in einen statischen
Teil und einen dynamischen Teil unterteilen.
initialer Mustergraph
Graph−Normalisierer
Graph−Analysierer
GrGen−
Regelgenerator
Backend−
Regelgenerator
Datenbank−
Generator
Abbildung 7.1: Architektur des Moduls Regelerzeuger
Der statische Teil entspricht der Regelerzeugung und ist in Abbildung 7.1 dargestellt. Der Regelerzeuger bekommt als Eingabe die initialen Zwischensprachengraphen
der Befehls-Spezifikation. Diese werden zunächst durch den Graph-Normalisierer nach
den in Kapitel 5 beschriebenen Verfahren in eine einheitliche Form gebracht. Aus den normalisierten Graphen extrahiert der Graph-Analysierer alle Informationen, die zusätzlich
zum Verhalten der reichhaltigen Befehle angegeben wurden. Diese Analyseinformation
wird einerseits vom GrGen-Regelgenerator dazu verwendet, den initialen Graphen zu
beschneiden und Muster- und Ersetzungsgraph zu erzeugen, woraus wiederum die Graphersetzungsregeln entstehen. Andererseits verwendet der Backend-Regelgenerator die
Analyseinformation, um Knotenspezifikationen für das IA32-Backend [28] zu erzeugen.
Dadurch kann das Backend für die neu eingeführten Maschinensprachenknoten auto-
63
64
7 Implementierung und Messungen
Regeldatenbank
zu optimierender Firm−Graph
Graph−Normalisierer
Suchbaum−Generator
Firm GES
Filter
Ersetzer
Firm GES
Nachbearbeiter
optimierter Firm−Graph
Abbildung 7.2: Architektur des Moduls Regelanwender
matisch Code ausgeben. Schließlich speichert der Datenbank-Generator die erzeugten
Regeln in geeigneter Form ab.
Ist die Regeldatenbank erstellt, können die Regeln durch den Regelanwender, der dem
dynamischen Teil der Implementierung und der eigentlichen Optimierung entspricht, ausgeführt werden. Zwischen der Regelgenerierung und Regelanwendung muss der Übersetzer neu übersetzt werden, da er die generierte Backend-Spezifikation nicht dynamisch
einlesen kann, sondern diese statisch mitübersetzt werden muss.
Der Regelanwender ist in Abbildung 7.2 dargestellt. Dieser erhält als Eingabe den
zu optimierenden Zwischensprachengraphen sowie Informationen über die Regeldatenbank. Zunächst wird der zu optimierende Graph durch den Graph-Normalisierer auf eine einheitliche Form gebracht. Der normalisierte Graph wird dem Suchbaum-Generator
übergeben, der nach den Methoden in Abschnitt 6.2 die möglichen Passungen sucht. Zur
Mustersuche ruft er wiederholt das Graphersetzungssystem Firm GES auf. Wird eine
Passung gefunden, wird diese durch den Filter nach den Methoden aus Abschnitt 4.3
auf Gültigkeit überprüft. Den fertigen Suchbaum verwendet der Ersetzer, um die kostengünstigste Kombination von Graphersetzungsregeln auszuwählen und anzuwenden.
Dazu ruft er wieder für jede anzuwendende Regel Firm GES auf, welches die eigentliche
Ersetzung durchführt. Der Nachbearbeiter reiht, wie in Abschnitt 4.2 beschrieben, den
neu eingefügten reichhaltigen Befehl in die Speicherabhängigkeitskette ein. Schließlich
steht nach dem Anwenden aller Graphersetzungsregeln der optimierte Zwischensprachengraph zur Verfügung, welcher durch das Übersetzer-Backend zu ausführbarem Code
verarbeitet wird.
7.2 Messergebnisse
65
7.2 Messergebnisse
Unser Testrechner ist mit einem Intel Pentium 4 Prozessor (Prescott Kern, 3,2 GHz)
ausgestattet, der sich durch die SSE3 Befehlssatzerweiterung auszeichnet. Das System
verfügt über 2 GByte Hauptspeicher. Als Betriebssystem verwenden wir SUSE Linux 9.3.
7.2.1 Bewegungsschätzung
Unser erstes Beispiel implementiert einen sogenannten Block-Matching-Algorithmus zur
Bewegungsschätzung1 , wie er bei der MPEG Kompression in Video-Codecs Einsatz findet [17]. Der Algorithmus operiert auf einem Feld von der Größe 256x256 Byte und wird
testweise 100 mal ausgeführt. Das Quellprogramm ist in Listing 7.1 zu sehen.
Listing 7.1: Block-Matching
1
2
3
4
5
unsigned int sad(int test_blockx, int test_blocky, int *best_block_x,
int *best_block_y, unsigned char frame[256][256])
{
int i, x, y, blocky, blockx;
unsigned tmp_diff, min_diff = 0xFFFFFFFF;
6
// Iterate over whole frame; x,y=coords of current block
for(x = 1; x < 256 - 16; x++)
for(y = 0; y < 256 - 16; y++) {
tmp_diff = 0;
// Compare current block with reference block
for(blocky = 0; blocky < 16; blocky++) {
for(blockx = 0; blockx < 16; blockx++)
if(a[blocky][blockx] > b[blocky + y][blockx])
tmp_diff += (frame[blocky][blockx] - frame[blocky + y][blockx]);
else
tmp_diff += (frame[blocky + y][blockx] - frame[blocky][blockx]);
}
7
8
9
10
11
12
13
14
15
16
17
18
19
// Check if the current block is least different
if(min_diff > tmp_diff) {
min_diff = tmp_diff;
*best_block_x = x;
*best_block_y = y;
}
20
21
22
23
24
25
}
}
return(min_diff);
26
27
28
29
}
1
engl. Motion estimation
66
7 Implementierung und Messungen
(a) Laufzeit und Graph Statistiken
Standard Opt.
2392
5324
13,55 s
# Knoten
# Kanten
Laufzeit
reichhaltige Befehle
680
1480
420 ms
Faktor
3,52
3,60
Speedup: 32,26
(b) Ausgenutzte reichhaltige Befehle
Muster
VectorLoad 16b v1
VectorLoad 16b v3
angewandt
1x
1x
SAD 16b
1x
Component 0Iu
1x
Befehle
lddqu
lddqu
psadbw
pshudf
paddd
movd
# Knoten
82
84
# Kanten
96
128
289
461
3
2
Tabelle 7.1: Messergebnisse Block-Matching“.
”
Die Ergebnisse der Optimierung zeigt Tabelle 7.1. Die Änderungen, die am Arbeitsgraphen vorgenommen wurden, sind in Tabelle 7.1(a) dargestellt, die gefundenen Passungen und die eingefügten reichhaltigen Befehle in Tabelle 7.1(b). Das ursprüngliche
Programm, optimiert mit Standardoptimierungen, hatte exklusive der zufälligen Initialisierung der Testdaten eine Laufzeit 13,55 Sekunden. Das von uns optimierte Programm
hatte eine Ausführungszeit von nur 450ms, was eine Beschleunigung um Faktor 32,26
bedeutet. Der gesamte Optimierungsprozess dauerte 1,2 Sekunden. Insgesamt wurden
vom Graphersetzungssystem 4480 Knoten und 17853 Kanten gepasst und 4 Passungen
ersetzt. Die eigentliche Graphersetzung inkl. Mustersuche schlug mit 40ms zu Buche,
der Rest der Zeit wurde für zusätzliche Analysen und das Entfernen von totem Code
verwendet.
7.2.2 Skalarprodukt
Ein weiteres Beispiel berechnet das Skalarprodukt von 2 500 000 vierdimensionalen Vektorpaaren und addiert die Ergebnisse aller Skalarprodukte auf. Der Quellcode ist in
Listing 7.2 zu sehen.
Listing 7.2: Skalarprodukt
1
2
3
4
5
6
7
static float scalar_product(float *a, float *b) {
float res;
for(i = 0; i < 10000000; i += 4)
res += a[i] * b[i] + a[i + 1] * b[i + 1] +
a[i + 2] * b[i + 2] + a[i + 3] * b[i + 3];
return(res);
}
7.2 Messergebnisse
67
(a) Laufzeit und Graph Statistiken
# Knoten
# Kanten
Laufzeit
Standard Opt.
604
1376
760 ms
reichhaltige Befehle
404
1008
720 ms
Faktor
1,30
1,37
Speedup: 1,05
(b) Ausgenutzte reichhaltige Befehle
Muster
VectorLoad 4f v1
VectorMul Speicher 4f v1
HorizontalAdd 4f
Component 0f
angewandt
1x
1x
1x
1x
Befehle
movaps
mulps
2x haddps
-
# Knoten
23
31
9
3
# Kanten
25
39
13
2
Tabelle 7.2: Messergebnisse Skalarprodukt“.
”
Die Ergebnisse der Optimierung sind in Tabelle 7.2 zu sehen. Insgesamt wurden 549
Knoten und 1836 Kanten gepasst. Von 28 Passungen wurden 4 ersetzt. Die von Firm GES
verbrauchte Zeit lag unter der Messgrenze, die Gesamtzeit betrug 1,5 Sekunden. Den vergleichsweisen geringen Speedup von 1,05 erklären wir dadurch, dass der sehr langsame
Befehl haddps eingesetzt werden musste, der für eine komplette horizontale Vektoraddition in jeder Iteration zwei Mal angewendet werden muss. Erwähnenswert an diesem
Beispiel ist, dass durch die algorithmische Steuerung (siehe Kapitel 6) korrekterweise
der reichhaltige Befehl VectorMul mit Speicheroperand – ein Multiplikationsbefehl, der
einen Vektor direkt aus dem Speicher laden kann – dem Multiplikationsbefehl, der nur
auf Registern arbeitet, vorgezogen wurde.
7.2.3 Reihungselemente begrenzen
Dieses Beispiel aus dem Bereich der Klangverarbeitung sucht eine Reihung aus 50 000 000
Elementen nach zu großen Werten ab und setzt sie ggf. auf einen konstanten Maximalwert herab (sog. Clipping“).
”
Listing 7.3: Clipping
1
2
3
4
5
6
7
8
9
10
void clipping(short *a, int max) {
short konstant[8] = {8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000};
int i, j;
for(i = 0; i < max; i += 8) {
for(j = 0; j < 8; j++) {
if(a[i + j] > konstant[j])
a[i + j] = konstant[j];
}
}
}
68
7 Implementierung und Messungen
(a) Laufzeit und Graph Statistiken
# Knoten
# Kanten
Laufzeit
Standard Opt.
1120
2468
510 ms
reichhaltige Befehle
508
1076
170 ms
Faktor
2,20
2,29
Speedup: 3
(b) Ausgenutzte reichhaltige Befehle
Muster
VectorLoad 8sI v0
VectorLoad 8sI v1
max 8sI
VectorStore 8sI v1
angewandt
1x
1x
1x
1x
Befehle
lddqu
lddqu
pmaxsw
movups
# Knoten
42
43
123
43
# Kanten
41
49
161
62
Tabelle 7.3: Messergebnisse Clipping“.
”
Bei diesem Beispiel musste der Optimierung durch einen entsprechenden Programmierstil etwas nachgeholfen werden: Da Vektorkomponenten nicht direkt mit einer Konstanten verglichen werden, muss die Konstante 8-fach in ein Vektorregister geladen werden. Die Ergebnisse der Optimierung sind in Tabelle 7.3 dargestellt. Insgesamt wurden
28 Passungen gefunden, von denen 4 ersetzt wurden. Alle Passungen umfassten 1331
Knoten und 4717 Kanten. Firm GES benötigte nur 16ms für das Finden aller Passungen. Die gesamte Optimierungszeit betrug 1 Sekunde. Das Programm wurde um Faktor 3
beschleunigt.
7.2.4 Demonstration der Musterauswahl
Unser letztes Beispiel berechnet den Ausdruck ~a ∗~b+~b∗ d~ und ist in Listing 7.4 dargestellt.
Listing 7.4: Zweifaches Skalarprodukt
1
2
3
float ueberlappung()
{
float a[4], b[4], d[4], sp1, sp2;
4
sp1 = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
sp2 = b[0] * d[0] + b[1] * d[1] + b[2] * d[2] + b[3] * d[3];
5
6
7
return(sp1 + sp2);
8
9
}
Dieses Beispiel dient der Demonstration der algorithmischen Steuerung, weshalb wir
nicht die Laufzeit messen, sondern den Suchbaum präsentieren. Interessant an diesem
Beispiel ist, dass zwei Varianten eines Vektormultiplikations-Musters zur Verfügung stehen: Eine Version, die ihre Operanden in Registern erwartet, und eine Version mit einem
7.2 Messergebnisse
69
Priorität
0
1
Muster
VectorLoad 4f
VectorLoad 4f
VectorMul Speicher 4f
VectorMul 4f
Ausdruck
Laden von ~b
Laden von d~
Laden von ~a
ai ∗ bi
ai ∗ di
Tabelle 7.4: Ersetzte Passungen beim Beispiel Zwei Skalarprodukte“.
”
Speicheroperanden. Allerdings soll durch den Speicheroperanden nicht der Vektor ~b geladen werden, da dieser vom zweiten Skalarprodukt auch gebraucht wird und deshalb
explizit in Registern zur Verfügung stehen muss. Die Steuerung beachtet dies, indem
dynamische Kosten aufaddiert werden, falls innere Knoten einer Passungen Verwendung
außerhalb finden.
Als Ergebnis wurden insgesamt 8 reichhaltige Befehle eingefügt. Wir zeigen im Speziellen die Befehle der Prioritätsklasse 0, da hier Überlappungen vorkommen. Die eingesetzten Befehle sind in Tabelle 7.4 dargestellt. Der Vektor ~b wurde korrekterweise explizit
in ein Register geladen. Der Multiplikationsbefehl mit Speicheroperand wurde zwar zwei
Mal erkannt, konnte jedoch nur einmal eingesetzt werden, da es ansonsten zu Verklemmungen der Speicherkante gekommen wäre (siehe Abschnitt 4.3.3). Der Vollständigkeit
halber ist in Abbildung 7.3 der erzeugte Suchbaum für die Prioritätsklasse 0 zu sehen.
Die markierten Knoten repräsentieren ersetzte Passungen, die nicht markierten Knoten
repräsentieren gefundene, jedoch aus kostengründen nicht ersetzte Passungen.
Abbildung 7.3: Suchbaum für Prioritätsklasse 0
70
7 Implementierung und Messungen
7.3 Bewertung
Die Messungen demonstrieren, dass großes Potential besteht, Programme mit Hilfe von
vorgenerierten Graphersetzungsregeln zu optimieren. Während den Tests hat sich gezeigt, wie einfach reichhaltige Befehle durch die Befehls-Spezifikation in einer Hochsprache zu integrieren sind. Vor allem Programme aus dem Multimediabereich wie
Video-Codecs oder numerische Berechnungen, die Skalar- und Matrixmultiplikationen
durchführen, kommen für die Optimierung in Frage. Es ist vor allem dann eine hohe Beschleunigung zu erreichen, wenn die ersetzten reichhaltigen Befehle verzweigten
Steuerfluss beinhalten. Da sich auch die von der Optimierung in Anspruch genommene Übersetzungszeit in Grenzen hält, lohnt es sich unserer Meinung nach, eine solche
Optimierung in Standard-Übersetzer einzubauen.
Wie sich im Verlauf der Messungen gezeigt hat, ist noch nicht die Optimierung beliebiger Programme möglich, weshalb im Folgenden kurz einige weiterführende Ideen
präsentiert werden sollen.
7.4 Weiterführende Ideen
Ergebnisse von reichhaltigen Befehlen lassen sich bisher noch nicht über verschiedene
Schleifeniterationen hinweg transportieren. Durch die SSA-typischen Phi -Knoten, die im
Mustergraph so nicht vorkommen, können wir aktuell noch keine Passungen finden, die
Schleifengrenzen überstreichen. Eine Möglichkeit wäre, auf Unterstützung von Seiten des
Graphersetzungssystems zu hoffen. Sogenannte Pfadausdrücke würden es uns erlauben,
über Phi -Operationen hinwegzuschauen“.
”
Wird ein ein reichhaltiger Befehl gefunden, der Ergebnisse aus einer früheren Schleifeniteration verwendet, wären weitere Transformationen notwendig. Die entsprechenden
Phi -Knoten müssten möglicherweise zu Vektor-Phi -Knoten konvertiert werden. Dabei
ist zu prüfen, ob alle Operanden auch vektorwertig sind. Gegebenenfalls müssen Operationen zur Konvertierung der Operanden oder des Ergebnisses eingefügt werden. Desweiteren ist das Finden von Folgepassungen momentan oft abhängig davon, dass bereits
Vektorlade-Befehle, die Vektorregister befüllen, eingefügt worden sind. Um das zu umgehen, wäre es möglicherweise sinnvoll, unabhängig von den benutzerspezifizierten Befehlen
Knoten einzuführen, die automatisch Werte aus verschiedenen Einzelregistern in Vektorregistern packen“ können. Es wäre auch denkbar, Komponenten eines Vektorregisters
”
zu permutieren, um den Registerinhalt für den nächsten Vektorbefehl aufzubereiten.
In Abbildung 7.4 ist dargestellt, wie das Einfügen eines reichhaltigen Befehls, der Ergebnisse aus einer früheren Schleifeniteration verwendet, vonstatten gehen könnte. Abbildung 7.4(a) zeigt die Operationen und die Datenabhängigkeiten einer einfachen Schleife,
die zwei mit 0 initialisierte Werte pro Iteration um 4 erhöht. Die gestrichelten Kanten
deuten Verwender außerhalb der Schleife an, die das Endergebnis weiterverwenden. Nun
wäre es möglich, die beiden Werte als Vektor aufzufassen und statt einzelner Additions-
7.4 Weiterführende Ideen
71
(a)
0x0
Phi
Phi
0x4
Add
(c)
(b)
0x0
0x4
0x0
Pack
Pack
Pack
VPhi
Add
VProj 0
Pack
VProj 1
0x4
Add
VPhi
Pack
VAdd
VProj 0
VProj 1
Add
0x4
0x0
Pack
Pack
VPhi
VAdd
(d)
Abbildung 7.4: Vektorwerte über Schleifeniterationen transportieren
befehle einen Vektoradditions-Befehl einzusetzen. Es ist aber nicht sofort offensichtlich,
wie ein solcher Befehl eingefügt werden kann. Dazu sind weitere Transformationen nötig,
die wir im folgenden schrittweise beschreiben.
Wir beginnen bei der Initialisierung des Vektors vor Beginn der Schleife. Die in
Abbildung 7.4(a) grau hinterlegten Knoten, die beide Werte einzeln mit 0 initialisieren, ersetzen wir in Abbilding 7.4(b) durch eine spezielle Pack -Operation. Diese
befüllt die beiden Komponenten eines Vektorregisters gleichzeitig mit 0. Die beiden Phi Operationen, die die beiden Einzelwerte dargestellt haben, sind durch ein einziges VPhi
(Vektor Phi) ersetzt worden. Wir müssen nun dafür sorgen, dass alle anderen Operanden des VPhi -Knotens ebenfalls Vektorwerte sind. Deswegen ist eine zusätzliche Pack Operation vonnöten, die beide Ergebnisse der einzelnen Additionen zu einem Vektor
zusammenfasst. Da die Verwender des Ergebnisses des Phi -Knotens noch Einzelbefehle
72
7 Implementierung und Messungen
sind, müssen noch VProj -Knoten zwischengeschaltet werden, um einzelne Vektorkomponenten zu extrahieren.
Erst jetzt können wir die Passung des Vektoradditions-Befehls erkennen, die in 7.4(b)
grau hinterlegt ist. In Abbildung 7.4(c) wurde sie durch einen VAdd-Knoten ersetzt. Die
VProj -Knoten des VPhi können wegfallen, da wir annehmen, dass auch der Verwender
außerhalb der Schleife das im Vektorregister gespeicherte Endergebnis direkt weiterverarbeiten kann, ohne Komponenten extrahieren zu müssen. Es verbleiben jetzt nur noch
die grau markierten VProj -Knoten des VAdd, die wir eliminieren müssen. Da die Pack Operation gerade als inverser VProj -Knoten angesehen werden kann, heben sich die grau
markierten Knoten gerade auf. Eventuell sind Permutationen von Vektorkomponenten
nötig.
Das Ergebnis der Transformation ist in Abbildung 7.4(d) zu sehen. Die Schleife ist
nun komplett vektorisiert. Aus Zeitgründen konnten diese Überlegungen nicht mehr in
die Implementierung mit aufgenommen werden.
8 Zusammenfassung und Ausblick
Ziel dieser Arbeit war es, Graphersetzungsregeln aus einer Befehls-Spezifikation, welche
auf einer Hochsprache basiert, zu generieren und deren Anwendbarkeit in Bezug auf eine
übersetzerinterne Optimierung zu prüfen.
Dazu haben wir einen bestehender Übersetzer um eine Optimierung erweitert, die automatisch die von modernen Prozessoren angebotenen reichhaltigen Befehle unterstützt.
Die Optimierung findet auf der Zwischensprache Firm statt. Dabei werden Teilgraphen
der Zwischendarstellung – bestehend aus mehreren Einzeloperationen – durch reichhaltige Befehle ersetzt. Zur Graphtransformation setzen wir das Graphersetzungssystem
GrGen ein. Die Graphersetzungsregeln für GrGen werden ausgehend von einer Spezifikation in einer Hochsprache automatisch generiert.
Die von uns entworfene Spezifikationssprache bedient sich nur der Sprachelemente der
Programmiersprache C und ist deshalb einfach zu erlernen, intuitiv und leicht erweiterbar, wie sich während unserer Tests gezeigt hat. Nach einem einleitenden Definitionsteil
der Spezifikation, der alle für die Optimierung und für das Backend wichtigen Informationen enthält, können im Verhaltensteil selbst komplexe reichhaltige Befehle mit relativ
geringem Aufwand beschrieben werden. Wir haben dargelegt, wie aus der Spezifikation
die Graphersetzungsregeln entstehen und haben untersucht, wie die Regeln angewendet
werden können.
Um die Trefferwahrscheinlichkeit bei der Mustersuche zu erhöhen, haben wir Maßnahmen wie Normalisierung und Variantenbildung ergriffen, um die syntaktischen Unterschiede von Muster- und Arbeitsgraph gering zu halten. Wir haben uns ausführlich
mit der Alias-Problematik auseinandergesetzt und Lösungen für die Mustersuche bei
unterschiedlich serialisierten Speicheroperationen angegeben. Gerade bei der Speicherabhängigkeit von Operationen gibt es viele Feinheiten zu beachten, auf die wir deshalb
genauer eingegangen sind.
Da Graphersetzungsregeln voneinander abhängen, bedarf es einer algorithmischen
Steuerung der Regelanwendung. Dazu haben wir zwei Verfahren entwickelt. Das suchbaumbasierte, explorative Verfahren floss in die Implementierung ein und hat für alle
Testprogramme in ausreichend kurzer Zeit funktioniert. Für sehr große Arbeitsgraphen
und eine große Menge an gefundenen Passungen ist jedoch das Problem der Regelauswahl
durch das zweite vorgestellte, PBQP basierte Verfahren effizienter lösbar. Wir haben an
einem Beispiel gezeigt, wie das PBQP-basierte Verfahren angewendet werden kann.
Es war uns möglich, hardware-spezifische Attribute in die Spezifikationssprache mit
aufzunehmen. Die Integration in das Backend ist also für den Benutzer transparent. Es
73
74
8 Zusammenfassung und Ausblick
ist somit möglich, unsere Implementierung vollkommen automatisch ohne Eingreifen des
Benutzers zur Codeoptimierung einzusetzen. Es konnten reale Programme optimiert und
hohe Beschleunigung erzielt werden. Allerdings musste bei einigen Beispielen der Programmierstil trotzdem angepasst werden. Einschränkungen und Verbesserungsmöglichkeiten der Implementierung sehen wir deshalb in folgenden Punkten:
• Prüfen, ob Daten im Speicher korrekt ausgerichtet sind. Die Spezifikationssprache kann dazu mit geringem Aufwand erweitert werden. Allerdings benötigen wir
hierbei auch Unterstützung des Übersetzers und entsprechende Analysen (siehe
Abschnitt 4.3.5).
• Parallelisierung von Speicherkanten. Eine Optimierung, die gemäß den Informationen einer Alias-Analyse Speicheroperationen parallelisiert, würde uns Aufwand
bei der Analyse von Passungen ersparen und Vorteile bei Passungen, die sich über
mehrere Grundblöcke erstrecken, bringen. Um der Alias-Analyse unter die Arme
zu greifen, wäre z.B. die Unterstützung des C99 Schlüsselworts restrict durch
den Übersetzer hilfreich.
• Transportieren von Ergebnissen reichhaltiger Befehle über Schleifengrenzen. Außerdem das Befüllen von Vektorregistern nicht nur durch Vektorlade-Operationen,
sondern auch durch das automatische Kopieren von Werten aus Allgemeinregistern. Dazu sind generischere Muster notwendig, so dass eine größere Anzahl von
Passungen gefunden werden kann (siehe Abschnitt 7.4).
• Eine große Anzahl von Passungen führt zwangsweise zum PBQP-basierten Ansatz der Regelauswahl, da dieses Verfahren im Vergleich zum explorativen Ansatz
deutlich besser skaliert (siehe Abschnitt 6.3).
Literaturverzeichnis
[1] CONVEX Computer Corp , O. Box 5937, Denver, CO 80217-9808. CONVEX Application Compiler User’s Guide, 2nd ed. , 1992.
[2] Alfred V. Aho, Ravi Sethi, and Jeffrey D. Ullman. Compilers: Principles, Techniques, and Tools. Addison-Wesley Pub Co, 1986.
[3] Gernot Veit Batz. Graphersetzung für eine Zwischendarstellung im Übersetzerbau.
Master’s thesis, Universität Karlsruhe, 2005.
[4] Jakob Blomer and Rubino Geiß. The GrGen.NET User Manual. Technical Report
2007-5, Universität Karlsruhe, IPD Goos, July 2007. ISSN 1432-7864.
[5] Boris Boesler. Codeerzeugung aus Abhängigkeitsgraphen. Master’s thesis, Universität Karlsruhe (TH), IPD, Jun 1998.
[6] Keith D. Cooper and Linda Torczon. Engineering a Compiler. Morgran Kaufmann
Publishers, 2004.
[7] Intel Corp. Data Alignment and Programming Issues for the Streaming SIMD
Extensions with the Intel C/C++ Compiler. Intel Application Notes, AP-833, 1999.
[8] Ron Cytron, Jeanne Ferrante, Barry K. Rosen, Mark N. Wegman, and F. Kenneth Zadeck. Efficiently computing static single assignment form and the control
dependence graph. ACM Trans. Program. Lang. Syst., 13(4):451–490, 1991.
[9] H. Ehrig, R. Heckel, M. Korff, M. Löwe, L. Ribeiro, A. Wagner, and A. Corradini.
Algebraic Approaches to Graph Transformation - Part II: Single Pushout A. and
Comparison with Double Pushout A. In [26], volume 1, pages 247–312. 1999.
[10] Bernhard Scholz Erik Eckstein, Oliver König. Code instruction selection based on
SSA-graphs. Lecture notes in computer science (Lect. notes comput. sci.) ISSN
0302-9743, 2003.
[11] M. Flynn. Some computer organizations and their effectiveness. IEEE Trans.
Comput., Vol. C-21, pp. 948, 1972.
[12] R. Geiß. Graphersetzung mit Anwendungen im Übersetzerbau. PhD thesis, Universität Karlsruhe, IPD Prof. Goos, 2007.
75
76
Literaturverzeichnis
[13] Rubino Geiß, Gernot Veit Batz, Daniel Grund, Sebastian Hack, and Adam M.
Szalkowski. Grgen: A fast spo-based graph rewriting tool. In A. Corradini, H. Ehrig,
U. Montanari, L. Ribeiro, and G. Rozenberg, editors, Graph Transformations ICGT 2006, Lecture Notes in Computer Science, pages 383 – 397. Springer, 2006.
Natal, Brasil.
[14] Rakesh Ghiya, Daniel Lavery, and David Sehr. On the importance of points-to
analysis and other memory disambiguation methods for C programs. Proceedings
of the ACM SIGPLAN 2001 conference on Programming language design and implementation, pages 47–58, 2001.
[15] Enno Hofmann. Regelerzeugung zur maschinenabhängigen Codeoptimierung. Master’s thesis, Universität Karlsruhe, November 2004.
[16] Intel Corporation. Intel application notes. http://developer.intel.com.
[17] Intel Corporation. Block-Matching In Motion Estimation Algorithms Using Streaming SIMD Extensions 3. Intel Application Notes, december 2003.
[18] Intel Corporation, O. Box 5937, Denver, CO 80217-9808. IA-32 Intel Architecture
Software Developer’s Manual – Instruction Set Reference, 2005.
[19] Hannes Jakschitsch. Befehlsauswahl auf SSA-Graphen. Master’s thesis, Universität
Karlsruhe, November 2004.
[20] Ken Kennedy and Randy Allen. Optimizing Compilers for Modern Architectures:
A Dependence-based Approach. Morgan Kaufmann, 2001.
[21] Götz Lindenmaier. libfirm – a library for compiler optimization research implementing firm. Technical Report 2002-5, Universität Karlsruhe, Fakultät für Informatik,
Sep 2002.
[22] Christoph Mallon. If-Konversion auf SSA, 3 2007.
[23] Robert Metzger and Zhaofang Wen. Automatic algorithm recognition and replacement: a new approach to program optimization. MIT Press, Cambridge, MA, USA,
2000.
[24] Ivan Pryanishnikov, Andreas Krall, and Nigel Horspool. Pointer Alignment Analysis
for Processors with SIMD Instructions. 5th Workshop on Media and Streaming
Processors, 2003.
[25] Ivan Pryanishnikov, Andreas Krall, and Nigel Horspool. Compiler optimizations for
processors with SIMD instructions. Softw. Pract. Exper., 37(1):93–113, 2007.
[26] G. Rozenberg, editor. Handbook of Graph Grammars and Computing by Graph
Transformation. World Scientific, 1999.
Literaturverzeichnis
77
[27] Martin Trapp, Götz Lindenmaier, and Boris Boesler. Documentation of the intermediate representation FIRM. Technical Report 1999-14, Universität Karlsruhe,
Fakultät für Informatik, Dec 1999.
[28] Christian Würdig.
Entwurf und Implementierung eines SSA-basierten x86Backends, September 2006.
78
Literaturverzeichnis
Danksagung
Mein Dank gilt allen Mitgliedern des Instituts für Programmstrukturen und Datenorganisation, Prof. G. Goos, die mir während der Anfertigung dieser Diplomarbeit mit
Rat und Tat beiseite gestanden sind.
Mein besonderer Dank gilt meinem Betreuer Rubino Geiß, Michael Beck und meinen
Kommilitonen Christoph Mallon, Veit Batz, Moritz Kroll, Edgar Jakumeit, Matthias
Braun und Christian Würdig für die stets angenehme Atmosphäre, viele fruchtbare Gespräche, tatkräftige Hilfe und wertvolle Kritik. Außerdem bedanke ich mich bei meinen
Eltern, die mich während meines Studiums immer voll unterstützt haben. Nicht zuletzt gilt mein Dank Marijana dafür, dass sie da ist, für ihre Liebe und ihre mentale
Unterstützung.
79
Herunterladen