Codegenerierung für digitale Signalprozessoren: Erweiterung eines

Werbung
Universität Karlsruhe (TH)
Codegenerierung für digitale
Signalprozessoren:
Erweiterung eines
Codegeneratorgenerators
basierend auf
Graphersetzungsmethoden
Diplomarbeit
Institut für Programmstrukturen und Datenorganisation
Prof. Dr. Gerhard Goos
Fakultät für Informatik
Universität Karlsruhe (TH)
von
cand. inform.
Hannah Schröter
Betreuer:
Dr. Sabine Glesner
Boris Boesler
Verantwortlicher:
Prof. Dr. Gerhard Goos
Februar 2003
Ich versichere, die vorliegende Diplomarbeit selbständig angefertigt zu haben. Alle
dabei verwendeten Hilfsmittel, Werkzeuge und Quellen wurden vollständig aufgeführt.
Karlsruhe, den 28. Februar 2003
(Hannah Schröter)
Inhaltsverzeichnis
1 Einleitung
1
1.1 Problemstellung, Anforderungen . . . . . . . . . . . . . . . . . . . . .
1
1.2
Lösungsansatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
1.3
Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
1.4
Gliederung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
2 Verwandte Arbeiten
2.1 Ansätze zur Phasenkopplung . . . . . . . . . . . . . . . . . . . . . . .
2.2
2.3
5
5
2.1.1
Integer Linear Programming . . . . . . . . . . . . . . . . . . .
5
2.1.2
Constraint Logic Programming . . . . . . . . . . . . . . . . .
7
2.1.3
Genetische Algorithmen . . . . . . . . . . . . . . . . . . . . .
8
2.1.4
Dynamisches Programmieren . . . . . . . . . . . . . . . . . .
9
2.1.5
Heuristische Suche . . . . . . . . . . . . . . . . . . . . . . . .
9
2.1.6
Mutation Scheduling . . . . . . . . . . . . . . . . . . . . . . . 10
2.1.7
Software-Pipelining . . . . . . . . . . . . . . . . . . . . . . . . 11
2.1.8
Prozessorregister als Funktionseinheiten . . . . . . . . . . . . 12
2.1.9
Der TM-1000-Compiler . . . . . . . . . . . . . . . . . . . . . . 12
Auswahl von DSP-Befehlen . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.1
Vektorisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.2
Musterersetzung auf Quelltextebene . . . . . . . . . . . . . . . 14
2.2.3
Integer Linear Programming . . . . . . . . . . . . . . . . . . . 14
Zusammenfassung und Bewertung . . . . . . . . . . . . . . . . . . . . 15
ii
Inhaltsverzeichnis
3 Grundlagen
19
3.1 Die SSA-Darstellung Firm . . . . . . . . . . . . . . . . . . . . . . . . 19
3.2
3.3
BURS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.2.1
Kostenbehaftete Termersetzungssysteme . . . . . . . . . . . . 21
3.2.2
Übertragung auf BURS . . . . . . . . . . . . . . . . . . . . . . 23
Heuristische Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.3.1
Suchproblem . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.3.2
Grundalgorithmus
3.3.3
Heuristische Suchverfahren . . . . . . . . . . . . . . . . . . . . 26
. . . . . . . . . . . . . . . . . . . . . . . . 24
3.4
Befehlsanordnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.5
Erkennung von SIMD-Befehlen . . . . . . . . . . . . . . . . . . . . . 28
4 Effiziente Phasenkopplung mit zustandsbehafteter Termersetzung 29
4.1 Zustandsbehaftete Termersetzung . . . . . . . . . . . . . . . . . . . . 29
4.2
Maschinensimulation . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.2.1
Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.2.2
Phasengekoppelte Codeerzeugung . . . . . . . . . . . . . . . . 31
4.2.3
Identifikation von SIMD-Befehlen . . . . . . . . . . . . . . . . 32
4.3 Äquivalenz von Suchknoten . . . . . . . . . . . . . . . . . . . . . . . 33
4.4
Suboptimales Suchverfahren . . . . . . . . . . . . . . . . . . . . . . . 34
5 Implementierung
37
5.1 Anbindung der Maschinensimulation . . . . . . . . . . . . . . . . . . 37
5.1.1
Schnittstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
5.1.2
Regeln mit Befehlsmustern . . . . . . . . . . . . . . . . . . . . 39
5.1.3
Anbindung an die Suche . . . . . . . . . . . . . . . . . . . . . 40
5.2 Äquivalenzklassen von Suchknoten . . . . . . . . . . . . . . . . . . . 40
5.3
Parametrisierbare Suche . . . . . . . . . . . . . . . . . . . . . . . . . 40
5.4
Sonstige Laufzeitverbesserungen . . . . . . . . . . . . . . . . . . . . . 43
5.4.1
Verwaltung der Menge offener Knoten . . . . . . . . . . . . . 43
5.4.2
Iteration über Nachfolger eines Suchknotens . . . . . . . . . . 44
5.4.3
Menge der noch zu bearbeitenden Programmgraphknoten . . . 44
5.5 Übersicht über den Codegenerator . . . . . . . . . . . . . . . . . . . . 44
Inhaltsverzeichnis
6 Ergebnisse
iii
47
6.1 Testumgebung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
6.1.1
Idealisierte Prozessorarchitektur . . . . . . . . . . . . . . . . . 47
6.1.2
Codegeneratorspezifikation . . . . . . . . . . . . . . . . . . . . 48
6.1.3
Maschinensimulation . . . . . . . . . . . . . . . . . . . . . . . 48
6.1.4
Testgraphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6.1.5
Varianten des Codegenerators . . . . . . . . . . . . . . . . . . 49
6.2
Messergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.3
Auswertung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.3.1
Auswirkung der Phasenkopplung . . . . . . . . . . . . . . . . 52
6.3.2
Äquivalenzen von Suchknoten . . . . . . . . . . . . . . . . . . 53
6.3.3
One-Then-Best-Suche . . . . . . . . . . . . . . . . . . . . . . . 53
6.3.4
Laufzeitabhängigkeit von den Eingabegraphen . . . . . . . . . 54
7 Zusammenfassung und Ausblick
57
7.1 Ergebnisse dieser Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . 57
7.2
Offene Probleme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
Literaturverzeichnis
61
Index
65
1. Einleitung
1.1
Problemstellung, Anforderungen
Es werden auch heute noch signifikante Teile der Software für eingebettete Systeme und digitale Signalprozessoren (Digital Signal Processor – DSP) manuell in der
Assemblersprache der jeweiligen Architektur geschrieben. Dies liegt daran, dass der
Qualitätsunterschied zwischen handgeschriebenen Assemblerprogrammen und von
Übersetzern erzeugtem Code bisher oft zu groß ist. Weil diese manuelle Anpassung
jedoch viel Arbeitszeit in Anspruch nimmt und fehleranfällig ist, ist es nötig, künftig
auch mit Übersetzern eine bessere Codequalität zu erzielen.
Wir konzentrieren uns in dieser Arbeit auf die folgenden Probleme: Zum einen bearbeiten wir das Problem der Phasenkopplung zwischen Befehlsauswahl und Befehlsanordnung. Zum anderen wollen wir im Rahmen dieser Phasenkopplung auch
automatisch Single-Instruction-Multiple-Data-Befehle (SIMD) auswählen, wo dies
möglich und effizient ist.
Die Codeerzeugung soll direkt aus graphbasierten Darstellungen mit statischer Einmalzuweisung (Static Single Assignment – SSA) erfolgen, damit wir uns hier möglichst viele Spielräume erhalten.
Da für eingebettete Systeme oft die Anwendungssoftware und die Hardware gemeinsam entworfen werden, ist es besonders interessant, wenn die Übersetzer leicht
an Änderungen der Prozessoren angepasst werden können. Dadurch ist es nämlich
schneller möglich, die Auswirkungen dieser Änderungen auf die Effizienz der zu entwickelnden Anwendungen zu erproben. Daher wollen wir erreichen, dass man ein
Backend für einen neuen Prozessor mit unserem Verfahren erstellen kann, indem
man möglichst nur Parameter und/oder Prozessorspezifikationen anpassen muss.
Auch wenn im Bereich der Codeerzeugung für eingebettete Systeme etwas höhere
Übersetzerlaufzeiten akzeptiert werden können, müssen wir dennoch teilweise einen
Kompromiss zwischen der Qualität des erzeugten Codes und der Laufzeit des Codegenerators eingehen. Wir wollen unser Verfahren so entwickeln, dass dieser Kompromiss durch Parameter gesteuert werden kann, die beim Start der Codeerzeugung
2
1. Einleitung
gesetzt werden. Das Problem, optimalen Code aus einer graphbasierten Zwischensprache zu erzeugen, ist nämlich im Allgemeinen NP-vollständig (zur Befehlsauswahl aus DAGs, siehe [GaJo90]; zur Befehlsanordnung, siehe [HeGr83], zitiert nach
[ErKr91]; zur Registerallokation, siehe [FaLi97]; siehe auch [Seth75]).
1.2
Lösungsansatz
Unsere Methode basiert auf einer Kombination von Bottom Up Rewrite Systems
(BURS) und heuristischer Suche [NyKa97]. An unserem Institut wurde der Codegenerator-Generator cggg erstellt, in dem diese Kombination in der Praxis umgesetzt,
in der Laufzeit verbessert und zur Anwendung auf SSA-Graphen erweitert wurde
[Boes98]. In den genannten Arbeiten werden den Befehlen feste Kosten zugeordnet. Damit decken sie nur die Befehlsauswahl aus Bäumen [NyKa97] bzw. Graphen
[Boes98] ab. Boesler [Boes98] deutet jedoch schon die Möglichkeit an, die heuristische Suche auch dynamischer zu steuern, um eine Maschinensimulation zu integrieren, und schlägt ein Verfahren vor, dies umzusetzen. Wir greifen diese Idee in dieser
Arbeit auf, arbeiten sie genauer aus und setzen sie praktisch um.
Auf diese Weise erreichen wir eine Codeerzeugung aus SSA-Graphen, die die Phasen
der Befehlsauswahl und Befehlsanordnung koppelt. Dies hilft uns, die Parallelität
auf Befehlsebene (Englisch: Instruction Level Parallelism – ILP) auszunutzen, die
von Prozessoren mit Pipeline, superskalaren Prozessoren, Architekturen mit langen
Befehlswörtern (Very Large Instruction Word – VLIW) und digitalen Signalprozessoren angeboten werden. Es können desweiteren Teilbefehle von SIMD-Befehlen ausgewählt werden, wenn sie in einer einfachen Nachbearbeitung zu den SIMD-Befehlen
selbst kombiniert werden können.
Die Retargierbarkeit erreichen wir, indem wir den Codegenerator weiterhin wie in
[Boes98] aus wiederverwendbaren sowie von dem Codegenerator-Generator erzeugten Modulen konstruieren.
Den teilweise nötigen Kompromiss zwischen Codequalität und Generatorlaufzeiten
stellen wir her, indem wir ein alternatives Suchverfahren entwickeln. Wir bieten nun
eine Wahlmöglichkeit am Anfang der Codegenerierung an, ob die bisher verwendete
A∗ -Suche oder unser alternatives Suchverfahren verwendet werden soll. Bei unserem
neuen Suchverfahren, das schneller, aber nicht unbedingt optimal ist, kann man die
Codequalität bzw. Codegeneratorlaufzeit durch zusätzliche Parameter steuern.
1.3
Motivation
In traditionellen Übersetzern erfolgt die Codeerzeugung meist so, dass aus einer Zwischendarstellung in getrennten Phasen die Befehlsauswahl, Befehlsanordnung und
Registerzuteilung mit eventuellem Zwischenspeichern von Werten stattfindet. Zwar
gibt es auch in diesen traditionellen Codegeneratoren teilweise unterschiedliche Anordnungen dieser Phasen und teilweise werden Phasen wiederholt durchgeführt. Dennoch wird dies gerade bei der Codeerzeugung für eingebettete Systeme den dort sehr
hohen Anforderungen nicht gerecht. Die unterschiedlichen Phasen verfolgen nämlich
oft einander entgegengesetzte Ziele.
1.4. Gliederung
3
Zum Beispiel kann der Übersetzer bei getrennter Befehlsauswahl und -anordnung
nicht unterscheiden, ob je nach Belegung von Funktionseinheiten oder Prozessorregistern für den Ausdruck 2 · x entweder ein Multiplikationsbefehl, eine Addition von
x mit sich selbst oder ein Schiebebefehl am günstigsten ist.
Desweiteren bieten moderne Architekturen oft SIMD-Befehle an. Beispiele sind
sowohl digitale Signalprozessoren wie der TriMedia TM-1300 als auch Media-Befehlssätze in Allzweckprozessoren, zum Beispiel Intels MMX und SSE, Suns VISBefehlssatz oder die AltiVec-Befehle in der PowerPC-Architektur. Diese werden von
gängigen Übersetzern nicht automatisch ausgenutzt, sondern höchstens als Erweiterungen der übersetzten Programmiersprache(n) zur manuellen Verwendung angeboten. Wenn ein Softwareentwickler diese Erweiterungen jedoch verwendet, ergibt
sich daraus der Nachteil, dass eine Anpassung an andere Architekturen zusätzlichen
Aufwand bedeutet.
Ein anderes Hindernis für die Erzeugung von effizientem Maschinencode ist, dass
einerseits in der Forschung bevorzugt SSA-Darstellungen verwendet werden, andererseits jedoch der Code üblicherweise erzeugt wird, indem zuerst eine Transformation in eine traditionellere Baum- oder DAG-Darstellung erfolgt. Durch solche
Zwischenschritte verliert man jedoch einen Teil der Spielräume für optimierende
Codegenerierung.
Daher haben wir uns entschieden, die hier formulierte Problemstellung zu bearbeiten,
nämlich die Phasen der Befehlsauswahl und -anordnung zu koppeln, hiermit auch
SIMD-Befehle automatisch auszuwählen und diese Codeerzeugung direkt aus SSAGraphen durchzuführen.
1.4
Gliederung
In Kapitel 2 stellen wir verwandte Arbeiten zur Phasenkopplung und Codeerzeugung
für digitale Signalprozessoren vor. Dann erläutern wir die hier verwendeten Grundlagen, vgl. Kapitel 3. In Kapitel 4 entwickeln wir eine Theorie zustandsbehafteter
Termersetzung, die wir einsetzen, um eine Maschinensimulation in die Codegenerierung einzubinden. Dort erarbeiten wir auch eine Methode, um den Suchraum der
A∗ -Suche, wie sie bereits von Boesler [Boes98] verwendet wird, zu verkleinern, und
einen alternativen Suchalgorithmus, der es erlaubt, schnell guten, wenn auch nicht
garantiert optimalen, Code zu erzeugen. Die Implementierung dieser Verfahren beschreiben wir in Kapitel 5. Danach zeigen wir anhand von Experimenten, wie das hier
entwickelte Verfahren die gesetzten Ziele der verbesserten Codequalität bei gleichzeitig akzeptablen Laufzeiten des Übersetzers erreicht, vgl. Kapitel 6. Abschließend
fassen wir die Arbeit zusammen und skizzieren mögliche Weiterentwicklungen.
4
1. Einleitung
2. Verwandte Arbeiten
Zur Codegenerierung für DSPs gibt es bereits viele Ansätze. Zum einen existieren Methoden zur engeren Kopplung der traditionell meist getrennt behandelten
Phasen der Codeerzeugung, vorgestellt in Abschnitt 2.1. Zum anderen gibt es auch
Arbeiten, die zum Ziel haben, automatisch DSP-spezifische Befehle zu erzeugen, vgl.
Abschnitt 2.2.
Abschließend fassen wir die verwandten Arbeiten zusammen und bewerten sie anhand unserer Anforderungen.
2.1
2.1.1
Ansätze zur Phasenkopplung
Integer Linear Programming
In [WGHB95] ([MaGo95], Kapitel 6) wird ein Ansatz vorgestellt, der auf der Basis von ganzzahliger linearer Programmierung (Integer Linear Programming – ILP)
eine simultane Lösung für die Befehlsauswahl, -anordnung und Registerzuteilung
einschließlich der Entscheidung über eventuelles Zwischenspeichern (Spilling) erreicht. Ein Frontend übersetzt eine Programmiersprache namens C+− (C ohne
Zeigerarithmetik, dafür mit zusätzlichen Ausdrucksmitteln, um z.B. Muster der
Verwendung von Reihungen darzustellen) in eine Zwischendarstellung (Symboltabellen, Daten- und Kontrollflussgraphen). Danach folgen Optimierungen auf dieser
Zwischendarstellung, die entweder manche Entscheidungen komplett vorwegnehmen
oder Entscheidungsmöglichkeiten lediglich markieren, damit sie später in die ILPFormulierung der phasengekoppelten Codegenerierung einfließen können. Anschließend wird ein ganzzahliges lineares Programm aufgestellt, das folgende Teilaufgaben
simultan löst:
• Kombination von generischen Operationen im Datenflussgraphen zu komplexeren Maschinenbefehlen, z.B. + und ∗ zu einem Multipliziere-und-AddiereBefehl,
• Anordnung der Befehle,
6
2. Verwandte Arbeiten
• globale Registerzuteilung,
• Zwischenspeichern von Werten (Spilling) und
• Kompression (Kombination von Teilbefehlen zu endgültigen Maschinenbefehlen).
Laut [Käst01], Seite 3, soll dieses Verfahren jedoch aufgrund der Komplexität der
ILP-Formulierung zu exzessiven Laufzeiten des Codegenerators führen.
Das System PROPAN [Käst01, Käst00, Käst97] arbeitet als Nachbearbeitung auf
bereits generiertem Assembler-Code. Es werden verschiedene Graphen hergestellt
([Käst00], Seite 47 ff.), wie der Ressourcengraph (Zuordnung aller Befehle zu den
Prozessorressourcen, die alternativ zur Ausführung verwendet werden können) und
der Registergraph (Zuordnung der Anweisungen zu möglichen Registern, in die das
Ergebnis der jeweiligen Instruktion geschrieben werden kann). Daraus wird eine der
beiden folgenden ILP-Formulierungen erstellt und dann gelöst.
In der SILP-Form ( Scheduling and allocation with Integer Linear Programming“)
”
ist der Hauptteil der Entscheidungsvariablen eine Angabe von Reihenfolgen (xkij = 1
⇔ Befehl j wird direkt nach Befehl i auf einer Funktionseinheit des Typs k ausgeführt). Es wird für jede Art von Ressource ein Ressourcenflussgraph erstellt, der den
Fluss“ jeder Ressource durch alle Befehle, die diese Ressource verwenden, model”
liert. Daraus werden Beschränkungen für das resultierende ILP abgeleitet. Insgesamt
gelten als Beschränkungen:
• Die Gesamtlaufzeit ist das Maximum der Zeitpunkte, zu denen die einzelnen
Befehle abgesetzt werden. Die Variable Gesamtlaufzeit“ kann so als zu mini”
mierende Zielfunktion verwendet werden.
• Datenabhängigkeiten müssen erfüllt sein.
• Im Ressourcenflussgraphen muss jede Ressource die Befehle auch verlassen, die
sie betritt.
• Jeder Befehl muss von genau einer Ressource ausgeführt werden.
• Die Anzahl der vorhandenen Ressourcen darf nicht überschritten werden.
• Jede Instanz einer Ressource kann gleichzeitig nur einen Befehl ausführen.
Durch die Modellierung von Registerflussgraphen (Fluss der Register durch die Befehle) kann die Registerzuteilung integriert werden. Diese Formulierung hat O(n2 )
Entscheidungsvariablen und Beschränkungen (siehe [Käst00], Seiten 61 f.), wobei n
die Anzahl der Befehle ist.
Im OASIC-Modell ( Optimal Architectural Synthesis with Interface Constraints“)
”
gibt es hauptsächlich Entscheidungsvariablen xkjn , die besagen, dass Befehl j zum
Zeitpunkt n von einer Funktionseinheit des Typs k ausgeführt wird (zeitindizierte
Form). Beschränkungen werden für folgende Sachverhalte erzeugt:
2.1. Ansätze zur Phasenkopplung
7
• Befehle werden genau einmal innerhalb der Gesamtausführungszeit des Grundblocks ausgeführt.
• Es können nie mehr Befehle auf einem Ressourcentyp ausgeführt werden als
Instanzen dieses Typs vorhanden sind.
• Datenabhängigkeiten müssen erfüllt sein.
Auch hier können zusätzliche Beschränkungen die Registerzuteilung einbeziehen. Die
Anzahl der Variablen hat die Größenordnung O(n · m2 ) mit O(k 3 ) Beschränkungen,
wobei n die Anzahl der Ressourcentypen, m die Anzahl der Befehle und k die Anzahl
der Knoten des Datenflussgraphen ist.
In der Praxis ist SILP für die Integration der Registerzuteilung besser geeignet,
während OASIC seine Vorteile hauptsächlich bei hoher Parallelität auf Befehlsebene
ausspielen kann ([Käst00], Seiten 79 f.).
Bei größeren Programmstücken werden Approximationen benutzt, um die Laufzeiten in Grenzen zu halten. So können auch größere Grundblöcke oder Superblöcke“ –
”
das sind Kontrollstrukturen mit genau einem Eingangsblock und genau einem Ausgangsblock (für die genaue Definition, vgl. [Käst00], Seiten 12 und 97) – mit nahezu
optimalen Lösungen und akzeptablen Laufzeiten des Optimierers bearbeitet werden. Damit werden in Beispielen Laufzeiten im Bereich von Sekunden bis Minuten
erreicht (siehe [Käst01], Seite 16 bzw. 17, detaillierter in [Käst00], Kapitel 10, Seiten
183 ff.).
2.1.2
Constraint Logic Programming
Bashford und Leupers stellen in [BaLe99b, BaLe99a] einen Ansatz zur phasengekoppelten Codeerzeugung auf der Basis von Constraint Logic Programming vor.
Als Zwischensprachen werden Baumdarstellungen oder azyklische Datenflussgraphen
verwendet. Es werden zuerst Überdeckungen von Datenflussgraphen mit Mustern gefunden. Die Muster, die die Autoren mit dem Begriff Factored Register Transfer“
”
(FRT) bezeichnen, sind definiert mit
• der Operation,
• den möglichen Speicherressourcen (Register, Speicherzugriffe, virtuelle Ressourcen zur Darstellung komplexerer Muster) für den definierten und die verwendeten Werte,
• zusätzlichen Ressourceninformationen, nämlich Funktionseinheiten zur Ausführung, Kosten und Maschinenbefehlstyp zur Steuerung der eventuell möglichen parallelen Ausführung, sowie
• sonstigen Beschränkungen.
In den Beschränkungen können Zusammenhänge zwischen der konkreten Wahl von
Speicherressourcen oder zwischen Speicherressourcen und Funktionseinheiten usw.
dargestellt werden. Bei der Graphüberdeckung mit diesen Mustern werden zusätzlich
Beschränkungen erzeugt, die darstellen, dass das Ergebnis eines Operandenknotens
8
2. Verwandte Arbeiten
eventuell von der Ergebnis-Speicherressource in die Operanden-Speicherressource des
verwendenden Knotens übertragen werden muss. Die Komplexität der Überdeckung
wird mit O(N ·D2 ) angegeben, wobei N die Anzahl der Knoten im Datenflussgraphen
und D das Maximum der Anzahlen von Funktionseinheiten, Speicherressourcen und
Befehlstypen ist. Anschließend wird mit Hilfe des Systems ECLiPSe eine optimale
bzw. bei größeren Problemen eine heuristisch nahezu optimale Lösung gesucht, die
die Kosten (als Summe der einzelnen Muster- und Transferkosten) minimiert und
dabei die vorhandenen Beschränkungen erfüllt. Hierbei werden die Entscheidungen
über die Bindung von Befehlen an Speicherressourcen und Funktionseinheiten aufgeschoben. Während einer listenbasierten Befehlsanordnung werden gegebenenfalls
zusätzliche Beschränkungen für diese Entscheidungen erzeugt. Erst am Ende werden
die Zuweisungen unter Berücksichtigung aller dieser Beschränkungen festgelegt. Dadurch wird die Phasenkopplung zur Befehlsanordnung erreicht. Laut [BaLe99b], Seite 28, werden für Bäume generell gute Laufzeiten des Codegenerators bei optimaler
Codeauswahl erzielt. Bei Graphen ist allerdings ein Ausweichen auf die heuristische,
suboptimale Strategie nötig.
2.1.3
Genetische Algorithmen
In [LLMD+ 01] stellen Lorenz, Leupers, Marwedel, Dräger und Fettweis eine Methode
vor, um möglichst energiesparenden Code für DSPs aus azyklischen Datenflussgraphen zu erzeugen. Dabei wird ein genetischer Algorithmus verwendet. Mögliche Lösungen werden als Chromosomen“ betrachtet, die wiederum aus einzelnen Genen“
”
”
bestehen. Es soll eine gute Kombination von Allelen“ (konkrete Werte für die ein”
zelnen Gene) gefunden werden. Dazu werden auf eine Population von Chromosomen
die Operationen der Selektion, der Mutation und des Crossover angewendet. Es wird
eine Startpopulation erzeugt (Initialisierung) und bewertet. Dann werden wiederholt
eine Selektion (probabilistisch anhand der Bewertung auswählen, welche Individuen ihre Information an die nächste Generation vererben“), Crossovers (Austausch
”
von Genen zwischen Individuen) und Mutationen (Veränderung einzelner Allele)
durchgeführt. Die Ergebnisse werden wieder bewertet. Anschließend wird entweder
zur Selektion zurückgesprungen oder der Algorithmus beendet, je nachdem, ob eine Abbruchbedingung erfüllt ist. Konkret wird zur Initialisierung eine Menge von
Individuen geschaffen, indem jeweils in einer listenbasierten Befehlsanordnung probabilistisch ein nächster ausführbarer Befehl ausgewählt wird. Jedes Gen entspricht
einer möglichen Überdeckung eines Graphknotens mit Annotation der zuzuteilenden Ressourcen (Register/Speicherstellen, Funktionseinheiten). Die Befehlsauswahl
wird hierbei integriert, indem jeweils erst nach der Auswahl des nächsten ausführbaren Graphknotens ein Befehl ausgewählt wird. Die Bewertungsfunktion kann dem
jeweiligen Optimalitätskriterium angepasst werden (z.B. Geschwindigkeit, Echtzeitverhalten oder Energieverbrauch). Der Crossover erfolgt durch zufälligen Austausch
von Genen. Die Mutation ähnelt etwas der Initialisierung und enthält zusätzlich
eine Korrektheitsprüfung, da durch den willkürlichen Austausch im Crossover gegebenenfalls Inkonsistenzen auftreten können. Im Experiment wurde mit akzeptablen
Laufzeiten im Schnitt relativ guter Code produziert (siehe [LLMD+ 01], Tabelle 2).
Es wurden für n Knoten im Graph 20 · n Generationen mit einer Populationsgröße
von 30 verwendet. Die Verbesserungen gegenüber klassischer baumbasierter Codeerzeugung waren im Bereich von ca. 30 % weniger Befehlen, ca. 30 bis 40 % weniger
Speicherzugriffen und ca. 20 bis gut 30 % geringerem simulierten Energieverbrauch.
2.1. Ansätze zur Phasenkopplung
9
Die Laufzeiten bewegten sich von 3 Sekunden einer 333 MHz-Ultra-10-CPU bei 12
Graphknoten bis zu 256 Sekunden bei 80 Graphknoten.
2.1.4
Dynamisches Programmieren
In [KeBe01] stellen Keßler und Bednarski eine Methode zur Phasenkopplung zwischen Befehlsauswahl und -anordnung auf Grundblockebene für Architekturen mit
einem beschränkten Satz von Allzweckregistern vor. Die Grundidee ist eine topologische Sortierung eines DAG, der die Abhängigkeiten zwischen Operationen beschreibt. Damit ist dieses Verfahren entlang der Befehlsanordnung organisiert, die
Befehlsauswahl erfolgt verzahnt während der Anordnung. Bei gewöhnlicher listenbasierter Befehlsanordnung wird an jeder Stelle, an der eine Entscheidungsmöglichkeit
besteht, sofort anhand einer Heuristik eine Entscheidung getroffen und festgelegt.
Im vorliegenden Ansatz dagegen geht man als Idee von dem Suchbaum der möglichen topologischen Sortierungen aus. An jeder Stelle im Baum wird als Information
die Menge der DAG-Knoten, die zur Auswahl zur Verfügung stehen (d.h. keine Vorgänger oder alle Vorgänger bereits ausgewählt), und der Maschinenzustand (d.h.
Vorgeschichte der Belegung der Funktionseinheiten durch die bisher ausgewählten
Befehle) mitgeführt. Desweiteren werden die Knoten attributiert mit der Anordnung
der bisher ausgewählten Befehle und der Zahl der für diese benötigten Prozessortaktzyklen. Aus dem Suchbaum wird ein Suchgraph, indem Knoten zusammengelegt
werden, die sich nur in der Anordnung, aber nicht in der Menge der ausgewählten
Befehle und/oder dem Zeitverbrauch unterscheiden. Dabei wird lediglich die bislang beste Anordnung der Befehle und der zugehörige beste Zeitverbrauch belassen.
Dies beeinträchtigt die Optimalität nicht, siehe Satz 1 auf Seite 5 von [KeBe01].
Die Graphknoten werden in eine Matrix einsortiert, die durch die Anzahl der bereits ausgewählten Befehle und die bisher nötigen Taktzyklen indiziert ist. Damit
kann letztlich ein dem dynamischen Programmieren entsprechender Algorithmus die
Suche implementieren, wobei die äußere Schleife über den Zeitindex und die innere
über die Anzahl der bereits gewählten Befehle läuft. Im Gegensatz zur simplen Aufzählung aller möglichen topologischen Sortierungen, die bis zu ca. 15 Graphknoten
praktikabel ist, ist mit dem vorliegenden Ansatz eine Optimierung für bis zu ca. 50
Graphknoten praktikabel, allerdings schon in der Größenordnung von CPU-Stunden
([KeBe01], Seite 7).
2.1.5
Heuristische Suche
Hanono stellt einen Ansatz namens AVIV vor, der eine phasengekoppelte Codegenerierung für Grundblöcke mittels heuristischer Suche auf Ausdrucks-DAGs erreicht [Hano99, HaDe98]. Das Frontend erzeugt eine Sammlung von gewöhnlichen
Datenflussgraphen sowie Information über den Kontrollfluss zwischen diesen. Im
Codegenerator wird daraus zuerst eine so genannte Split-Node DAG“-Darstellung
”
erzeugt. Bei allen Knoten des ursprünglichen DAG werden als Vorgänger sogenannte Split-Knoten ( split nodes“) eingefügt, deren Vorgänger die Vorgänger aus dem
”
ursprünglichen Graphen sind. Dieser Vorgänger wird dupliziert (der Split-Knoten
hat in dem Fall also mehrere Vorgänger), wenn er auf verschiedenen Funktionseinheiten des Prozessors ausgeführt werden kann. Falls zwischen diesen Vorgängern und deren Verwendung noch Transfers nötig sind, werden noch dazu Transferknoten eingefügt. Aus einer Kante Use → Def wird also eine Reihe von Pfaden
Use → Split → Transfer1 → Def1 , . . . , Use → Split → Transfern → Defn . Auf
10
2. Verwandte Arbeiten
dieser Darstellung werden mittels Graphüberdeckung in einer ersten Phase simultan
die Befehlsauswahl, die Zuteilung von Funktionseinheiten und Registersätzen, die
Gruppierung in Instruktionen und die Befehlsanordnung durchgeführt. Es wird zuerst der Suchraum der Entscheidungsmöglichkeiten an jedem Split-Knoten mit einer
heuristischen Kostenbewertung beschnitten, die berücksichtigt, wieviel Parallelität
(z.B. durch VLIW) durch Entscheidungen aufgegeben wird und wieviele zusätzliche
Transfers (Transferknoten) nötig sind. Dann werden für die übrigen Auswahlmöglichkeiten (mit (∗) bezeichnet) die nötigen Transferknoten hinzugefügt. Dies führt
wieder zu einer heuristischen Auswahl, wenn die Datenpfade beschränkt sind; hier
basiert die Heuristik nur auf dem Grad der möglichen Parallelität. Dann werden
maximale Gruppierungen von parallel ausführbaren Graphknoten gefunden (nicht
voneinander abhängig, nicht im Konflikt um Funktionseinheiten). Da es zu lange
dauern würde, alle diese Gruppierungen zu erzeugen, wird wieder eine heuristische
Beschränkung eingeführt: Es werden nur Graphknoten kombiniert, deren Tiefe (Abstand zur Wurzel des Graphen) bzw. Höhe (Abstand zu den Blättern) nicht zu weit
voneinander abweichen. Eventuell auf der Zielarchitektur aus besonderen Gründen
ungültige Gruppierungen werden verworfen. Jetzt wird tatsächlich eine möglichst
kostengünstige Anordnung gesucht: Unter den Gruppierungen, deren Vorgänger alle
bereits ausgewählt sind, wird eine möglichst große Gruppe ausgesucht. Wenn mehrere maximal große Gruppen existieren, wird geschätzt, wieviele weitere Gruppen (d.h.
Befehlsworte) noch gebraucht werden, wenn die betreffende Gruppe gewählt wird,
und danach entschieden. Dabei werden Zähler über die Registerverwendung (Anzahl
lebendiger Werte je Registersatz) mitgeführt und laufend Entscheidungen über das
Zwischenspeichern von Werten gefällt. Falls dieses nötig ist, wird der ursprüngliche
Transferknoten an dieser Stelle durch Speicher- und Ladeknoten ersetzt, die Gruppierungen für die Graphknoten, die noch nicht ausgewählt wurden, neu berechnet
und die Auswahl von Gruppierungen fortgesetzt. Nachdem dies für alle Auswahlen
(∗) durchgeführt worden ist, wird die günstigste dieser Wahlmöglichkeiten als generierter Code festgelegt. In einer abschließenden Phase werden die Register vollends
zugeteilt und Guckloch-Optimierungen ausgeführt. Die Laufzeiten des Codegenerators auf einer Sun Ultra-30/300 sind im Bereich von bis zu 10 Sekunden für DAGs
von 16 Knoten (106 Knoten in der Split-Node DAG“-Darstellung). Ohne heuris”
tische Beschränkungen liegt die Laufzeit bei denselben Beispielen bei bis zu 120
Sekunden.
2.1.6
Mutation Scheduling
In [NoNi95, NoND95] stellen Novack, Nicolau und Dutt eine Methode zur Integration der Befehlsauswahl in die Befehlsanordnung dar. In der Zwischendarstellung, die eine modifizierte SSA-Form ist, wird zu jedem Wert (SSA-Knoten) eine
Annotation erzeugt, auf welche verschiedene Weisen der Wert berechnet werden
kann ( Mutations“)1 , z.B. Synonyme (Zuweisung = Addition mit 0 = Multiplikati”
on mit 1 etc.), zusammengesetzte Befehle (z.B. Multiplikation und Addition oder
auch explizites Bypassing z.B. beim Intel i860) oder Reduktion der Höhe eines
(Teil)ausdrucksbaumes. Wenn dann während der Befehlsanordnung Ressourcenkonflikte festgestellt werden, wird versucht, einen Befehl, der bereits ausgewählt ist,
1
Zu beachten ist, dass der hier verwendete Begriff Mutations“ nicht mit den Mutationen in
”
einem genetischen Algorithmus zusammenhängt.
2.1. Ansätze zur Phasenkopplung
11
durch andere Varianten derselben Berechnung auszutauschen ( Mutation“), um die”
sen oder andere Befehle dann günstiger anordnen zu können und/oder den Registerdruck zu verringern. Laut [NoNi95], Seiten 11 ff., werden signifikante Verbesserungen
gegenüber der Verwendung genau desselben Codegenerators ohne Mutation erzielt.
Über die Laufzeit des Codegenerators wird in [NoNi95, NoND95] keine Aussage getroffen.
2.1.7
Software-Pipelining
Mesman, Strik, Timmer, van Meerbergen und Jess stellen einen Ansatz zur Kopplung
der Registerzuteilung und Befehlsanordnung vor [MSTM+ 97], der aus einer besonderen Form von Abhängigkeitsgraphen eine Befehlsanordnung und Registerzuteilung
berechnet. Der Graph besteht aus Knoten für jeden Befehl und Pseudobefehlen für
den Anfang und das Ende des zu bearbeitenden Codestücks. Die Kanten sind gerichtet und mit minimalen Latenzen sowie gegebenenfalls Namen für die entlang
der Kanten transportierten Werte bezeichnet. Zusätzliche Eingaben sind Ressourcenkonflikte zwischen Operationen, die Bindung der Werte an Registerbänke, eine
Gesamtlatenz l und ein Initialisierungsintervall ( initialization interval“) für die ent”
haltende Schleife (II).2 Dieses beschränkt die maximale Lebensdauer von Werten in
Registern und muss bei der Beachtung von Ressourcenkonflikten berücksichtigt werden: Befehl B kann nicht genau II Zyklen nach Befehl A ausgeführt werden, wenn
diese auf derselben Funktionseinheit laufen, da sonst die erste Ausführung von B mit
der zweiten von A in Konflikt gerät. Diese werden in den Graphen eingearbeitet, indem Rückwärtskanten vom Endeknoten zum Startknoten mit der Bezeichnung −l
(d.h. der Start kann frühestens −l Zyklen nach, also l Zyklen vor dem Ende erfolgen) und von Verwendungen von Werten zu deren Produktion mit der Bezeichnung
−II eingefügt werden. Anschließend wird eine initiale Registerzuteilung gewählt.
Dieser entsprechend werden zusätzliche Beschränkungen (Kanten) in den Graphen
eingearbeitet: Wenn z.B. ein Befehl A einen Wert für die Verwendung B produziert und ein Befehl C für Verwendung D und wenn beide Werte im selben Register
platziert werden sollen, muss entweder B vor C oder D vor A ausgeführt werden.
Wenn durch zusätzliche Beschränkungen schon klar ist, dass D nach A oder C nach
A ausgeführt wird, kann ohne Verlust von möglichen Anordnungen also eine Kante
von B nach C eingefügt werden. Danach wird geprüft, ob mit diesen zusätzlichen
Beschränkungen eine Anordnung möglich ist. Wenn ja, wird diese ausgegeben und
der Algorithmus terminiert. Wenn nein, wird eine möglichst kleine Menge von durch
die Registerzuteilung generierten Beschränkungen erzeugt, die für die Unmöglichkeit
der Anordnung verantwortlich ist. Diese Analyse ist nach Aussage der Autoren in
O(E) (E = Anzahl der Kanten im Graphen) möglich. Mit dieser Information wird
dann die Registerzuteilung revidiert und von vorn angefangen. Die Gesamtlaufzeit
wird nach den auf Seite 372 genannten Ergebnissen hauptsächlich von der Zahl der
nötigen Iterationen bestimmt. In den genannten Beispielen sind dies bis zu 20, mit
einer Laufzeit von 25 Sekunden.
In [PiME99] wird ein ähnlicher Ansatz vorgestellt. Solange bei einer Abschätzung
mit den vorhandenen Zeitabhängigkeiten im Abhängigkeitsgraphen die Anzahl der
2
Diese und die unten genannte Arbeit bezieht sich speziell auf Schleifencode, auf dem ein
Software-Pipelining durchgeführt wird. Das Initialisierungsintervall bezeichnet die Periode der
Schleife im Ergebnis, die Gesamtlatenz den Zeitabstand zwischen der i-ten Ausführung des ersten Befehls bis zur i-ten Ausführung des letzten Befehls.
12
2. Verwandte Arbeiten
sich überlappenden Lebensdauern (also von dem frühestmöglichen Zeitpunkt eines
Produzenten bis zum spätmöglichsten Zeitpunkt des letzten Konsumenten) zu groß
für die jeweiligen Registerbänke ist, werden Lebensdauern durch das Einfügen zusätzlicher Abhängigkeiten sequenziert, die einander zwar nach bisheriger Informationslage überlappen können, sich aber nicht zwingend überlappen. Für die genaue
Auswahl, welche Lebensdauern sequenziert werden, wurden in der Arbeit unterschiedliche Heuristiken erprobt. Von einer Menge von 60 Beispielprogrammstücken
wurden jedoch bei allen diesen Heuristiken nur für 44 bis 50 Stück Lösungen gefunden. Der Zeitaufwand bewegt sich im Bereich von 225 bis 720 Sekunden auf einem
Pentium II mit 233 MHz.
2.1.8
Prozessorregister als Funktionseinheiten
In [ZeWe01] wird eine Kombination von der Registerzuteilung mit der Befehlsanordnung vorgestellt. Zuerst wird entschieden, welche Werte in Registern gehalten
werden, und nötigenfalls Code zur Zwischenspeicherung erzeugt. Dann werden die
zugeteilten Register als Verwendung von virtuellen Ressourcen modelliert. Die Knoten des Abhängigkeitsgraphen sind den Ressourcen zugeordnet, die zur Ausführung
notwendig sind (Funktionseinheiten, Busse usw.), und die Kanten werden virtuellen
Ressourcen zugeordnet, die den möglichen Registerzuteilungen entsprechen. Wenn
mit diesen Zuordnungen eine Befehlsanordnung möglich ist, ist garantiert, dass dann
in einem getrennten Schritt die Zuweisung der konkreten Register auf jeden Fall möglich ist. Es werden bei N verschiedenen Registersätzen höchstens 2N − 1 virtuelle
Ressourcen benötigt.
2.1.9
Der TM-1000-Compiler
Im TriMedia-TM-1000-Compiler wird die Befehlsanordnung auf bewachten Entscheidungsbäumen (guarded decision trees) durchgeführt [HoAu99]. Diese sind Bäume
von Grundblöcken, in denen die Wurzel der einzige Punkt ist, an dem diese Menge von Grundblöcken betreten werden kann. Alle anderen Grundblöcke in diesen
Entscheidungsbäumen dürfen nur einen Vorgänger haben. Die Grundblöcke werden
in ihrer Ausdrucksmöglichkeit erweitert, indem sie konditionelle Befehle (predicated
instructions) enthalten dürfen. Die Registerzuteilung wird integriert, indem globale Lebensdauern, die einen einzelnen bewachten Entscheidungsbaum überspannen,
konventionell auf Register zugeteilt werden (Graphenfärbung). Lokale Lebensdauern
werden Registern zugewiesen, sobald in der Befehlsanordnung der erste Befehl davon ausgegeben wird. Der Registerdruck wird dabei heuristisch kontrolliert, indem
dieser auf die Anordnungspriorität Einfluss hat und flexible Anweisungen“ ( floa”
”
ters“) so spät wie möglich ausgegeben werden. Solche flexiblen Anweisungen sind
Blätter des betrachteten DAG und Anweisungen, die nur von genau einem solchen
Blatt abhängen. Bis auf diese Modifikationen wird ein relativ konventioneller Ansatz zur Befehlsanordnung realisiert: Innerhalb des bewachten Entscheidungsbaumes
wird grundblockweise vorgegangen und innerhalb der Grundblöcke wird eine listenbasierte Befehlsanordnung durchgeführt. Es wird in [HoAu99] nicht speziell auf die
Auswirkung der Phasenkopplung eingegangen, da sich die Autoren bei der Auswertung hauptsächlich auf die Befehlsanordnung als solche konzentrieren.
2.2. Auswahl von DSP-Befehlen
2.2
13
Auswahl von DSP-Befehlen
Neben der Phasenkopplung gibt es als weiteres Ziel die Nutzung der bei DSPArchitekturen oft vorhandenen besonderen Befehle für typische DSP-Anwendungen.
Auch wenn diese zum Teil auch bei den oben genannten Methoden zur Phasenkopplung ausgewählt werden (z.B. Multipliziere-und-Addiere-Anweisungen), gibt es
weitere gezielte Ansätze hierfür.
2.2.1
Vektorisierung
Eine typische Klasse von DSP-Befehlen sind datenparallele Berechnungen, z.B. vierfache Addition. Diese Befehle zeigen eine Ähnlichkeit zu den Möglichkeiten von Vektorprozessoren. Von daher ist es prinzipiell möglich, die Optimierungsmethoden für
diese auch für die Auswahl datenparalleler DSP-Befehle zu verwenden. Die Vektorisierung ist bereits lange bekannt, siehe z.B. [Lamp73, Lamp74]. In [Wolf96] sind
die dafür nötigen Grundlagen behandelt: Abhängigkeitsanalyse in Kapitel 5 bis 8,
Schleifenrestrukturierung in Kapitel 9 und automatische Vektorisierung in Kapitel
12.
In [ChLa97] stellen Cheong und Lam exemplarisch für den VIS-Befehlssatz von
Sun eine automatische Codegenerierung für DSP-Befehlssätze vor. Sie verwenden
einen vorhandenen SUIF-basierten Vektorisierer ([KLMT+ 96]), der den ursprünglichen Code auf eine abstrakte Vektormaschine (unbeschränkt viele, unbeschränkt lange Vektorregister; Befehle: Vektor laden, Vektorvergleich, Vektorarithmetik, Vektor
speichern) übersetzt. Daraus wird dann der eigentliche Code erzeugt, indem Streifen
von für den DSP-Befehlssatz passender Größe geschnitten werden und dann die abstrakten Vektorbefehle konkret umgesetzt werden. Dabei werden gegebenenfalls Anpassungen erzeugt, wie die Behandlung von Randbedingungen (Anzahl der Schleifendurchläufe nicht durch die Streifengröße teilbar) oder ungünstiger Datenausrichtung
sowie Formatanpassungen. Wenn aus irgendwelchen Gründen keine DSP-Befehle verwendet werden können, werden wieder konventionelle Schleifen erzeugt.
Auch in [SrGo00] wird als Grundlage SUIF benutzt und damit Code für Intels
Multimedia-Befehlssatzerweiterung MMX erzeugt. Zuerst wird mit Hilfe von Abhängigkeitsanalysen identifiziert, welche Teile überhaupt vektorisierbar sind. Danach
werden die Schleifen in passende Streifen geschnitten. Skalare werden nötigenfalls expandiert und Reduktionen (z.B. Bildung von Summen) transformiert, um die Abhängigkeitsstruktur zu verbessern. Anschließend wird die Schleifenkontrolle neu verteilt
(Loop Distribution) und abschließend wieder C-Code mit eingebettetem Assemblercode ausgegeben. In Benchmarks werden oft deutliche Verbesserungen erreicht.
In [LoWD02] stellen Lorenz, Wehmeyer und Dräger einen Codegenerator vor, der unter Einbeziehung des oben bereits dargestellten genetischen Algorithmus [LLMD+ 01]
(siehe Abschnitt 2.1.3) zusätzlich noch durch Schleifenoptimierungen SIMD-Befehle
und besondere Schleifenkonstrukte von DSPs ausnutzen kann. Es wird zwar eine Methode zur Vektorisierung auf Grundblockebene beschrieben, bei der die auf
Grundblockebene verfügbare Parallelität durch das Ausrollen von Schleifen erhöht
wird und anschließend im Grundblock zu SIMD-Befehlen kombinierbare Einzelbefehle identifiziert werden (nach [Leup00], siehe auch Abschnitt 2.2.3). Die Autoren
14
2. Verwandte Arbeiten
entscheiden sich jedoch gegen diese Methode, weil sie zum einen zu hohe Codegeneratorlaufzeiten erwarten, zum anderen auch die Codegröße durch das Ausrollen
der Schleifen zu groß wird, falls die Vektorisierung auf Grundblockebene für das gegebene Programmstück nicht möglich ist, vgl. [LoWD02], Abschnitt IV A. Daher
entscheiden sich die Autoren von [LoWD02] gegen diesen Ansatz und für klassische
Vektorisierung: Zuerst werden Schleifen analysiert (Identifikation der Grundblöcke
einer Schleife, Induktionsvariablen und Schleifengrenzen). Dann wird durch Abhängigkeitsanalysen festgestellt, ob eine Vektorisierung möglich ist, und diese gegebenenfalls durch Transformationen (z.B. Skalarexpansion, Schleifenteilen) ermöglicht.
Abschließend wird die Vektorisierung in der verwendeten Zwischensprache annotiert.
Die Nutzung von spezifischen Schleifenkonstrukten ist einfach: Wenn die Zahl der
Iterationen einer Schleife statisch bekannt ist, wird die Schleifenkontrolle durch Befehle zur Initialisierung und zum Abschluss der Schleife ersetzt und anschließend
toter Code (Induktionsvariablen) entfernt.
2.2.2
Musterersetzung auf Quelltextebene
In [MaKC99] wird vorgestellt, wie Code für DSP-Befehlssätze (exemplarisch wieder
Suns VIS-Befehlssatz) durch Musterersetzung auf Quelltextebene ausgewählt wird.
Es wird ein programmierbares Codetransformationswerkzeug für ANSI C namens
ctt verwendet. Dieses erlaubt, Muster zu definieren, die in einem Quelltext erkannt
werden. Es können auch weitere Bedingungen angegeben werden. Wenn ein Muster
erkannt und dessen Bedingungen erfüllt sind, wird der Code durch ein in der Regel
angegebenes Fragment ersetzt. In der Beispielanwendung werden z.B. Schleifen zur
Berechnung von Skalarprodukten ersetzt durch eine transformierte Fassung, in der
den VIS-Befehlen entsprechende Funktionsaufrufe generiert werden. Diese werden
dann als Inline-Assembler-Funktionen definiert und damit zu echten DSP-Befehlen
übersetzt. In Benchmarks wurden mittels der Muster laut [MaKC99], Seite 7, 85 %
der SIMD-geeigneten Schleifen tatsächlich erkannt und transformiert.
2.2.3
Integer Linear Programming
In [Leup99] werden zwei Optimierungstechniken für Media-Prozessoren vorgestellt:
Ausnutzung von konditionellen Befehlen (predicated instructions) sowie Verwendung von Multimedia“-Befehlen. Ersteres wird mit dynamischer Programmierung
”
erreicht: Bei geschachtelten Bedingungen gibt es zwei Entscheidungsmöglichkeiten,
nämlich ob konditionelle Anweisungen oder bedingte Sprünge verwendet werden und
ob zusätzlicher Code nötig ist, um äußere Bedingungen immer korrekt einzubeziehen, wenn für diese das Übersetzungsmodell mit konditionellen Anweisungen gewählt
wurde. Es werden zuerst von den inneren Verzweigungen nach außen die Kosten für
die 4 Entscheidungsmöglichkeiten geschätzt. Danach wird von außen nach innen
die Entscheidung endgültig gefällt. Zur Ausnutzung von Multimedia-Befehlen wird
Code aus Datenflussgraphen erzeugt. Diese werden zuerst in Ausdrucksbäume zerschnitten. Jedoch wird für diese dann nicht eine Überdeckung, sondern Mengen von
alternativen Überdeckungen berechnet. Danach wird für alle Teilbäume des Datenflussgraphen simultan ein ganzzahliges lineares Programm konstruiert. Dieses soll die
Verwendung von DSP-Befehlen maximieren. Als Beschränkungen werden erzeugt:
• Die passenden Einzelbefehle müssen vorliegen.
2.3. Zusammenfassung und Bewertung
15
• Abhängigkeiten müssen eingehalten werden.
• Bei Speicherzugriffen müssen die Operanden im Speicher so angeordnet sein,
dass die Lade- oder Speicheroperationen kombiniert werden können und dadurch eine korrekte Platzierung im Register erreicht wird.
Die ILP-Modellierung ist in [Leup00] genauer beschrieben: Für jeden Knoten im
Datenflussgraphen ni gibt es mögliche anzuwendende Regeln rj . Die Entscheidungsvariable xij ∈ {0, 1} besagt, dass an Knoten ni Regel rj ausgewählt wird. Jeder
P
Knoten darf letztlich nur von einer Regel überdeckt werden: ∀i : j xij = 1. Desweiteren müssen die Auswahlen zueinander passen: Wenn nach Auswahl der Regel rj an
Knoten ni in davon abhängigen Knoten nd die Auswahl eingeschränkt wird, wird das
P
von Ungleichungen der Form xij ≤ k xdk , wobei k nur über die nach Auswahl der
Regel rj möglichen Regeln für nd läuft. Desweiteren müssen an gemeinsamen Teilausdrücken die Wahlen der Registerart (gesamtes Register, obere/untere Hälfte, usw.)
konsistent sein, was zu weiteren Gleichungen führt. Weiter gibt es Entscheidungsvariablen yij für die Kombination von SIMD-Teilbefehlen für alle möglicherweise kombinierbaren Paare von Knoten (ni , nj ). Diese werden in Beziehung gesetzt damit,
dass falls yij = 1 für ni eine Regel ausgewählt sein muss, die die obere Registerhälfte
berechnet, und dass für nj eine Regel ausgewählt sein muss, die die untere Registerhälfte berechnet. Auch können Knoten höchstens in ein solches Paar eingeordnet
werden. Desweiteren werden zyklische Abhängigkeiten für die Befehlsanordnung vermieden: Falls ni → nj , nk → nl , können nicht gleichzeitig Paare (ni , nl ) und (nj , nk )
gebildet werden. Dies kann auch für andere Arten von SIMD-Befehlen erweitert werden. In [LeBa00] wird zu dieser Methode ausgesagt, dass die Rechenzeiten für größere
Probleme lange werden und als Lösung vorgeschlagen, große Datenflussgraphen in
kleinere zu zerteilen und getrennt zu behandeln. In [Leup00], Seite 5 sind genauere
Ergebnisse genannt. Die Laufzeiten der Codegenerierung gehen bis zu einer halben
Minute für Graphen mit 95 Knoten.
2.3
Zusammenfassung und Bewertung
Die vorgestellten verwandten Arbeiten zur Phasenkopplung fassen wir in Tabelle 2.1,
die zur Identifikation von SIMD-Befehlen in Tabelle 2.2 zusammen. In Tabelle 2.1
bedeutet Aus“ die Befehlsauswahl, An“ die Anordnung und Reg“ die Registerzu”
”
”
teilung. In einigen Arbeiten ist nicht klar ersichtlich, wie gut die Anpassung auf neue
Architekturen (Retargierung) unterstützt wird. In diesem Falle geben wir ein ?“ an,
”
während wir −“ angeben, wenn eine Arbeit sich von vornherein auf eine bestimmte
”
Architektur konzentriert hat. In der Spalte Zeit“ fassen wir das Laufzeitverhalten
”
zusammen. Wir sehen Laufzeiten im Bereich von Sekunden oder Minuten als akzeptabel an und bezeichnen dies mit +“, wesentlich längere Laufzeiten dagegen mit
”
−“. Wo in den Arbeiten keine Aussage über die Codegeneratorlaufzeiten getroffen
”
wird, geben wir ein ?“ an.
”
Die meisten Arbeiten werden der Anforderung, direkt aus möglicherweise zyklischen
Graphdarstellungen, insbesondere SSA-Graphen, Code zu erzeugen, nicht gerecht, da
sie auf baumbasierten oder DAG-Darstellungen arbeiten. Bei der ILP-basierten Arbeit von Wilson et. al. [WGHB95] sind die Laufzeiten der Codegenerierung zu groß.
Im Mutation-Scheduling [NoNi95] wird zwar auf einer modifizierten SSA-Darstellung
16
2. Verwandte Arbeiten
Arbeit
ILP [WGHB95]
PROPAN [Käst97]
CLP
[BaLe99b, BaLe99a]
Genetisch [LLMD+ 01]
DP [KeBe01]
AVIV [Hano99, HaDe98]
Mutation [NoNi95]
SW-Pipelining
[MSTM+ 97]
SW-Pipelining [PiME99]
Register als Funktionseinheiten [ZeWe01]
TM-1000 [HoAu99]
Phasen
Aus+An+Reg
An+Reg
Aus+An+Reg
zykl. Graph
+
−
−
Retargierbarkeit
?
+
+
Zeit
−
+
+
Aus+An+Reg
Aus+An
Aus+An+Reg
Aus+An+Reg
An+Reg
−
−
−
+
(+)
?
?
?
?
?
+
−
+
?
+
An+Reg
An+Reg
(+)
−
?
?
+
?
An+Reg
−
?
?
Tabelle 2.1: Arbeiten zur Phasenkopplung
Arbeit
Vektorisierung für VIS [ChLa97]
Vektorisierung für MMX [SrGo00]
Vektorisierung [LoWD02]
Musterersetzung [MaKC99]
ILP [Leup99, Leup00]
zykl. Graph
−
−
−
−
−
Retargierbarkeit
−
−
?
+
?
Zeit
?
?
?
?
+
Tabelle 2.2: Identifikation von SIMD-Befehlen
gearbeitet, jedoch nicht genau angegeben, wie die Zyklen behandelt werden. Die
Techniken auf der Basis von Software-Pipelining ([MSTM+ 97] und [PiME99]) behandeln zwar Zyklen, aber nur auf innere Schleifen spezialisiert. Daher kann nur
eingeschränkt davon die Rede sein, dass sie zyklische Graphen behandeln.
Bisher erfolgt die Auswahl von SIMD-artigen Befehlen entweder auf Quelltext- bzw.
Zwischensprachebene (Vektorisierung, Musterersetzung auf Quelltextebene) oder es
werden besondere Techniken im Rahmen der Codegenerierung eingesetzt (Abschnitt
2.2.3).
Auch wenn die Identifikation von SIMD-Parallelität auf höherer Abstraktionsebene
wie z.B. im Quelltext (bzw. im abstrakten Syntaxbaum) oder in der Zwischensprache notwendig ist, um komplexere Analysen durchführen zu können, erwarten wir,
dass sich auch weitere Möglichkeiten der SIMD-Parallelität erst auf der Ebene der
Codeerzeugung ergeben.
Die Methode von Leupers [Leup99] (s. Abschnitt 2.2.3) ist spezialisiert auf die Erkennung von SIMD-Befehlen. Sie integriert dies jedoch nicht mit den allgemeineren
Aufgaben der Befehlsauswahl. Hierfür wird auf traditionelle Baumüberdeckung zurückgegriffen.
2.3. Zusammenfassung und Bewertung
17
Wir wollen im weiteren Verlauf dieser Arbeit zeigen, wie wir die zwei Teilprobleme
der Phasenkopplung zwischen Befehlsauswahl und -anordnung und der Identifikation
von SIMD-Befehlen in einem integrierten Ansatz lösen können.
18
2. Verwandte Arbeiten
3. Grundlagen
Im Folgenden stellen wir vor, auf welche Grundlagen wir zurückgreifen konnten, um
die für diese Arbeit gesteckten Ziele zu erreichen. Dies umfasst auch die Ergebnisse
von Boesler [Boes98], der bereits das Problem der Codegenerierung aus graphbasierten SSA-Darstellungen gelöst hat.
Zuerst beschreiben wir die verwendete Zwischensprache Firm, eine graphbasierte
SSA-Darstellung, vgl. Abschnitt 3.1. Dann erklären wir das Prinzip der Befehlsauswahl aus Graphen mit Hilfe von Bottom Up Rewrite Systems (BURS). Die Graphersetzung steuern wir mit heuristischer Suche, deren Grundlagen wir im Abschnitt 3.3
erläutern. Dann gehen wir auf die Maschinensimulation zur Befehlsanordnung ein,
siehe Abschnitt 3.4. Dieses Kapitel schließen wir mit einer kurzen Darstellung über
einen Algorithmus zur Erkennung von SIMD-Befehlen. Dessen Grundidee greifen wir
auf, setzen sie aber anders um.
3.1
Die SSA-Darstellung Firm
SSA-Darstellungen erlauben, interessante Optimierungen auf Zwischensprachebene effizient durchzuführen. Daher wurden an unserem Institut eine graphbasierte
Zwischendarstellung in SSA-Form namens Firm (Firm Intermediate Representation Mesh) [TrLB99] und eine Bibliothek zur Erzeugung und Bearbeitung derselben
[Lind02] entwickelt.
Die bezeichnende Eigenschaft von SSA-Darstellungen ist die statische Einmalzuweisung: Jede Variable wird genau an einer Stelle des Zwischenprogrammcodes zugewiesen. Wenn im Quelltext mehrere Zuweisungen an eine Variable auftreten, so muss
diese bei der Herstellung der SSA-Form an jeder dieser Zuweisungen umbenannt
werden. Die Verwendungen der Variable werden dann entsprechend angepasst.
Als Beispiel betrachten wir den Ausschnitt aus dem Euklidischen Algorithmus:
20
3. Grundlagen
if x < y
then
y := y − x
else
x := x − y
Verwendung von x und y . . .
Da x und y schon am Eingang dieses Codestücks Werte haben, müssen wir diesen
Werten auch eigene Namen geben. Damit ergibt sich:
if x1 < y1
then
y2 := y1 − x1
else
x2 := x1 − y1
Verwendung von x und y . . . ← ???
Jedoch können bei der Verwendung in der letzten Zeile verschiedene Versionen von
x bzw. y gültig sein, je nachdem, welcher Zweig der bedingten Verzweigung gewählt
wurde. Damit kann bei der Verwendung bis jetzt nicht angegeben werden, welcher
Name hier stehen soll.
Daher wird in einem solchen Fall, wenn zwei oder mehrere verschiedene Versionen
einer Variable einen Grundblock erreichen können, eine neue Operation eingefügt, die
sogenannte Phi-Auswahlfunktion. Die Operanden dieser Phi-Funktion entsprechen
den Werten in den Vorgängern des Grundblocks, der die Phi-Operation enthält. Das
Ergebnis wird einer neuen Version der Variablen zugewiesen, die dann im Rest des
Grundblocks einheitlich verwendet werden kann:
if x1 < y1
then
y2 := y1 − x1
else
x2 := x1 − y1
x3 := Φ(x1 , x2 )
y3 := Φ(y2 , y1 )
Verwendung von x3 und y3 . . .
Hiermit wird also dieses Problem behoben, es kann immer x3 bzw. y3 verwendet
werden.
In [CFRW+ 91] sind Algorithmen angegeben, wie man eine solche Darstellung effizient konstruieren und später zur Codeerzeugung die Phi-Operationen korrekt behandeln kann.
Die Darstellung Firm ist eine SSA-Darstellung, die Kontroll- und Datenfluss gemeinsam als Graph darstellt. Für das oben verwendete Codebeispiel hat der Graph die
in Abbildung 3.1 gezeigte Form.
Im Zusammenhang mit Schleifen wird der Graph zyklisch. Und zwar entstehen sowohl Kontrollflusszyklen, die aus Blockknoten sowie Sprung- oder Verzweigungsknoten (Jmp bzw. Cond) bestehen, als auch gegebenenfalls Datenflusszyklen, die jeweils
mindestens einen Phi-Knoten enthalten.
3.2. BURS
21
Block
Cmp
x(in)
y(in)
ProjLT
Cond
Block
Block
Sub
Sub
Jmp
Jmp
Block
Phi
x(out)
Phi
y(out)
Abbildung 3.1: SSA-Beispielgraph
Auch Abhängigkeiten, die durch Speicherzugriffe entstehen, werden ausdrücklich
dargestellt, indem ein besonderer Wert, der Speicherzustand, als zusätzliche Einund Ausgabe der Lade- und Speicheroperationen fungiert.1
3.2
BURS
Ein modernes Verfahren zur Codeerzeugung ist die Anwendung von Bottom Up
Rewrite Systems (BURS) [Pele88, NyKa97, NKWA96]. Boesler hat dieses Verfahren
auf Graphen erweitert, vgl. [Boes98], Seiten 39 ff.
3.2.1
Kostenbehaftete Termersetzungssysteme
Die Grundlage für BURS sind kostenbehaftete Termersetzungssysteme, siehe auch
die Darstellung in [Boes98], Seiten 19 f. Hierfür wird zunächst der Begriff von Termen
definiert.
Definition 3.1 (Alphabet mit Rang) Ein Alphabet mit Rang ist ein Paar Σ =
(S, r) mit einer endlichen Menge von Symbolen S und einer Funktion r : S → IN .
Die Menge S wird auch die Menge der Operatoren genannt. Zu einem Operator
a ∈ S gibt die Zahl r(a) dessen Rang (Stelligkeit) an. Wir bezeichnen Alphabete
mit Rang auch kurz mit dem Begriff Alphabet.
1
Durch Aliasanalysen kann der Speicherzustand tatsächlich noch differenziert werden, damit die
Reihenfolge voneinander unabhängiger Speicherzugriffe nicht zu früh festgelegt wird.
22
3. Grundlagen
Wenn man hierzu noch eine Menge V von Symbolen (Variablen), die zu S disjunkt
ist, wählt, kann man Terme definieren:
Definition 3.2 (Terme) Für ein Alphabet Σ und eine Menge von Variablen V ist
die Menge der Terme TΣ (V ) wie folgt induktiv definiert:
1. Jede Variable v ∈ V ist ein Term, d.h. V ⊂ TΣ (V ).
2. Jeder Operator a ∈ S mit r(a) = 0 ist ein Term, d.h. {a|a ∈ S, r(a) = 0} ⊂
TΣ (V ).
3. Für jeden Operator a ∈ S mit r(a) > 0 und Terme t1 , . . . , tr(a) ∈ TΣ (V ) ist
auch a(t1 , . . . , tr(a) ) ∈ TΣ (V ).
4. Außer den so definierten Termen hat TΣ (V ) keine weiteren Elemente.
Die Menge der in einem Term t tatsächlich auftretenden Variablen wird mit V ar(t)
bezeichnet.
Definition 3.3 (Grundterm) Ein Term ohne Variablen, also mit V ar(t) = ∅,
heißt Grundterm.
Im weiteren betrachten wir nur Grundterme, d.h. V = ∅, und kürzen ab: TΣ := TΣ (∅).
Definition 3.4 (Positionen) Eine Position in einem Term ist eine möglicherweise leere Folge von positiven ganzen Zahlen. Die leere Folge schreiben wir , die Konkatenation bezeichnen wir mit ·. Damit ist die Menge der Positionen eines Terms
P os(t) wie folgt definiert:
1. Für t = a mit r(a) = 0: P os(t) := {}
2. Für t = a(t1 , . . . , tr(a) ): P os(t) := {} ∪ 1 · P os(t1 ) ∪ . . . ∪ r(a) · P os(tr(a) ).
Definition 3.5 (Teilterme) Zu einem Term t ist der Teilterm an der Position p,
t|p , wie folgt definiert:
1. t| := t
2. Für t = a(t1 , . . . , tr(a) ) ist t|k·p0 := tk |p0
Damit kann man kostenbehaftete Grundtermersetzungssysteme wie folgt definieren.
Definition 3.6 (Kostenbehaftetes Termersetzungssystem) Ein kostenbehaftetes Grundtermersetzungssystem, kurz mit kostenbehaftetes Termersetzungssys”
tem“ bezeichnet, ist ein Tripel (Σ, R, C) mit
1. einem nicht leeren Alphabet Σ,
3.3. Heuristische Suche
23
2. einer Menge von Regeln R ⊂ TΣ × TΣ , so dass für alle Regeln (t, t0 ) ∈ R gilt
t 6= t0 , und
3. einer Kostenfunktion C : R → IR≥0 .
Eine Regel r = (t, t0 ) ∈ R kann auf einen Term t0 angewendet werden (Ersetzung),
indem ein Teilterm von t0 , der gleich zu t ist, durch t0 ersetzt wird. Der Rest von t0
bleibt hierbei unverändert. Wenn der Teilterm an Position p ersetzt wird, kann dies
so geschrieben werden: hr, pit0 = t1 .
Durch wiederholte Regelanwendungen können ganze Ersetzungssequenzen definiert
werden. Die Kosten werden als die Summe der Kosten der einzelnen Regeln definiert.
3.2.2
Übertragung auf BURS
BURS wird durch eine Einschränkung dieser Grundtermersetzungssysteme definiert:
Nach der Ersetzung eines bestimmten Teilterms dürfen die darin enthaltenen Teilterme nicht weiter bearbeitet werden, die Ersetzung erfolgt also von unten“ nach
”
oben“. Anders ausgedrückt: Wenn eine Regelanwendung hr1 , p1 i vor einer Regelan”
wendung hr2 , p2 i kommt, ist entweder p1 = p2 oder p1 ist kein Präfix von p2 . Im
ersten Fall müssen auch alle dazwischen liegenden Regelanwendungen an derselben
Position stattfinden. Desweiteren muss eine obere Schranke k existieren, so dass für
jeden Eingabeterm an jeder Position höchstens k Ersetzungen möglich sind.
Die Codeerzeugung erfolgt, indem an die Termersetzungsregeln zusätzlich Befehlsmuster gekoppelt werden, die bei der Anwendung einer Regel in das Maschinenprogramm ausgegeben werden.
Die Erweiterung auf gerichtete azyklische Graphen (DAGs) ist einfach ([Boes98],
Seiten 42 f.). Zyklen werden bei der Berechnung der möglichen Ersetzungssequenzen
an bestimmten Stellen (Block-Knoten für Kontrollflusszyklen, Phi-Knoten für Datenflusszyklen) aufgebrochen, was in [Boes98], Seiten 43 ff. beschrieben wird. Damit
wird die Codeerzeugung direkt aus SSA-Graphen ermöglicht.
3.3
Heuristische Suche
Während Pelegrı́-Llopart [Pele88] aus BURS-Spezifikationen Automaten vorberechnet, die letztlich die tatsächlich verwendeten Ersetzungen berechnen, haben Nymeyer
und Katoen [NyKa97, NKWA96] stattdessen einen Ansatz verfolgt, der zunächst alle
möglichen Ersetzungssequenzen berechnet und dann mit Hilfe heuristischer Suche
die günstigste auswählt. Auch Boesler [Boes98] hat diese Methode gewählt und dabei gegenüber der Arbeit von Nymeyer und Katoen noch einige Verbesserungen
vorgenommen.
3.3.1
Suchproblem
Viele Probleme lassen sich als Suchprobleme darstellen. Ein Suchproblem besteht
darin, in einem sogenannten Suchgraphen einen möglichst guten“ Pfad von einem
”
definierten Initialknoten zu einem Zielknoten zu finden. Dies stellt sich formal so
dar:
24
3. Grundlagen
Definition 3.7 (Suchgraph) Ein Suchgraph ist ein Tupel
(N, E, n0 , Ng , C)
mit folgenden Eigenschaften:
1. (N, E) ist ein verbundener gerichteter azyklischer Graph.
2. Der Initialknoten n0 ∈ N ist die Wurzel des Graphen, d.h. für jedes n ∈ N
existiert ein gerichteter Pfad von n0 nach n, aber es existiert kein Pfad von
einem n ∈ N \ {n0 } nach n0 .
3. Die Menge der Zielknoten Ng ⊂ N ist nicht leer.
4. Es gibt keine Kanten, die von Zielknoten ausgehen:
∀ n ∈ Ng : ¬ ∃ m ∈ N : (n, m) ∈ E.
5. Die Kostenfunktion C : E → IR≥0 bildet jede Kante auf die ihr zugeordneten
Kosten ab. Die Kostenfunktion wird additiv auf Pfade erweitert.
6. Es gibt keine unendlich langen Pfade mit endlichen Kosten.2
In der Anwendung auf BURS repräsentiert ein Knoten im Suchgraphen (Suchknoten)
einen Zustand des Graphen, auf den BURS angewendet wird. Der Initialknoten
n0 entspricht also dem Eingabegraphen. Zielknoten entsprechen den gewünschten
Ergebnissen der Graphersetzung, also üblicherweise dem Zustand, wenn der Graph
auf einen einzelnen Knoten reduziert ist. Eine Kante im Suchgraphen entspricht der
Anwendung einer Ersetzungsregel und damit der Ausgabe eines Maschinenbefehls.
Definition 3.8 (Suchproblem) Das Problem, in einem Suchgraphen einen Knoten ng ∈ Ng und einen Pfad von n0 nach ng nach finden, so dass die Kosten dieses
Pfades minimal sind, nennt man Suchproblem.
Da dieses Problem oft schwierig ist, können auch Approximationen vorgenommen
werden. Dann wird ein Knoten ng ∈ Ng und ein Pfad von n0 nach ng gesucht, jedoch
wird gegebenenfalls hingenommen, dass die Kosten nicht minimal sind.
3.3.2
Grundalgorithmus
Die meisten Suchalgorithmen haben folgende grundlegende Struktur:
Algorithmus 3.1 (Suchalgorithmus)
Eingabe:
Suchgraph wie oben definiert
Ausgabe:
Gefundener Zielknoten ng ∈ Ng ,
ein Pfad von n0 nach ng .
Lokale Variablen: Menge offener Knoten NO ⊂ N ,
Menge geschlossener Knoten NC ⊂ N ,
partielle Abbildung prev : N → N
2
Diese Beschränkung schließt Suchgraphen aus, in denen die Kosten auf einem Pfad z.B. die
Folge ( 12 , 14 , 18 , . . .) darstellen und so trotz Vorhandenseins zum Beispiel einer Lösung mit Kosten 2
in endlicher Tiefe auch eine A∗ -Suche gegebenenfalls nie terminiert.
3.3. Heuristische Suche
25
1. Der Initialknoten wird in die Menge offener Knoten aufgenommen (NO :=
{n0 }). Die Menge geschlossener Knoten ist anfangs leer (NC := ∅) und die
Funktion prev nirgends definiert.
2. In einer Schleife werden folgende Schritte durchlaufen:
(a) Wähle einen Knoten n aus NO aus. Wenn die Menge offener Knoten leer
ist, ist die Suche fehlgeschlagen.
(b) Wenn n ∈ Ng ist, wird diese Lösung (Zielknoten und der gefundene Pfad
vom Initialknoten zum Zielknoten, letzterer mit Hilfe von prev) ausgegeben. Falls mehrere Lösungen gesucht werden, kann der Algorithmus dann
fortgesetzt werden, sonst terminiert er hier.
(c) Verschiebe n aus NO nach NC .
(d) Für alle Nachfolger m von n:
• Falls m ∈ NC : Wenn der neu gefundene Pfad von n0 nach m günstiger als der durch prev spezifizierte ist, dann setze prev(m) := n und
verschiebe m aus NC wieder nach NO ; sonst tue nichts.
• Falls m ∈ NO : Wenn der neu gefundene Pfad von n0 nach m günstiger als der durch prev spezifizierte ist, dann setze prev(m) := n;
sonst tue nichts.
• Sonst: Setze prev(m) := n und füge m in NO ein.
Falls n ∈ Ng , bewirkt dieser Schritt nichts, da solche n keine Nachfolger
haben (Eigenschaft 4 in Definition 3.7).
Es wird angenommen, dass ein Knoten nur endlich viele Nachfolger hat. Damit ist
die Menge der Knoten in dem Suchgraphen aufzählbar.
Vergleiche auch die Darstellung in [Gins93], Seite 22.
In der Praxis wird meist dynamisch nur der Teil des Suchgraphen berechnet, der
während der Suche auch betrachtet wird: Der Initialknoten wird in Schritt 1 berechnet und in Schritt 2d werden zu dem gerade betrachteten Knoten die Nachfolger
und die Kanten zu diesen Nachfolgern bestimmt und dem Graphen hinzugefügt.
Die einzelnen Suchverfahren unterscheiden sich hauptsächlich durch die Wahl des
nächsten zu bearbeitenden Knotens in Schritt 2a und dadurch, ob mehrere Lösungen
bestimmt werden (Schritt 2b).
Bei uninformierten Suchverfahren wird diese Wahl nur durch die Position des Knotens im Suchgraphen und/oder die Reihenfolge des Einfügens in die Menge offener
Knoten bestimmt. Zum Beispiel ist eine Breitensuche so aufzufassen, dass die Menge
offener Knoten als Schlange organisiert ist. Bei einer Tiefensuche dagegen wird sie
als Keller verwendet.
Bei informierten Suchverfahren dagegen fließt Wissen aus dem Anwendungsgebiet
in die Auswahl in Schritt 2a ein. Dieses Wissen wird oft als Heuristik bezeichnet,
die Suchverfahren als heuristische Suche.
26
3. Grundlagen
3.3.3
Heuristische Suchverfahren
Für einen Knoten n ∈ N werden folgende Bezeichnungen eingeführt:
• g(n) sind die Kosten eines günstigsten Pfades von n0 nach n.
• h(n) sind die Kosten eines minimalen Pfades von n zu einem günstigsten von
n aus erreichbaren Zielknoten ng ∈ Ng .
• h∗ (n) ist eine heuristische Abschätzung von h(n).
• f (n) := g(n) + h(n) und f ∗ (n) := g(n) + h∗ (n).
Ein einfaches Beispiel heuristischer Suche wird von h∗ (n) gesteuert: In der Best”
first“-Suche wird als Nächstes ein n ∈ NO mit minimalem h∗ (n) bearbeitet ([Gins93],
Seite 71).3
Algorithmen des Typs A ([Farr88], Seiten 22 ff.) steuern die Suche dagegen, indem
bei der Auswahl eines Knotens n aus NO die Summe von g(n) und h∗ (n), also f ∗ (n),
minimiert wird.
Falls die Heuristik die tatsächlichen Kosten nie überschätzt, also für alle n ∈ N gilt:
h∗ (n) ≤ h(n), spricht man von einem Algorithmus des Typs A∗ ([Farr88], Seite 25).
Die Heuristik h∗ (n) nennt man in diesem Falle zulässig oder auch optimistisch.4
Satz 3.1 Wenn ein Zielzustand ng ∈ Ng existiert, für den es einen Pfad endlicher
Länge von n0 nach ng gibt, terminiert ein Suchalgorithmus vom Typ A∗ und findet
eine Lösung mit minimalen Kosten.
Zum Beweis verweisen wir auf die Literatur, z.B. [Gins93], Seiten 78 f.
Mit h∗ (n) = 0 für alle n wird die Suche zu einer Art von Breitensuche.5 Wenn dagegen h∗ (n) = h(n) für alle n und bei gleichen f ∗ (n) tiefer im Graphen gelegene n
bevorzugt werden, findet der A∗ -Algorithmus direkt den Weg zu einer optimalen Lösung. Gibt es dagegen n ∈ N mit h∗ (n) > h(n), ist es möglich, dass der Algorithmus
einen Knoten ng ∈ Ng mit nicht minimalem Pfad findet.
Es gibt weiterhin die Varianten Ae∗ , wo nur h∗ (n) ≤ h(n) + e (e > 0) gefordert wird
und Aα∗ , wo nur h∗ (n) ≤ h(n) · (1 + α) (α > 0) gefordert wird. Ein Ae∗ -Algorithmus
findet eine Lösung, deren Kosten das Optimum um höchstens e übersteigen. Bei
einem Aα∗ -Algorithmus liegen die Kosten höchstens um einen Faktor von (1 + α)
über dem Optimum.
In den Arbeiten von Nymeyer und Katoen [NyKa97] sowie von Boesler [Boes98]
wird ein A∗ -Algorithmus eingesetzt und damit, in den Grenzen der jeweils zugrunde
liegenden Modellierung, eine optimale Lösung gefunden. Die Heuristikfunktion h∗ (n)
3
Zu beachten ist, dass der hier verwendete Begriff von Best-first“-Suche, der von [Gins93]
”
abgeleitet ist, etwas anderes bedeutet als der Begriff Best-first“-Suche in [NyKa97] oder [Boes98].
”
4
Heuristiken, die die Kosten nie unterschätzen, also pessimistisch sind, verwenden wir in dieser
Arbeit nicht.
5
Dieser Fall ist der, der in [NyKa97, Boes98] als Best-first“-Suche bezeichnet wird. Die Suche
”
wird allein durch g(n) gesteuert.
3.4. Befehlsanordnung
27
wird aus den Kosten (bzw. bei variablen Kosten aus Angaben der minimalen Kosten)
der einzelnen Befehle berechnet. Das Verfahren hierfür ist in [Boes98] auf Seite 49
angegeben.
Dagegen verfolgen wir in dieser Arbeit auch eine alternative Suchstrategie, die es
ermöglicht, Approximationen zu berechnen. Dafür ist dieses Suchverfahren oft wesentlich schneller. So können wir einen Kompromiss zwischen der Geschwindigkeit
des Codegenerators und der Qualität des erzeugten Codes wählen.
Als Grundlage hierfür haben wir das Verfahren ein Weg, dann vom Besten aus“
”
(One-Then-Best-Backtracking, siehe [Bund97], Nr. 186) herangezogen. Hierbei wird
vom Startzustand aus in einem gierigen Verfahren in die Tiefe gegangen, bis entweder
eine Lösung oder eine Sackgasse (d.h. ein Knoten, der keine Nachfolger hat, aber auch
kein Zielknoten ist) gefunden wird. Dann wird irgendeiner der dabei auch erzeugten
Knoten (d.h. Geschwister der bereits besuchten Knoten) ausgewählt. Im Gegensatz
zu einfacheren Verfahren mit Zurücksetzen (Backtracking) wird diese Auswahl aber
heuristisch gesteuert. Die Details arbeiten wir in Abschnitt 4.4 aus.
Es ist zu beachten, dass dieses Suchverfahren bei unendlich großen Suchgraphen
möglicherweise auch dann nicht terminiert, wenn eine Lösung in endlicher Tiefe existiert. Dies ist für uns jedoch wegen der BURS-Eigenschaft, dass an jeder Position
nur beschränkt viele Ersetzungsschritte möglich sind, nicht der Fall: die Suchgraphen sind zwar oft sehr groß aber endlich. In endlichen Suchgraphen findet jede
Instantiierung des Grundalgorithmus 3.1 natürlich eine Lösung.
3.4
Befehlsanordnung
Für die Erzeugung guten Maschinencodes ist es nötig, die Befehle gut anzuordnen.
Zum Beispiel kann so dafür gesorgt werden, dass Prozessorpipelines gut gefüllt bleiben und dabei Strukturkonflikte, wie z.B. der zu frühzeitige Zugriff auf einen Wert,
der aus dem Speicher gelesen wird, vermieden werden. Auch sollen mehrfach vorhandene Funktionseinheiten möglichst gut ausgenutzt werden.
Daher koppeln wir in dieser Arbeit die Befehlsauswahl, die bereits Boesler [Boes98]
für SSA-Graphen realisiert hat, mit der Befehlsanordnung. Außerdem zeigen wir, wie
mit Hilfe dieser Phasenkopplung auch SIMD-Parallelität identifiziert werden kann.
Boesler hat in seiner Diplomarbeit bereits näher beschrieben, wie diese Kopplung
stattfinden kann ([Boes98], Seiten 28 f.), dies jedoch nicht implementiert. Die von
Boesler vorgeschlagene Klassenbildung von Befehlen ([Boes98], Seite 48) verfolgen
wir hier nicht weiter, da eine solche Klasseneinteilung gerade in Hinblick auf die
Behandlung von Lade- und sonstigen Ergebnislatenzen, aber auch bei der Abhängigkeitsprüfung für die Identifikation von SIMD-Befehlen nicht haltbar ist. Es kann
während der Suche (Abschnitt 3.3) bei den Knoten im Suchgraphen zusätzlich zum
Zustand des Programmgraphen noch ein Maschinenzustand gehalten werden, der den
Zustand der Pipeline bzw. Funktionseinheiten sowie Informationen zur Identifikation von Strukturkonflikten darstellt. Dadurch kann die Kostenfunktion so bemessen
werden, dass sie Verzögerungen und Gewinne durch die Befehlsanordnung aufzeigt.
Zum Beispiel entstehen höhere Kosten durch Strukturkonflikte, sei es, dass der Prozessor die Ausführung verzögert oder dass NOP-Befehle eingefügt werden müssen,
um überhaupt eine korrekte Programmausführung zu erreichen. Gewinne entstehen,
28
3. Grundlagen
indem Parallelität auf Befehlsebene ausgenutzt wird, wozu auch SIMD-Befehle zählen.
Diese Kostenfunktion wird durch eine Maschinensimulation berechnet, siehe z.B.
[Müll95]. Es gibt eine Menge von Maschinenzuständen Q und eine Menge von Maschinenbefehlen I. Die Simulation besteht aus einem Paar von Funktionen cost :
Q × I → IR≥0 und δ : Q × I → Q.6 Die Funktion cost bestimmt, welche Kosten es
verursacht, in dem gegebenen Maschinenzustand den angegebenen Befehl abzusetzen. Der sich dadurch ergebende neue Maschinenzustand wird von δ berechnet. Dies
fassen wir später in Definition 4.3 noch einmal formal.
3.5
Erkennung von SIMD-Befehlen
In [GlGB02] stellen Glesner, Geiß und Boesler einen Algorithmus zur automatischen
Erzeugung von SIMD-Befehlen vor. Sie verwenden dabei Beschränkungen erzeugende Graphersetzungssysteme ([GlGB02], Abschnitt 3). Den Ersetzungsregeln werden
zusätzlich zu dem Code, der erzeugt wird, Beschränkungen (Variablen und Prädikate) zugeordnet, wobei für jede Regelanwendung durch Umbenennung frische Variablen erzeugt werden. Es werden Teilbefehle von SIMD-Befehlen (z.B. die einzelne
Addition einer Vierfach-Addition) erkannt. Diese werden durch eine Kostenfunktion bevorzugt, die solche Teilbefehle besonders günstig bewertet, in der Erwartung,
dass diese zu effizienten SIMD-Befehlen kombiniert werden können. Zugleich werden Beschränkungen ausgegeben, die sicher stellen, dass diese Teilbefehle entweder
gleichzeitig als Teil desselben SIMD-Befehls oder sequentiell ausgeführt werden und
dass die Maximalzahl der Teilbefehle eines SIMD-Befehls nicht überschritten wird.
In einer Nachbearbeitung, die mit der Befehlsanordnung zusammen ausgeführt wird,
werden die Teilbefehle unter Beachtung dieser Beschränkungen zu SIMD-Befehlen
kombiniert. Es wird angedeutet, dass man die Zuteilung der entsprechenden Teile
der SIMD-Register bzw. Vektorregister mit einer ähnlichen Methode lösen kann: Es
werden weitere Variablen und Beschränkungen erzeugt, die bei der Registerzuteilung
ausgewertet werden. In [GlGB02], Abschnitt 5, stellen die Autoren weiterhin einen
Checking-Algorithmus zur Verifikation dieser Übersetzung vor.
Im Weiteren zeigen wir, wie wir die in diesem Kapitel erläuterten Grundlagen verwenden und anpassen, um mit Hilfe von zustandsbehafteten Grundtermersetzungssystemen eine Maschinensimulation in die Codeerzeugung einzubinden.
6
Die verwendeten Funktionssymbole sind in [Müll95] andere. Wir verwenden das Symbol δ, das
für Zustandsübergangsfunktionen üblich ist, und den anschaulichen Namen cost.
4. Effiziente Phasenkopplung mit
zustandsbehafteter
Termersetzung
In diesem Kapitel erarbeiten wir die theoretischen Grundlagen für die Phasenkopplung zwischen Befehlsauswahl und -anordnung. In Abschnitt 4.1 erweitern wir die
kostenbehafteten Grundtermersetzungssysteme zu zustandsbehafteten Grundtermersetzungssystemen. Wir entwickeln dann ein Verfahren, wie wir mit diesem Instrument eine Maschinensimulation anbinden und damit die gewünschte Kopplung
erreichen, siehe Abschnitt 4.2.
Außerdem entwickeln wir zwei Verfahren, um die Suche effizienter zu gestalten: Mit
Hilfe einer Äquivalenzrelation auf der Menge der Suchknoten verringern wir die Größe des Suchraums, vgl. Abschnitt 4.3. Desweiteren entwickeln wir für den Fall, dass
die garantiert optimale A∗ -Suche zu langsam ist, in Abschnitt 4.4 ein alternatives
Suchverfahren, das einen Kompromiss zwischen der Laufzeit des Codegenerators und
der Qualität des erzeugten Maschinencodes erlaubt.
4.1
Zustandsbehaftete Termersetzung
Das verwendete BURS-Verfahren (siehe Abschnitt 3.2) basiert theoretisch auf kostenbehafteten Grundtermersetzungssystemen. Diese wurden zunächst auf die Ersetzung von unten nach oben eingeschränkt, was den Begriff des BURS ausmacht.
Anschließend wurde das BURS-Verfahren in zwei Schritten (DAGs, dann auch Behandlung von Zyklen) auf die Verarbeitung von Graphen erweitert.
In dieser Arbeit verwenden wir als theoretische Grundlage eine Erweiterung
der Termersetzungssysteme, nämlich zustandsbehaftete Grundtermersetzungssysteme mit Kosten (kurz im Folgenden zustandsbehaftete Termersetzungssysteme“ ge”
nannt). Der Übergang zu BURS und dann zu Graphen gestaltet sich analog zu dem
bisherigen Codeerzeugungsverfahren nach [Boes98]. Daher stellen wir diesen nicht
erneut dar; denn unsere Änderung betrifft nur die Kostenberechnung.
30
4. Effiziente Phasenkopplung mit zustandsbehafteter Termersetzung
Die Kostenberechnung in einem zustandsbehafteten Termersetzungssystem erfolgt
nicht durch eine einfache Zuordnung von Regel zu Kostenmaß. Stattdessen verwenden wir einen Mealy-Automaten.
Definition 4.1 (Mealy-Automat) Ein Mealy-Automat ist ein Tupel
A = (Q, I, O, δ, λ, q0 )
mit
1. endlichen Mengen Q (Zustandsmenge), I (Eingabealphabet) und O (Ausgabealphabet),
2. einer Zustandsübergangsfunktion δ : Q × I → Q, die zu einem vorherigen
Zustand und einer Eingabe den Folgezustand berechnet,
3. einer Ausgabefunktion λ : Q × I → O, die zu einem vorherigen Zustand und
einer Eingabe die Ausgabe berechnet, und
4. einem Startzustand q0 ∈ Q.
Diesen verwenden wir zur Kostenberechnung, indem das Eingabealphabet die Menge
der Regeln und das Ausgabealphabet die Kostenangaben darstellt.
Damit definieren wir die zustandsbehafteten Grundtermersetzungssysteme:
Definition 4.2 (Zustandsbehaftetes Termersetzungssystem) Ein zustandsbehaftetes Grundtermersetzungssystem mit Kosten, im weiteren kurz zustandsbe”
haftetes Termersetzungssystem“ genannt, ist ein Tripel
(Σ, R, A = (Q, R, IR≥0 , δ, cost, q0 )) .
1. Hier ist Σ ein nicht leeres Alphabet,
2. R ⊂ TΣ × TΣ eine Menge von Regeln, so dass für alle Regeln (t, t0 ) ∈ R gilt
t 6= t0 , und
3. A ein Mealy-Automat, für den das Eingabealphabet die Menge der Regeln des
Termersetzungssystems und das Ausgabealphabet die Menge IR≥0 ist.
Das Symbol cost entspricht hierbei der Ausgabefunktion λ des Mealy-Automaten.
Eine Regel r = (t, t0 ) ∈ R kann auf ein Paar (t0 , q0 ) ∈ TΣ × Q eines Terms und eines
Zustandes des Mealy-Automaten angewendet werden, indem ein Teilterm von t0 , der
gleich zu t ist, durch t0 ersetzt wird. Der Rest von t0 bleibt hierbei unverändert und
das Ergebnis heiße t1 . Das Ergebnis ist das Paar (t1 , q1 := δ(q0 , r)). Wir schreiben
dies auch so: hr, pi(t0 , q0 ) = (t1 , q1 ). Die Kosten dieser Regelanwendung werden mit
cost(q0 , r) berechnet.
Durch wiederholte Regelanwendungen definieren wir Ersetzungssequenzen. Die Kosten sind hierbei die Summe der Kosten der einzelnen Regeln. Bei einer Sequenz
4.2. Maschinensimulation
31
hr1 , p1 i(t0 , q0 ) = (t1 , q1 ), hr2 , p2 i(t1 , q1 ) = (t2 , q2 ) sind die Kosten also zum Beispiel
cost(q0 , r1 ) + cost(q1 , r2 ).
Wir beschränken uns hier auf Grundtermersetzungssysteme, weil diese unseren Anforderungen zur Codeerzeugung genügen. Die Kostensteuerung durch einen MealyAutomaten kann man jedoch analog auch für allgemeine Termersetzungssysteme
durchführen.
4.2
Maschinensimulation
Nachdem wir mit dem Formalismus der zustandsbehafteten Termersetzung ausgestattet sind, können wir den Begriff der Maschinensimulation definieren und die
Anbindung derselben an die Termersetzung ausführen.
4.2.1
Definition
Wir definieren im Folgenden die Maschinensimulation. Diese Definition basiert auf
[Müll95], wo auch genau das Funktionenpaar verwendet wird, das hier den Funktionen δS und costS entspricht.
Definition 4.3 (Maschinensimulation) Eine Maschinensimulation über einem
Befehlssatz I ist ein Quadrupel
S(I) = (QS , qS0 , δS , costS )
mit
1. einer Menge von Maschinenzuständen QS ,
2. einem Initialzustand qS0 ∈ QS ,
3. einer Zustandsübergangsfunktion δS : QS × I → QS , die zu einem gegebenen
Maschinenzustand und einem Befehl den Folgezustand berechnet, und
4. einer Kostenfunktion costS : QS × I → IR≥0 , die zu einem gegebenen Maschinenzustand und einem Befehl i ∈ I die Kosten dieses Befehls berechnet.
4.2.2
Phasengekoppelte Codeerzeugung
Bei der Anwendung von Termersetzungssystemen auf die Codeerzeugung definieren
wir zusätzlich eine Befehlsausgabefunktion:
Definition 4.4 (Befehlsausgabefunktion) Eine Befehlsausgabefunktion aus einem Termersetzungssystem mit der Menge von Regeln R in einen Befehlssatz I ist
eine Funktion
γ : R → I∗ .
Dabei wird mit I ∗ die Menge von Befehlsfolgen (endliche Folgen von Elementen aus
I), die auch die leere Folge enthält, bezeichnet.
32
4. Effiziente Phasenkopplung mit zustandsbehafteter Termersetzung
Hiermit definieren wir den Mealy-Automaten A = (Q, R, IR≥0 , δ, cost, q0 ) wie folgt:
1. Q := QS : Die Zustandsmenge wird von der Maschinensimulation übernommen.
2. δ(q, r) := q, falls die Regel keinen Befehl ausgibt, also γ(r) = .
3. δ(q, r) := δS (. . . δS (q, i1 ) . . . , in ), falls γ(r) = i1 · . . . · in .
4. cost(q, r) := 0, falls die Regel keinen Befehl ausgibt, also γ(r) = .
5. cost(q, r) := costS (q, i1 ) + · · · + costS (δS (. . . δS (q, i1 ) . . . , in−1 ), in ), falls γ(r) =
i1 · . . . · in−1 · in .
6. Der Startzustand wird übernommen: q0 := qS0 .
So können wir Code aus Termen generieren, wobei eine Maschinensimulation die
Kostenbewertung der Termersetzung steuert.
Diese Theorie übertragen wir weiterhin analog wie in Abschnitt 3.2 bzw. [Boes98],
Abschnitte 3.1.2 und 4.3, auf BURS und Graphen. Dies nennen wir zustandsbehaftete
BURS-Codegenerierung.
Wir können dieses zustandsbehaftete BURS-Verfahren nun einsetzen, um bei der
Codeerzeugung simultan die Befehlsauswahl und -anordnung zu betrachten, also
dieses Phasenkopplungsproblem zu lösen. Dadurch, dass die Kostenberechnung in
der Termersetzung bzw. BURS-basierten Graphersetzung jetzt die Anordnung der
Befehle berücksichtigen kann, und durch die Eigenschaft der A∗ -Suche ergibt sich,
dass mit unserem Verfahren die Ausgabe des Codegenerators optimal hinsichtlich
der Befehlsauswahl und -anordnung ist. Die Heuristik für die A∗ -Suche bestimmen
wir hierbei, indem wir den Ersetzungsregeln weiterhin ein Kostenmaß zuordnen,
das jedoch nur eine untere Schranke für die tatsächlichen, je nach Maschinenzustand verschiedenen, Kosten darstellt. Die Heuristikfunktion wird hieraus weiterhin
entsprechend [Boes98], Seite 49, berechnet. Unter diesen Voraussetzungen ist diese
Heuristikfunktion zulässig im Sinne der A∗ -Suche (Abschnitt 3.3).
4.2.3
Identifikation von SIMD-Befehlen
Wir nutzen unser Verfahren der Phasenkopplung auch dazu, SIMD-Befehle automatisch auszuwählen. Ein Beispiel ist die parallele Ausführung zweier Additionen,
wenn die Operationen voneinander unabhängig sind, siehe Abbildung 4.1. Einige der
speziellen DSP- oder Media-Befehle entsprechen einem derartigen Muster.
...
...
Add
Add
...
...
Additionen
unabhängig!
Abbildung 4.1: Zwei unabhängige Additionen
Da unsere Termersetzung nur zusammenhängende Muster, die als DAG mit Wurzel
vorliegen, erkennen kann, müssen wir hierfür folgende Methode einsetzen:
4.3. Äquivalenz von Suchknoten
33
Wir identifizieren die einzelnen Additionen als Teilbefehle. Diesen weisen wir in der
Maschinensimulation günstigere Kosten zu (im Falle der zweifachen Addition also
jeweils die Hälfte der Kosten des Zweifach-Additions-Befehls), falls die Kombination zu dem tatsächlichen SIMD-Befehl möglich ist. Wenn dies nicht möglich ist,
berechnen wir die Kosten so, dass die Additionen einzeln ausgeführt werden. Indem
wir bei der Ausgabe der gefundenen Befehlsfolge die Maschinensimulation noch einmal mitführen, können wir während der Ausgabe dann eine Verschmelzung durch
eine einfache Guckloch-Optimierung durchführen, die unter der Bedingung, dass die
Teilbefehle kombinierbar sind, aufeinanderfolgende Additions-Teilbefehle zu einem
Zweifach-Additions-Befehl zusammenfügt. Falls die Befehle nämlich kombinierbar
sind, werden sie direkt aufeinander folgend ausgewählt, da sonst die Kosten ja höher
sind.
Letztlich erzeugen wir also phasengekoppelt Code für eine fiktive Architektur, die
die SIMD-Teilbefehle anstatt der vollständigen SIMD-Befehle kennt. In einer anschließenden Phase übersetzen wir den Code von dieser fiktiven Architektur auf die
tatsächliche Zielarchitektur. Unser Verfahren stellt sicher, dass die Kostenbewertung
mit der endgültigen Architektur konform ist und dass diese letzte Übersetzungsphase
sehr einfach realisiert werden kann.
Dieses Verfahren ist von der Grundidee her ähnlich zu dem in [GlGB02] ausgearbeiteten Algorithmus, siehe auch Abschnitt 3.5. Dort wird auch die Auswahl von
DSP-Teilbefehlen durch eine entsprechende Kostenfunktion bevorzugt. Jedoch werden diese dort zusammen mit Beschränkungen ausgegeben, die von einer Nachbearbeitung ausgewertet werden. Die Kostenbewertung findet also nicht phasengekoppelt mit der Nachbearbeitung statt. Dagegen geben wir in dem hier entwickelten Verfahren keine Beschränkungen aus, sondern werten die Bedingungen bereits
in der Maschinensimulation selbst aus, so dass die Nachbearbeitung sehr einfach
wird (Guckloch-Optimierung). Da die Kostenberechnung in der Maschinensimulation bereits berücksichtigt, welche DSP-Teilbefehle in der nachfolgenden GucklochOptimierung kombiniert werden, wahren wir so die Optimalität. Dies ist in dem
Ansatz von Glesner, Geiß und Boesler schwieriger, denn bei einem einfachen Ansatz der Kostenfunktion werden dort DSP-Teilbefehle auch dann bevorzugt, wenn
sie nachher nicht kombiniert werden können. Dies wird nämlich erst in der Nachbearbeitung bei der Auswertung der Beschränkungen erkannt, und so können andere
günstige Befehle eventuell übersehen werden.
4.3
Äquivalenz von Suchknoten
Die Suchimplementierung von [Boes98] erzeugt in Schritt 2d der Suche nach Algorithmus 3.1 immer neue Knoten als Nachfolger. Also tritt immer der Fall, der dort
mit sonst“ bezeichnet ist, ein. Diese Einteilung ist jedoch unnötig fein, da in Wirk”
lichkeit ja die Knoten im Suchgraphen nur den Zustand der Graphersetzung, also
die aktuelle Form des Graphen und eventuell den Maschinenzustand, repräsentieren
müssen. Daher definieren wir hier eine Äquivalenzrelation für die bisher implementierten Suchknoten.
Definition 4.5 (Äquivalenz von Suchknoten) Zwei Suchknoten sind äquivalent, wenn sie
34
4. Effiziente Phasenkopplung mit zustandsbehafteter Termersetzung
1. denselben Zustand der BURS-Graphersetzung repräsentieren und
2. denselben Maschinenzustand bezeichnen.
Die neue Menge der Knoten im Suchgraphen ergibt sich als Menge der Äquivalenzklassen der bisherigen Suchknoten, also als Quotientenmenge nach der oben definierten Äquivalenzrelation. Damit wird der Suchgraph kleiner und die Suche effizienter.1
Da die Prüfung der so definierten Äquivalenz sehr aufwendig ist, definieren wir noch
eine verfeinerte Äquivalenzrelation und zeigen, dass diese Verfeinerung korrekt ist.
Definition 4.6 (Verfeinerte Äquivalenz von Suchknoten) Zwei Suchknoten
sind verfeinert äquivalent, wenn sie
1. beide einen Zustand des Programmgraphen repräsentieren, wo keine Programmgraphknoten nur teilweise bearbeitet sind,
2. an den bereits bearbeiteten Programmgraphknoten dasselbe Ergebnis der Graphersetzung erreicht ist und
3. beide denselben Maschinenzustand bezeichnen.
Wenn zwei Knoten n1 und n2 verfeinert äquivalent sind, ist nach Bedingung 3 der
Maschinenzustand derselbe. Dies erfüllt auch Bedingung 2 der Äquivalenz nach Definition 4.5. Nach Bedingung 2 der verfeinerten Äquivalenz ist das Ergebnis der
Ersetzung an den bereits bearbeiteten Programmgraphknoten dasselbe. Damit ist
also auch der gesamte, teilweise bearbeitete Programmgraph derselbe. Also folgt
Bedingung 1 der Äquivalenz und hiermit gilt der folgende Satz:
Satz 4.1 (Korrekte Verfeinerung) Die verfeinerte Äquivalenz nach Definition
4.6 verfeinert die Äquivalenz nach Definition 4.5. Es gilt also: Sind zwei Knoten
n1 und n2 verfeinert äquivalent, so sind sie auch äquivalent.
4.4
Suboptimales Suchverfahren
Bei vielen Messbeispielen (siehe Messungen in Abschnitt 6.2) werden die Laufzeiten
der Codegenerierung auch mit den sonstigen von uns vorgenommenen Verbesserungen zu lange. Daher haben wir auch ein Suchverfahren entwickelt, das die garantierte
Optimalität der Lösungen aufgibt, aber dafür einen Kompromiss zwischen der Qualität des erzeugten Codes und der Laufzeit des Generators erlaubt.
Als Grundlage dient weiterhin der in Algorithmus 3.1 dargestellte Grundalgorithmus.
Jedoch modifizieren wir diesen etwas, nämlich in den Schritten 2a (Auswahl eines
Knotens aus der Menge offener Knoten) und 2b (Lösung gefunden und Prüfung,
ob die Suche beendet wird). Die Grundidee stammt von dem Verfahren ein Weg,
”
dann vom Besten aus“ (One-Then-Best-Backtracking), siehe das Ende von Abschnitt
3.3.3. Unsere Modifikationen am Grundalgorithmus sind im einzelnen:
1
Bei den Messungen (Abschnitt 6.2) sehen wir, dass dies nur teilweise zutrifft. Eine Interpretation dieser Messungen erfolgt in Abschnitt 6.3.2.
4.4. Suboptimales Suchverfahren
35
• Es wird zusätzlich als Zustand noch gemerkt, welcher Knoten nprev zuletzt in
Schritt 2a ausgewählt wurde, sowie die bisher günstigste gefundene Lösung ng0
und deren Kosten cg0 := C(n0 , ng0 ).
• In Schritt 2a wird der heuristisch (f ∗ (n)) günstigste Nachfolger von nprev gewählt. Nur wenn keine solchen Nachfolger vorhanden sind, wird aus allen Knoten in NO der heuristisch günstigste ausgesucht. Wenn NO leer ist, aber bereits
eine Lösung ng0 vermerkt ist, schlägt die Suche nicht fehl, sondern diese Lösung
wird ausgegeben.
• In Schritt 2b wird die Lösung zunächst nur vermerkt, falls sie günstiger ist
als eine bereits gefundene. Das Ende der Suche kann auf verschiedene Weise
gesteuert werden:
– Wenn in NO kein Knoten n mehr ist, für den f ∗ (n) < cg0 , kann die Suche
beendet werden, da in diesem Fall bereits eine optimale Lösung gefunden
wurde. Dies ist so der Fall, da bei einer zulässigen Heuristikfunktion h∗ ja
die Kosten einer Lösung, die aus einem Knoten n hervorgeht, mindestens
f ∗ (n) sind.
– Wenn für alle Knoten n ∈ NO gilt f ∗ (n) + e ≥ cg0 , bzw. f ∗ (n) ∗ (1 + α) ≥
cg0 , so sind die Kosten der Lösung ng0 höchstens um e bzw. einen Faktor
(1 + α) höher als das Optimum. Die Begründung ergibt sich analog zum
vorigen Fall.
– Es kann auch eine Anzahl numg min festgelegt werden, so dass mindestens
numg min Lösungen (alle, wenn weniger als numg min ) gefunden werden,
außer im ersten Fall oben, wo weitere Lösungen ohnehin keine Verbesserung bringen können.
– Alternativ kann auch ein Maximum numg max für die Anzahl der zu findenden Lösungen festgelegt werden, um die Laufzeit der Suche zu begrenzen.
– Eine weitere Möglichkeit ist eine Zeitsteuerung: Es wird so lange gesucht,
bis eine vorgegebene Maximalzeit abgelaufen ist und eine Lösung gefunden wurde. Natürlich ist ein vorzeitiges Ende weiterhin möglich, wenn die
vermerkte Lösung sicher optimal ist (erster Fall, siehe oben).
Wir können auch eine Mischstrategie benutzen. Zum Beispiel führen wir zuerst eine
bestimmte Anzahl von Iterationen nach dem A∗ -Verfahren durch. Nur dann, wenn
hiermit nicht bereits eine Lösung erreicht wurde, wechseln wir zur Approximation.
Hierzu zählen wir zusätzlich zu den obigen Modifikationen in Schritt 2a die Anzahl
der Iterationen mit und wählen bis zum Erreichen der Schwelle immer aus allen
Knoten in NO aus. Erst nach Erreichen der Schwelle wählen wir zuerst unter den
Nachfolgern von nprev aus.
36
4. Effiziente Phasenkopplung mit zustandsbehafteter Termersetzung
5. Implementierung
Zur Implementierung des nun entwickelten Verfahrens ziehen wir den CodegeneratorGenerator cggg (Code Generator Generator for Graphs) heran, wie er von Boesler
[Boes98] entwickelt wurde.
Wir erweitern diesen um eine Anbindung der Maschinensimulation nach den Abschnitten 4.1 und 4.2, siehe Abschnitt 5.1. Weiterhin realisieren wir die Äquivalenzklassenbildung von Suchknoten von Abschnitt 4.3 und das schnelle suboptimale
Suchverfahren nach Abschnitt 4.4, vgl. Abschnitte 5.2 und 5.3. Wir verbessern außerdem die Implementierung der Suche wesentlich (Abschnitt 5.4). Abschließend
fassen wir in diesem Kapitel die Struktur des sich so ergebenden Codegenerators
zusammen.
5.1
5.1.1
Anbindung der Maschinensimulation
Schnittstellen
Wir kapseln die Maschinensimulation als getrenntes Modul und organisieren sie um
zwei abstrakte Datentypen: Maschinenbefehl machsim_insn_t und Maschinenzustand machsim_state_t.
Maschinenbefehle werden über Konstruktorfunktionen erzeugt. Ein leerer Maschinenbefehl wird über die Funktion
machsim_insn_t machsim_insn_none(void);
konstruiert. Diesen verwenden wir an allen Stellen, wo in der Spezifikation kein
echter Befehl zu einer Regel zugeordnet ist. Damit können wir alle Regeln uniform
behandeln, auch die, die nur Hilfstransformationen im Programmgraphen vornehmen
und daher keinen Code erzeugen. Die restlichen Konstruktoren hängen von dem
(abstrakten) Befehlssatz der Maschine ab und können auch Argumente haben, z.B.
machsim_insn_t machsim_make_insn_load(int source_psr,
int destination_psr);
38
5. Implementierung
Desweiteren gibt es Funktionen zum Vergleich, Kopieren und Löschen von Befehlen:
/* Vergleich auf Gleichheit */
int machsim_insn_compare(machsim_insn_t, machsim_insn_t);
/* Kopieren */
machsim_insn_t machsim_insn_copy(machsim_insn_t);
/* Löschen */
void machsim_insn_delete(machsim_insn_t);
Einen Maschinenzustand können wir auch kopieren, löschen und mit einem zweiten
Maschinenzustand vergleichen, außerdem kann auch ein Hashwert berechnet werden:
/* Kopieren */
machsim_state_t machsim_state_copy(machsim_state_t);
/* Löschen */
void machsim_state_delete(machsim_state_t);
/* Vergleich */
int machsim_state_equal(machsim_state_t, machsim_state_t);
/* Hashfunktion */
unsigned int machsim_state_hash(machsim_state_t);
Der Initialzustand q0 wird mit der Funktion machsim_state_initial() konstruiert.
Die Funktionen cost (Kostenberechnung) und δ (Berechnung des Folgezustandes)
werden ohnehin immer gepaart verwendet. Deshalb implementieren wir sie als eine
C-Funktion:
machsim_cost_t machsim_step(machsim_state_t previous_state,
machsim_insn_t instruction, machsim_state_t *next_state);
Der neue Zustand wird über den Zeigerparameter next_state zurückgegeben, die
Kosten als Ergebniswert.
Damit die Maschinensimulation auch die Codeausgabe beeinflussen kann, stellen wir
hierfür folgende Schnittstellen bereit:
/* Vor Beginn der Codeausgabe */
void machsim_emit_initial(FILE *output_file);
/* Vor der Ausführung des EMIT-Teils der Regel */
void machsim_emit_insn_before(machsim_state_t previous_state,
machsim_insn_t, FILE *output_file);
5.1. Anbindung der Maschinensimulation
39
/* Nach der Ausführung des EMIT-Teils der Regel */
void machsim_emit_insn_after(machsim_state_t previous_state,
machsim_insn_t, FILE *output_file);
/* Am Ende der Codeausgabe */
void machsim_emit_final(machsim_state_t final_state,
FILE *output_file);
Die Maschinenzustände, die übergeben werden, berechnen wir mit machsim_state_
initial und machsim_step. An machsim_emit_insn_before und machsim_emit_
insn_after übergeben wir die Zustände vor der gerade bearbeiteten Anweisung, an
machsim_emit_final den Zustand nach dem letzten Befehl.
Im Prinzip könnte also die komplette Codeausgabe so neu gestaltet werden, dass die
Ausgabe komplett in machsim_emit_insn_before oder machsim_emit_insn_after
erfolgt und keine EMIT-Teile mehr in der Spezifikation angegeben werden.
Diesen Mechanismus verwenden wir auch, um z.B. bei der Erkennung von SIMDBefehlen die nötigen Zusatzinformationen für die Nachbearbeitung auszugeben.
5.1.2
Regeln mit Befehlsmustern
Wir passen das Format der Codegeneratorspezifikation an, damit man Befehlsmuster
zwischen dem EVAL-Teil (Berechnung von Attributen) und dem EMIT-Teil (Ausgabe
von Code) angeben kann. Bis auf diese Modifikation belassen wir die Syntax der
Spezifikation unverändert wie in [Boes98], Abschnitt 5.2.
Die Syntax der Befehlsangabe ist:
insn
→
insn
→
insn name →
insn args
→
insn args
→
INSN { insn name insn args }
(legaler Teil eines C-Bezeichners)
(legale C-Parameterliste)
Die Entsprechungen zwischen Befehlsspezifikation und Konstruktoraufrufen sind folgenden Beispielen zu entnehmen:
Spezifikation
keine
INSN { foo }
INSN { bar(arg0, arg1) }
Konstruktoraufruf
machsim_insn_none()
machsim_make_insn_foo()
machsim_make_insn_bar(arg0, arg1)
Ein Beispiel einer Regel mit Befehlsspezifikation ist Folgendes:
RULE
COND
COST
EVAL
INSN
EMIT
.
}
l:Load(m:MemState r:Register) -> s:LoadState;
{ ATTR(l, mode) == M_I && ATTR(r, mode) == M_P }
{ 4 }
{ ATTR(s, mode) = M_I; }
{ load(PSR(r), PSR(s)) }
{
ld32d(0) {R r} -> {R s}
40
5. Implementierung
In diesem Fall beziehen wir die Nummern der Pseudoregister (PSR) in die Befehlsspezifikation ein, damit die Maschinensimulation auch Ladelatenzen berechnen kann.
Die Angabe PSR(r) entspricht der Adresse, von der geladen wird, und ist einbezogen, da ja auch für diese eine Ladelatenz entstehen kann. Das Pseudoregister PSR(s)
entspricht dem hier geladenen Wert.
5.1.3
Anbindung an die Suche
Die Anbindung zwischen Suche und Maschinensimulation ist einfach: In den Suchknoten halten wir zusätzlich den Maschinenzustand. Mit einer Funktion recalculate_machsim berechnen wir diesen und die sich bis zu einem Suchknoten ergebenden Kosten bei Bedarf passend.
Die Berechnung der Kosten vom Initialknoten zum laufenden Suchknoten erfolgt
nun also über die Maschinensimulation, während die Heuristik h∗ (n) über die in den
COST-Attributen in der Codegeneratorspezifikation angegebenen minimalen Kosten
berechnet wird.
5.2
Äquivalenzklassen von Suchknoten
In der alten cggg-Implementierung wurde der Vergleich von Knoten im Suchgraphen mittels der Objektidentität realisiert. Im Vergleich zur abstrakten Definition
eines solchen Knoten bei der Anwendung auf BURS-Codegenerierung (siehe Abschnitt 3.3) ist diese Einteilung zu fein, sprich der abstrakt gleiche Suchknoten wird
möglicherweise mehrfach instantiiert.
Unsere veränderte Suche behebt das Problem, indem wir von diesen konkreten Suchknoten Äquivalenzklassen bilden, vgl. Abschnitt 4.3. Wir verwenden hier die verfeinerte Äquivalenz nach Definition 4.6, da diese mit weniger Aufwand berechnet
werden kann.
Wenn zwei äquivalente, aber nicht identische Suchknoten gefunden werden, können wir denjenigen mit höheren Kosten verwerfen. Alle Nachfolger ordnen wir dem
günstigeren Knoten zu. Dies entspricht den ersten beiden Fällen in Schritt 2d in
Algorithmus 3.1.
5.3
Parametrisierbare Suche
Wir setzen den alternativen Suchalgorithmus aus Abschnitt 4.4 im selben Code um
wie die A∗ -Suche. Wir mussten nur ca. 100 Zeilen Code spezifisch hinzufügen.
Während der Aufruf des Codegenerators bisher über die Funktion
void cg_gen(cg_gnode_t *graph, FILE *output_file,
const char *name);
erfolgte, bieten wir jetzt auch eine alternative Funktion an:
void cg_gen_with_options(cg_gnode_t *graph, FILE *output_file,
const char *name, const search_options_t *options);
5.3. Parametrisierbare Suche
41
Die Optionen definieren wir wie folgt:
enum search_type {
SEARCH_TYPE_DEFAULT = 0,
SEARCH_TYPE_ASTAR = 0,
SEARCH_TYPE_ONETHENBEST = 1
};
typedef struct search_options {
enum search_type search_type;
/* parameters for SEARCH_TYPE_ONETHENBEST */
unsigned int onethenbest_max_goals;
int onethenbest_max_cost_add;
double onethenbest_max_cost_factor;
unsigned int onethenbest_astar_turns;
} search_options_t;
Mit dem Feld search_type kann man zunächst die Suchmethode auswählen:
SEARCH_TYPE_DEFAULT bzw. SEARCH_TYPE_ASTAR ist die alte A∗ -Suche, während
SEARCH_TYPE_ONETHENBEST die neue, auf dem Algorithmus ein Weg, dann vom
”
Besten aus“ basierende Methode ist.
Die weiteren Felder sind nur für den neuen Suchalgorithmus relevant:
onethenbest_max_goals
onethenbest_max_cost_add
onethenbest_max_cost_factor
onethenbest_astar_turns
Anzahl gesuchte Lösungen
Maximale Mehrkosten gegenüber Optimum
Maximales Verhältnis Kosten : Optimum
Mischstrategie: A∗ -Durchläufe
Für die One-Then-Best-Suche halten wir im Zustand der Suche folgende Information
zusätzlich zu den übernommenen Optionseinstellungen:
searchnode_t *found_goal;
/* data for one-then-best search */
unsigned int onethenbest_found_goals;
int onethenbest_found_goal_cost;
searchnode_t *onethenbest_preferred_node;
int onethenbest_preferred_node_cost;
int onethenbest_selected_count_at_bb_start;
Im Feld found_goal vermerken wir die beste bisher gefundene Lösung, onethenbest_found_goals ist ein Zähler, wieviele Lösungen bereits gefunden wurden. Die
Kosten der besten bisher gefundenen Lösung halten wir in onethenbest_found_
goal_cost.
Für die Auswahl des nächsten Suchknotens (Schritt 2a in Algorithmus 3.1) führen wir die Felder onethenbest_preferred_node (bisher günstigster Nachfolger
des zuletzt ausgewählten Suchknotens), onethenbest_preferred_node_cost (Kosten f ∗ (n) desselben) und onethenbest_selected_count_at_bb_start (Stand von
42
5. Implementierung
selected_count, dem Zähler der Durchläufe durch Schritt 2a, am Beginn des aktuellen Grundblocks) ein.
Hiermit implementieren wir 5 Funktionen der Suche neu. Die Umschaltung zwischen
A∗ und der neuen Suche erfolgt dementsprechend über 5 Funktionszeiger, über die
die Aufrufe erfolgen.
Am Start der Suche (also des Grundblocks) rufen wir die Funktion onethenbest_
basic_block_init auf. Diese setzt die Variablen found_goal, onethenbest_
found_goals und onethenbest_preferred_node zurück. Außerdem initialisieren
wir hier onethenbest_selected_count_at_bb_start auf den aktuellen Stand von
selected_count.
Die Funktion onethenbest_select_snode_from_open wählt den nächsten zu bearbeitenden Knoten aus der Menge offener Knoten aus. Wenn die Anzahl der Schleifendurchläufe (Differenz zwischen selected_count und onethenbest_selected_
count_at_bb_start) bereits mindestens onethenbest_astar_turns ist, versuchen
wir zuerst, einen Nachfolger des zuletzt ausgewählten Knotens zu finden: Wenn
das Feld onethenbest_preferred_node gesetzt ist und dieser Knoten weiterhin in
der Menge offener Knoten enthalten ist, wählen wir diesen Knoten aus. Das Feld
onethenbest_preferred_node wird dann gelöscht. In allen anderen Fällen (noch
zu wenige Schleifendurchläufe oder kein gültiger Nachfolger gefunden) geben wir das
günstigste (im Sinne von f ∗ (n)) Element der Menge offener Knoten zurück, gleich
wie im A∗ -Algorithmus.
Wenn der ausgewählte Knoten ein Zielknoten ist, rufen wir die Funktion onethenbest_goal_found auf. Wenn wir bisher noch kein Ziel gefunden haben oder die
Kosten des neuen Zielknotens geringer als die des bereits gefundenen Ziels sind,
speichern wir das neue Ziel in found_goal und dessen Kosten in onethenbest_
found_goal_cost. Dann erhöhen wir den Zähler onethenbest_found_goals.
Nach der Auswahl des Knotens, gegebenenfalls dem Vermerken eines Zielknotens
und der Erweiterung des Knotens (Schritt 2d der Suche) rufen wir die Endeprüfung
onethenbest_finish_p auf. Diese gibt entweder NULL oder das endgültig gewählte
Ziel zurück. Wenn wir noch überhaupt kein Ziel gefunden haben, geben wir NULL
zurück und setzen damit die Suche fort. Sonst geben wir das günstigste bisher gefundene Ziel zurück, wenn
1. die Menge offener Knoten leer ist,
2. die bisher gefundene Lösung höchstens so hohe Kosten hat wie f ∗ (n) des günstigsten Elements der Menge offener Knoten (dann ist die Lösung bereits optimal) oder
3. der Schwellwert für die Anzahl der zu findenden Lösungen onethenbest_max_
goals erreicht ist, die Differenz der Kosten zu den Kosten des günstigsten Knotens in Menge offener Knoten höchstens onethenbest_max_cost_add beträgt
(falls dieses ≥ 0, sonst wird diese Bedingung weggelassen) und das Verhältnis
der Kosten der Lösung und des günstigsten Elements in Menge offener Knoten
höchstens onethenbest_max_cost_factor beträgt.
5.4. Sonstige Laufzeitverbesserungen
43
Die in Abschnitt 4.4 auch vorgeschlagene Zeitsteuerung haben wir hier nicht implementiert.
Die fünfte Funktion ist onethenbest_open_set_add_wrapper. Diese rufen wir auf,
wenn in Schritt 2d der Suche Knoten zur Menge offener Knoten hinzugefügt werden
sollen. Zuerst fügt sie den Knoten tatsächlich in die Menge offener Knoten ein. Dann
vermerkt sie den Knoten in onethenbest_preferred_node und dessen annähernde
Kosten f ∗ (n) in onethenbest_preferred_node_cost, wenn dort noch kein Knoten
vermerkt ist oder der neue Knoten günstiger ist. Durch diesen Mechanismus realisieren wir die Auswahl des günstigsten Nachfolgers des zuletzt ausgewählten Knotens
auf effiziente Weise.
Im Falle der A∗ -Suche realisieren wir diese 5 Methoden einfacher: Die Variable
found_goal verwenden wir auch, aber nur zur Kommunikation zwischen astar_
goal_found und astar_finish_p.
Die Funktion astar_basic_block_init setzt found_goal zurück. Die Auswahl des
nächsten Knotens erfolgt in astar_select_snode_from_open, indem wir das günstigste Element der Menge offener Knoten wählen. Wenn wir ein Ziel gefunden haben
(astar_goal_found), vermerken wir dies in found_goal. Die Endeprüfung astar_
finish_p ist einfach: Wir geben found_goal zurück, falls es schon gesetzt ist. Die
Funktion astar_open_set_add_wrapper fügt den Knoten ohne sonstige Bearbeitung in die Menge offener Knoten ein.
5.4
Sonstige Laufzeitverbesserungen
Neben einigen Optimierungen, die die grundlegende Implementierung der Suche
nicht ändern, führen wir auch drei algorithmische Verbesserungen in der Suchimplementierung durch, die für beide Suchalgorithmen nennenswerte Verbesserungen
bringen.
Bei der Messung von Laufzeitprofilen hat sich nämlich für ein Beispiel (me1, siehe
Abschnitt 6.1.4) ergeben, dass fast die ganze Zeit zu etwa gleichen Teilen in den drei
Bereichen, die in den folgenden Abschnitten dargestellt werden, verbraucht wurde.
Mit den folgenden Optimierungen verbessern wir die Laufzeit für dieses Beispiel um
gut eine Größenordnung.
5.4.1
Verwaltung der Menge offener Knoten
Die Menge offener Knoten war bisher als eine hashbasierte Menge von Suchknoten
implementiert. Dies erlaubte ein effizientes Auffinden, Einfügen und Löschen von
Suchknoten. Jedoch stellte sich durch Messungen heraus, dass das Auffinden des
günstigsten Elements, das durch lineare Suche über die Menge implementiert war,
einen Flaschenhals darstellte.
Daher haben wir eine alternative Implementierung erprobt. Das Auffinden von äquivalenten Knoten implementieren wir weiterhin durch eine Hashtabelle. Parallel hierzu halten wir jedoch auch eine Halde (Heap). In dieser erfolgt das Einfügen und
Löschen in logarithmischer Zeit und das Auffinden eines minimalen Elements in
konstanter statt bisher linearer Zeit.
Die Einträge sind so untereinander verknüpft, dass auch bei einem Auffinden über
die Hashtabelle die Position in der Halde in konstanter Zeit gefunden wird und
umgekehrt.
44
5.4.2
5. Implementierung
Iteration über Nachfolger eines Suchknotens
In den ersten zwei Fällen in Schritt 2d des Suchalgorithmus (Algorithmus 3.1) müssen die Vorgänger- und Kostenangaben in den Nachfolgern des jeweils teureren konkreten Suchknotens geändert werden.
Dies erfolgt in der Funktion redirect. Diese war bisher so implementiert, dass über
alle Knoten in der Menge offener bzw. geschlossener Knoten iteriert wurde und die
Knoten mit dem passenden Vorgänger bearbeitet wurden. Dabei ist es ja nur nötig,
die Nachfolger jeweils eines bestimmten Knoten zu bearbeiten. Bisher war jedoch
in den Suchknoten nur die Information über die Vorgänger vermerkt. Also haben
wir die Datenstruktur so verändert, dass in jedem Suchknoten auch ein Verweis auf
einen Nachfolger und auf einen nächsten und vorhergehenden Geschwisterknoten
aktuell gehalten werden. Dadurch realisieren wir die Funktion redirect wesentlich
effizienter. Um die Nachfolger eines Suchknotens aufzuzählen, folgen wir nun dem
Verweis auf einen Nachfolger, die weiteren Nachfolger werden über die Verweise auf
dessen Geschwisterknoten gefunden.
5.4.3
Menge der noch zu bearbeitenden Programmgraphknoten
Beim Erweitern in Schritt 2d der Suche (Algorithmus 3.1) werden die Nachfolger
eines Suchknotens nur bei Bedarf berechnet. Hierzu wurde jeweils eine Menge der
noch nicht bearbeiteten Programmgraphknoten im aktuellen Grundblock berechnet,
indem die Menge der Programmgraphknoten umkopiert und anschließend die darin
bereits bearbeiten Knoten gelöscht wurden. Dann wurde mit der so angepassten
Kopie der Knotenmenge gearbeitet.
Dies hat sich auch als ineffizient erwiesen. Stattdessen iterieren wir über die Originalmenge der Programmgraphknoten im Grundblock und überspringen den Schleifendurchlauf, wenn der betrachtete Programmgraphknoten bereits bearbeitet wurde.
Zusammen mit einer effizienteren Implementierung von Mengen von Programmgraphknoten erzielen wir so eine weitere signifikante Verbesserung.
5.5
Übersicht über den Codegenerator
Die Modulstruktur des Codegenerators ist nahezu gleich wie in [Boes98], Abschnitt
5.1 bzw. Abbildung 5.1. Hinzu kommt nun die Anbindung einer Maschinensimulation, die aus der Suche heraus aufgerufen wird. Die Modulabhängigkeiten gestalten
sich nun wie in Abbildung 5.1 gezeigt.
Modulnamen in Rechtecken liegen mit cggg vor und werden lediglich kopiert. Module
in Ellipsen werden generiert, während Module in Rauten spezifisch für den Codegenerator zu erstellen sind. Das Modul machsim mit der Maschinensimulation könnte
in Zukunft auch aus einer Spezifikation erzeugt werden, sobald ein entsprechendes
Verfahren implementiert ist (z.B. nach [Müll95]).
Das Modul none steuert die Codegenerierung. Zuerst ruft es eventuell vorbereitende
Transformationen auf (transform). Danach wird der eigentliche Codegenerator über
den Funktionsaufruf cg_gen bzw. cg_gen_with_options im Modul init aufgerufen.
Zuerst werden in decorate alle relevanten Dekorationen berechnet, danach folgt die
Suche search inklusive Maschinensimulation und Codeausgabe.
5.5. Übersicht über den Codegenerator
45
operators
evaler
attribute
matcher
transform
gnode
register
none
decorate
rule
triple
init
localtrim
search
emitter
machsim
Abbildung 5.1: Die Modulabhängigkeiten des Codegenerators
prepare
46
5. Implementierung
6. Ergebnisse
In Experimenten konnten wir prinzipiell bestätigen, dass unsere Phasenkopplung zu
den gewünschten Erfolgen führt: Eine Befehlsanordnung für klassische Architekturen
mit Pipelining und superskalare Architekturen wird erreicht. Desweiteren kann unser
Verfahren auch SIMD-ähnliche Befehle erkennen.
Wir haben außerdem evaluiert, wie mit cggg komplexe, aber zusammenhängende
Muster erkannt werden können. Hierfür sind die Modifikationen, die wir in dieser
Arbeit vorgenommen haben, nicht unbedingt notwendig, da auch schon die Verfahren von Boesler ([Boes98]) hierfür ausreichen. Jedoch führen unsere Optimierungen
der Implementierung (siehe Abschnitt 5.4) sowie unser alternativer Suchalgorithmus
(siehe Abschnitte 4.4 und 5.3) zu deutlichen Laufzeitverbesserungen des Codegenerators.
6.1
Testumgebung
Für die Messungen haben wir eine gesonderte Testumgebung verwendet. Diese besteht aus einem Hauptprogramm, das eine einfach gehaltene Graphsyntax einliest
und danach die Codegenerierung startet. Desweiteren haben wir verschiedene Beispielgraphen in dieser Syntax manuell erstellt.
Als Beispiel für die Codegeneratorspezifikation erzeugen wir Code für eine idealisierte Prozessorarchitektur, die im Folgenden noch beschrieben wird. Auch unsere
Maschinensimulation ist stark vereinfacht, repräsentiert aber übliche Probleme der
Befehlsanordnung durch das Einhalten einer Ladelatenz. Auch zeigt sie die Identifikation von SIMD-Befehlen exemplarisch am Beispiel einer vierfachen Addition.
6.1.1
Idealisierte Prozessorarchitektur
Die Prozessorarchitektur, für die wir hier probeweise eine Spezifikation erstellt haben, ist eine klassische Architektur mit Pipelining. Den Befehlssatz, jedoch nicht
die VLIW-Eigenschaften, haben wir vom TriMedia TM-1300 [Phil00] abgeleitet. Die
vierfache Addition taucht im TM-1300 nicht auf und haben wir hier hinzugefügt,
um die Effektivität unseres Codeerzeugungsverfahrens in diesem Fall zu zeigen.
Alle Befehle haben hier eine einheitliche Ausführungszeit. Es gibt keine Konflikte,
jedoch eine Ladelatenzzeit von einem Befehl.
48
6.1.2
6. Ergebnisse
Codegeneratorspezifikation
Wir haben für alle Programmbeispiele (Abschnitt 6.1.4) und Varianten des Codegenerators (Abschnitt 6.1.5) dieselbe Codegeneratorspezifikation verwendet.
Diese enthält insbesondere Muster zur Erkennung von MLA-Befehlen (Multipliziereund-Addiere) sowie für die komplexeren Muster der Bewegungsschätzung (MotionEstimation) und FIR-Filterung. Außerdem erkennt sie die Teilbefehle der vierfachen
Addition.
6.1.3
Maschinensimulation
Die Maschinensimulation für die Testumgebung haben wir einfach gehalten und
manuell implementiert, da eine Implementierung eines Generierungsverfahrens auch
für Maschinensimulationen (z.B. nach [Müll95]) den Rahmen dieser Diplomarbeit
gesprengt hätte.
Der Maschinenzustand besteht aus 3 Variablen:
unsigned int add_byte_state;
int add_psrs[4];
int load_psr;
Die Variable add_byte_state zeigt an, wieviele möglicherweise mit dem nächsten
Befehl kombinierbare Additions-Teilbefehle direkt vorher kamen (0 bis 3). Die Reihung add_psrs enthält die Ausgabe-Pseudoregister der in add_byte_state gezählten Befehle, um die Unabhängigkeit der Additionen prüfen zu können. Indirekte
Abhängigkeiten stellen hier kein Problem dar, da dann ja zwischen den indirekt voneinander abhängigen Befehlen ein anderer Befehl ausgewählt werden müsste, was die
Zählung in add_byte_state ohnehin unterbräche.
Die Variable load_psr enthält entweder den Wert −1 oder die Nummer des direkt
vorher geladenen Pseudoregisters. Hiermit berechnen wir Ladelatenzen.
6.1.4
Testgraphen
Für die Messungen haben wir 13 Eingabegraphen verwendet. Sie bestehen alle aus
drei Grundblöcken: je einer für den Beginn und das Ende der Funktion und einer,
der die betrachteten Befehle enthält.
Mit einem dieser Graphen, load-latency, prüfen wir gezielt die Beachtung der Ladelatenz unserer Beispielarchitektur. Es wird der Ausdruck −(+(load1 , load2 ), load3 )
berechnet. Das Ziel der Codeerzeugung ist, load3 direkt vor der Addition auszuwerten und somit die Ladelatenzen aufzufüllen.
Mit sechs Graphen zeigen wir die Erkennung komplexer Muster und das Laufzeitverhalten des Codegenerators.
Einer, mla, enthält den Ausdruck +(∗(load1 , load2 ), load3 ). Der Graph me1 enthält
den Ausdruck (Abs(−(Convi (load1 ), Convi (load2 )))). Dieser sollte als Bewegungsschätzung (Motion-Estimation) für ein Operandenpaar erkannt werden. Entsprechend gibt es den Graphen me2, wo zwei Operandenpaare verwendet werden (Summe
zweier Ausdrücke der obigen Form).
6.2. Messergebnisse
49
Die fir-Familie entspricht dem Grundmuster Summe von Produkten. Dieses Muster
wird für Filterungen in der digitalen Signalverarbeitung gebraucht und wird daher
z.B. vom TriMedia TM-1300 direkt unterstützt (ifir16 und ufir16: Summe von 2
Produkten von 16-Bit-Werten; ifir8ii, ifir8ui und ufir8uu: Summe von 4 Produkten von 8-Bit-Werten). Der Graph fir2 berechnet die Summe von 2 Produkten.
Desweiteren gibt es entsprechend fir3 mit 3 Produkten und fir4 mit 4 Produkten.
Mit der dritten Familie von Graphen testen wir die Erkennung von SIMD-Befehlen.
Der Graph add1b enthält nur eine Addition der Form, die potentiell kombiniert
werden kann. Natürlich muss hier letztlich festgestellt werden, dass dieser alleine
steht, und ein normaler Additionsbefehl erzeugt werden. Bei add2b existieren zwei
Additionen, die kombiniert werden sollen, und bei add4b wird die volle Mächtigkeit
des Vierfach-Additions-Befehls ausgenutzt. Dieser ist zwar nicht im realen TM-1300Befehlssatz, aber z.B. in Befehlssätzen wie SSE und AltiVec1 vorhanden.
Um die Skalierung auf größere Graphen zu erproben, haben wir auch für Varianten
mit 6, 7 und 8 Additionen (add6b, add7b, add8b) Messungen durchgeführt.
6.1.5
Varianten des Codegenerators
Wir haben fünf Varianten des Codegenerators erprobt. Die Variante no-phasecoupling ist nicht phasengekoppelt und führt eine reine Befehlsauswahl mittels der
A∗ -Suche durch. Sie enthält aber bereits die Verbesserungen der Implementierung
aus Abschnitt 5.4.
Dann folgt die Variante phase-coupled-no-equivalencies, die eine phasengekoppelte Codegenerierung mit A∗ -Suche realisiert, aber keine Äquivalenzklassenbildung
von Suchknoten vornimmt.
In der Variante phase-coupled-equivalencies holen wir diese Äquivalenzklassenbildung nach (siehe Abschnitt 4.3 und 5.2).
Das alternative schnelle, aber nicht garantiert optimale Suchverfahren (Abschnitte
4.4 und 5.3) haben wir in den Varianten onethenbest-16 und onethenbest-256
getestet. Die Parameter haben wir hierfür wie folgt gewählt:
search_type
onethenbest_max_goals
onethenbest_max_cost_add
onethenbest_max_cost_factor
onethenbest_astar_turns
6.2
SEARCH_TYPE_ONETHENBEST
16 bzw. 256
−1 (toleriere jegliche Abweichung)
1020 (toleriere jegliche Abweichung)
0 (reine One-Then-Best-Suche)
Messergebnisse
Wir haben die Messungen auf einem AMD-Athlon-Rechner mit ca. 1,5 GHz durchgeführt. Der Speicherbedarf wurde hierbei auf 256 Megabyte, die Laufzeit auf eine
Stunde beschränkt.
Jede Tabelle enthält Zeilen für die einzelnen Eingabegraphen mit dem Namen des
Graphen, der Anzahl der Graphknoten im relevanten Grundblock, der Laufzeit des
Codegenerators und der Angabe, ob in Hinblick auf unser Prozessormodell optimaler
1
Media-Erweiterungen von IA-32 bzw. PowerPC
50
6. Ergebnisse
Graph
load-latency
mla
me1
me2
fir2
fir3
fir4
add1b
add2b
add4b
add6b
add7b
add8b
# Knoten
Laufzeit
7
0,01 s
7
0,01 s
14
0,02 s
28
24,61 s
26
52,95 s
39
(Zeitüberschreitung) > 1 h
52
(Zeitüberschreitung) > 1 h
14
0,01 s
27
0,02 s
53
0,04 s
79
0,07 s
92
0,09 s
105
0,10 s
Optimaler Code?
nein
ja
ja
ja
ja
N/A
N/A
nein
nein
nein
nein
nein
nein
Tabelle 6.1: Ergebnisse Variante no-phase-coupling
Graph
load-latency
mla
me1
me2
fir2
fir3
fir4
add1b
add2b
add4b
add6b
add7b
add8b
# Knoten
Laufzeit
7
0,02 s
7
0,01 s
14
0,47 s
28
(Zeitüberschreitung) > 1 h
26
(Zeitüberschreitung) > 1 h
39
(Zeitüberschreitung) > 1 h
52
(Speicher > 256MB) > 3516 s
14
0,23 s
27
(Zeitüberschreitung) > 1 h
53
0,04 s
79
(Speicher > 256MB) > 2780 s
92
(Speicher > 256MB) > 2724 s
105
0,10 s
Optimaler Code?
ja
ja
ja
N/A
N/A
N/A
N/A
ja
N/A
ja
N/A
N/A
ja
Tabelle 6.2: Ergebnisse Variante phase-coupled-no-equivalencies
Graph
load-latency
mla
me1
me2
fir2
fir3
fir4
add1b
add2b
add4b
add6b
add7b
add8b
# Knoten
7
7
14
28
26
39
52
14
27
53
79
92
105
Laufzeit
0,01 s
0,01 s
0,33 s
(Zeitüberschreitung) > 1 h
(Zeitüberschreitung) > 1 h
(Zeitüberschreitung) > 1 h
(Zeitüberschreitung) > 1 h
0,17 s
(Zeitüberschreitung) > 1 h
0,03 s
(Zeitüberschreitung) > 1 h
(Zeitüberschreitung) > 1 h
0,12 s
Optimaler Code?
ja
ja
ja
N/A
N/A
N/A
N/A
ja
N/A
ja
N/A
N/A
ja
Tabelle 6.3: Ergebnisse Variante phase-coupled-equivalencies
6.2. Messergebnisse
Graph
load-latency
mla
me1
me2
fir2
fir3
fir4
add1b
add2b
add4b
add6b
add7b
add8b
# Knoten Laufzeit Optimaler Code?
7
0,01 s ja
7
0,01 s ja
14
0,01 s ja
28
0,03 s ja
26
0,02 s ja
39
0,03 s ja
52
0,04 s ja
14
0,01 s nein
27
0,03 s ja
53
0,04 s ja
79
0,10 s ja
92
0,13 s nein
105
0,16 s ja
Tabelle 6.4: Ergebnisse Variante onethenbest-16
Graph
load-latency
mla
me1
me2
fir2
fir3
fir4
add1b
add2b
add4b
add6b
add7b
add8b
# Knoten Laufzeit Optimaler Code?
7
0,01 s ja
7
0,02 s ja
14
0,06 s ja
28
0,11 s ja
26
0,09 s ja
39
0,11 s ja
52
0,14 s ja
14
0,07 s nein
27
0,11 s ja
53
0,05 s ja
79
0,26 s ja
92
0,38 s ja
105
0,16 s ja
Tabelle 6.5: Ergebnisse Variante onethenbest-256
51
52
6. Ergebnisse
3000.00 s
1000.00 s
= no−phase−coupling
= phase−coupled−no−equivalencies
= phase−coupled−equivalencies
= onethenbest−16
TT
TT
= onethenbest−256
T = Time exceeded
M = Memory exceeded
TTT
TMT
TT
T
M
T
M
300.00 s
100.00 s
30.00 s
10.00 s
3.00 s
1.00 s
0.30 s
0.10 s
0.03 s
0.01 s
ll
mla
me1
me2
fir2
fir3
fir4
add1b add2b add4b add6b add7b add8b
Abbildung 6.1: Messergebnisse
Code erzeugt wurde. Einen Abbruch kennzeichnen wir in der Spalte Laufzeit“ mit
”
dem Abbruchgrund und > Zeit bis zum Abbruch“ , in der Spalte Optimaler Code“
”
”
steht dann N/A“.
”
In Tabelle 6.1 zeigen wir die Ergebnisse ohne Phasenkopplung. Die Varianten mit
Phasenkopplung und A∗ -Suche stellen wir in Tabellen 6.2 (ohne Äquivalenzklassenbildung) bzw. 6.3 (mit Äquivalenzklassenbildung) dar. Die Resultate der One-ThenBest-Suche sind in den Tabellen 6.4 und 6.5 zu sehen.
Der Übersicht halber veranschaulichen wir die Laufzeiten auch graphisch in Abbildung 6.1. Die Zeiten haben wir auf einer logarithmischen Skala aufgetragen.
6.3
6.3.1
Auswertung
Auswirkung der Phasenkopplung
Unsere Erwartung, mit der Phasenkopplung zwischen Befehlsauswahl und -anordnung besseren Code zu erzeugen, hat sich erfüllt. Denn mit der Befehlsauswahl ohne
Phasenkopplung wurde das Optimierungsziel nur für die Erkennung komplexer zusammenhängender Muster erreicht. Dies ist klar; denn diese ist eine Nutzung der
BURS-Codeerzeugung, nicht der Phasenkopplung.
Unsere phasengekoppelte Codeerzeugung hat sich dagegen sowohl für die klassische
Befehlsanordnung, hier durch das Beispiel der Ladelatenz repräsentiert, als auch für
die automatische Erkennung von SIMD-Befehlen als nützlich erwiesen.
6.3. Auswertung
53
Jedoch müssen wir uns diese höhere Codequalität erwartungsgemäß mit teilweise
wesentlich höheren Laufzeiten und höherem Speicherbedarf der Codeerzeugung erkaufen. Dies ist in unseren Messungen an den Beispielen add2b, add6b und add7b, wie
auch der Erkennung komplexer Muster zu erkennen. Andererseits ist die Erkennung
komplexer Muster schon ohne Phasenkopplung teilweise langsam und speicherintensiv.
6.3.2
Äquivalenzen von Suchknoten
Die Auswirkung der Äquivalenzen von Suchknoten ist scheinbar widersprüchlich.
Der Grund für diese Paradoxie ist, dass zur Implementierung der Äquivalenzen eine
kompliziertere Vergleichsfunktion zwischen Suchknoten nötig ist. Bei Vergleichsmessungen, bei denen wir die Vergleichsfunktion ohne Äquivalenzen künstlich langsamer
gemacht haben, so dass sie etwa gleich schnell wie die Vergleichsfunktion mit Äquivalenzen ist, bestätigt sich diese Aussage.2
Unsere ursprüngliche Motivation, diese Äquivalenzen zu erproben, waren lange Laufzeiten des Beispiels add4b, die inzwischen aber auch ohne Äquivalenzen kaum ins
Gewicht fallen. Wir gehen davon aus, dass die Optimierungen der Implementierung
und gegebenenfalls die Beseitigung von in der Zwischenzeit vorhandenen Fehlern zu
dieser Veränderung geführt haben.
6.3.3
One-Then-Best-Suche
Die Auswirkungen der One-Then-Best-Suche sind auch bemerkenswert. Die Codegenerator-Laufzeiten fallen weit unter eine Sekunde zurück und auch der Speicherverbrauch wird wesentlich geringer. Damit wird die phasengekoppelte Codeerzeugung
überhaupt wirklich praktikabel. Mit den anderen Methoden sind nämlich die oft sehr
hohen Laufzeiten nicht akzeptabel.
Der durch dieses Verfahren erzeugte Code ist in fast allen Fällen de facto trotzdem
optimal, auch wenn das Verfahren diese Optimalität im Gegensatz zur A∗ -Suche
nicht garantiert.
In der Variante onethenbest-16 wird für zwei Graphen nicht optimaler Code erzeugt. Bei add1b wird einmal eine Ladelatenz missachtet, was bei unserer Testarchitektur zu einer Verzögerung führt. Bei add7b werden drei SIMD-Befehle statt
der nur 2 nötigen erzeugt. Die Laufzeiten sind mit höchstens 0,16 Sekunden sehr
akzeptabel.
Mit einer Erhöhung der Anzahl der erzeugten Ziele auf 256 wird für das Beispiel
add7b optimaler Code gefunden, wobei sich die Laufzeit von vorher 0,13 Sekunden
auf 0,38 Sekunden erhöht. Die Ladelatenz für add1b wird weiterhin missachtet.
Ein Test mit 1024 erzeugten Zielen liefert für alle Beispiele korrekten Code, wobei
die höchste erzielte Laufzeit bei dem Graphen add7b auftritt und 1,21 Sekunden
beträgt. Im Beispiel add1b ist es das 471-te Ziel, das als erstes eine optimale Lösung
enthält.
2
Beim Graphen me1 steigt die Laufzeit beispielsweise dann auf gut 21 Sekunden ohne Äquivalenzen, im Vergleich zu 0,33 Sekunden mit Äquivalenzen und 0,47 Sekunden ohne Äquivalenzen
ohne künstliche Verlangsamung. Bei dem Graphen add1b steigt die Laufzeit auf ca. 4,8 Sekunden,
verglichen zu vorher 0,23 Sekunden ohne bzw. 0,17 Sekunden mit Äquivalenzen.
54
6. Ergebnisse
Es zeigt sich bei weiteren Experimenten, dass sich die Ergebnisqualität auch ändert,
wenn das Testprogramm mit unterschiedlichen Optionen übersetzt ist. Dies können
wir dadurch erklären, dass dann die Speicherobjekte an anderen Speicherstellen liegen und dass unsere Implementierung teilweise Speicheradressen als Bestandteile von
Hashfunktionen verwendet. Dies wirkt sich dann auf die Reihenfolge von Iterationen
aus und kann somit im Falle der suboptimalen Suche unterschiedliche Ergebnisqualitäten, in allen Fällen auch Laufzeitunterschiede, die über den Unterschied der
Prozessorgeschwindigkeiten hinausgehen, erklären.
6.3.4
Laufzeitabhängigkeit von den Eingabegraphen
Bei der nicht phasengekoppelten Codeerzeugung ergibt sich hauptsächlich eine Abhängigkeit von der Größe der Graphen, was zum Beispiel an dem Laufzeitverhalten
der Additionsbeispiele zu erkennen ist. Desweiteren ergibt sich eine Abhängigkeit
von der Komplexität der Muster, die möglicherweise in den Eingabegraphen erkannt
werden können. Letzteres wirkt sich sehr stark aus: Die Muster für me2 und fir2 sind
schon schwer zu erkennen, bei den noch größeren Mustern fir3 und fir4 werden
die Laufzeiten extrem hoch.
Dieselbe Erhöhung der Komplexität bei komplexen Mustern zeigt sich, durch die
Phasenkopplung noch verstärkt, bei den phasengekoppelten Varianten mit A∗ -Suche.
Der Grund hierfür ist die Berechnung der Heuristikfunktion h∗ . Diese ist ja optimistisch und muss daher zunächst den günstigsten Fall berücksichtigen, dass tatsächlich das komplexe Muster, z.B. Abs(Sub(Conv(Register), Conv(Register))) (me1)
erkannt wird. Jedoch wird h∗ auch auf Pfaden im Suchgraphen nicht erhöht, wenn
z.B. die Regelanwendung Conv(Register) → Register eine spätere Erkennung des
me1-Musters unmöglich macht. Dadurch werden diese ungünstigeren Pfade im Suchgraphen unnötig lange durchsucht.
Ein interessantes Phänomen ist, dass die Laufzeit für add2b in der phasengekoppelten
Codeerzeugung höher ist als für add4b, desgleichen auch für add6b und add7b höher
als für add8b. Dies liegt wiederum an der Wahl der optimistischen Heuristik. Diese
schätzt für die in den Graphen vorhandenen Additionen die günstigen Kosten von
einer Viertel SIMD-Addition. Wenn diese Additionen vollständig ausgenutzt werden
(add4b und add8b) kann tatsächlich eine Lösung mit Kosten gefunden werden, die
h∗ (n0 ) (geschätzte Kosten ab dem Startknoten der Suche) entsprechen. Die Suche
wird also nach dem Finden dieser Lösung beendet.
In den anderen Fällen kann keine solche Lösung gefunden werden: Auch wenn nur 2
oder 3 SIMD-Teilbefehle gefunden werden, die Heuristik also zwei oder drei Viertel
der Kosten der SIMD-Addition schätzt, fallen die Kosten der ganzen SIMD-Addition
an. Diese benötigt ja auch bei unvollständiger Ausnutzung der SIMD-Register dieselbe Zeit. Auf den Pfaden im Suchgraphen führt dies zu einem Sprung von f ∗ (n),
sobald der erste Befehl, der auf die SIMD-Addition folgt, erreicht wird; denn dann
kann die Maschinensimulation erkennen, dass die SIMD-Addition nicht vollständig
ausgenutzt wird, und das Kostenmaß entsprechend anpassen. Damit wird die Suche bei diesem n zunächst nicht fortgesetzt, sondern es werden zuerst alle anderen
kombinatorischen Möglichkeiten gesucht, die Teiladditionen in anderer Reihenfolge
durchzuführen. Erst wenn diese alle durchsucht sind, die entsprechenden Knoten
mit günstigerem f ∗ (n) also nicht mehr in der Menge offener Knoten vorhanden sind,
6.3. Auswertung
55
wird die Suche bei einem n nach der SIMD-Addition fortgesetzt und endlich eine
Lösung gefunden.
56
6. Ergebnisse
7. Zusammenfassung und Ausblick
7.1
Ergebnisse dieser Arbeit
Wir haben in dieser Arbeit eine Theorie zustandsbehafteter Termersetzung entwickelt. Diese haben wir auf die Codeerzeugung aus graphbasierten SSA-Darstellungen
angewendet, indem wir ein existierendes BURS-Verfahren erweitert haben. Mit diesem zustandsbehafteten BURS-Verfahren haben wir eine Maschinensimulation in die
Codeerzeugung einbezogen und damit die Phasenkopplung zwischen Befehlsauswahl
und Befehlsanordnung erreicht. Wir haben weiterhin gezeigt, wie wir in einem integrierten Ansatz mit Hilfe dieser Phasenkopplung auch SIMD-Befehle automatisch
identifizieren können.
Da in unserem Verfahren, das die BURS-Codeerzeugung mittels heuristischer Suche
steuert, der Suchraum oft sehr groß wird, haben wir eine Äquivalenzrelation auf
Suchknoten definiert. Mittels dieser haben wir eine Quotientenmenge gebildet und
so den Suchraum verkleinert. Für den Fall, dass dieser verkleinerte Suchraum immer noch zu groß und für eine A∗ -Suche ungünstig strukturiert ist, haben wir auch
einen alternativen Suchalgorithmus entwickelt. Mit diesem garantieren wir zwar keine optimalen Lösungen, können es aber erlauben, durch entsprechende Parameter
den Kompromiss zwischen Laufzeit der Suche und Qualität der Lösung zu steuern.
Wir haben diese Verfahren implementiert, indem wir den Codegenerator-Generator
cggg von Boesler [Boes98] entsprechend erweitert und modifiziert haben. In der Implementierung konnten wir auch drei wesentliche Verbesserungen vornehmen, die
uns einen beträchtlichen Effizienzgewinn zusätzlich zu unseren theoretischen Fortschritten ermöglicht haben. In Experimenten mit dieser Implementierung haben wir
festgestellt, dass unsere Verfahren die phasengekoppelte Codeerzeugung tatsächlich
erreichen. Auch die Auswahl von SIMD-Befehlen ist uns gelungen. Es ergeben sich
mit der A∗ -Suche, die uns optimale Lösungen garantiert, teilweise sehr hohe Codegeneratorlaufzeiten. Andererseits ist zu sehen, dass wir mit unserem alternativen
Suchverfahren tatsächlich weiterhin gute Ergebnisse erzielen, die oft sogar optimal,
aber auch in den anderen Fällen nur leicht suboptimal sind. Wir haben so einen
tragfähigen Kompromiss erzielt, der guten Code phasengekoppelt erzeugt, automa-
58
7. Zusammenfassung und Ausblick
tisch SIMD-Befehle auswählen kann und hierbei sehr schnell ist, auch im Vergleich
zu verwandten Arbeiten.
7.2
Offene Probleme
Es ist eine interessante Problemstellung, wie die Kompromisse, die wir mit unserem
Suchverfahren eingehen, am besten zu steuern sind, also welche Parameter hierfür je nach Anforderungen am besten geeignet sind. Es könnte zum Beispiel eine
Zeitsteuerung entwickelt werden, bei der die Zeitschranke abhängig von der Größe
und Komplexität des gerade bearbeiteten Grundblocks gesetzt wird. Man könnte experimentell ermitteln, was für eine Funktion hier eine möglichst gleichmäßige
Codequalität bei akzeptablen Codegenerator-Laufzeiten bewirkt. Eine weitere offene
Frage ist, wie die Heuristikfunktion aus der Spezifikation eventuell genauer berechnet
werden kann, um schneller darauf reagieren zu können, wenn günstige Ersetzungen
durch eine ungünstige Wahl anderer Ersetzungen unmöglich gemacht werden (siehe
Abschnitt 6.3.4 zur Erkennung komplexer Muster).
Wir haben erkannt, dass die Ersparnis, die wir erzielen, indem wir den Suchraum mittels Äquivalenzklassenbildung verkleinern, teilweise durch den Aufwand, die Äquivalenz von Suchknoten zu prüfen, zunichte gemacht wird. Hier wäre interessant,
genauer zu untersuchen, wovon es abhängt, ob sich die Äquivalenzklassenbildung
lohnt. Auch könnten Verfahren entwickelt werden, um die Prüfung der Äquivalenz
effizienter durchzuführen.
Wir konnten in dieser Arbeit aus Zeitgründen kein Verfahren entwickeln, die Maschinensimulation auch aus Spezifikationen zu erzeugen. Stattdessen haben wir Schnittstellen zu einem gekapselten Modul entwickelt. Dadurch wollen wir es ermöglichen,
dass ein solcher Generator erstellt werden kann, der diese Schnittstellen nutzt, ohne
dass weitere Modifikationen am Codegenerator selbst nötig sein werden. Eine mögliche Grundlage für die automatische Erzeugung der Maschinensimulation bietet die
Dissertation von Müller [Müll95].
Weitere Forschungsfelder gerade im Bereich der Codeerzeugung für digitale Signalprozessoren umfassen zum Beispiel die Optimierung von Adressarithmetik und von
Moduseinstellungen. Die Adressarithmetik kann in vielen Architekturen durch die
Verwendung von Adressierungsarten, die gleichzeitig das Adressregister ändern (Präoder Postinkrement usw.), effizienter gestaltet werden.
Eine andere folgerichtige Entwicklung wäre, unser Verfahren auszubauen, um auch
die Registerzuteilung und das eventuell nötige Zwischenspeichern von Werten zu
integrieren. Es existiert an unserem Institut bereits ein graphfärbender Registerallokator. Jedoch wird dieser als getrennter Durchlauf durch den bereits ausgewählten
und angeordneten Code realisiert. Eine Phasenkopplung hierfür ist also noch nicht
erreicht. Desweiteren kann dieser Registerallokator noch keine Teilregister zuteilen,
wie dies für die üblichen SIMD-Befehle nötig ist. Eine Möglichkeit, diese Phasenkopplung zu modellieren, wäre vermutlich, die Registerzuteilung ähnlich wie die
Zuweisung von Funktionseinheiten, die ein Teilproblem der Befehlsanordnung ist, zu
behandeln, sprich letztlich ähnliche Schnittstellen wie die hier zwischen Befehlsauswahl und -anordnung realisierten zu verwenden.
Da sich diese Arbeit auf die Codeerzeugung konzentriert hat, haben wir Transformationen auf Quellcode- oder Zwischensprachebene nicht behandelt. Eigentlich kann
7.2. Offene Probleme
59
aber die Erkennung von Parallelität im Sinne von SIMD-Befehlen auch als klassische Vektorisierung behandelt werden. Im Beispiel der vierfachen Addition gelten die
hierfür verwendbaren Register als Vektorregister der Länge 4. Wir erwarten jedoch,
dass sich unsere Techniken gut mit bereits bekannten Vektorisierungsmethoden kombinieren lassen und so gute Ergebnisse zu erzielen sein werden. Insbesondere ist die
Erkennung von SIMD-Parallelität bei der Codeerzeugung dann weiterhin interessant, da sich diese ja auch aus Operationen ergeben kann, die der Vektorisierung
nicht zugänglich sind.
60
7. Zusammenfassung und Ausblick
Literaturverzeichnis
[BaLe99a]
Steven Bashford und Rainer Leupers. Constraint Driven Code Selection
for Fixed-Point DSPs. In Proceedings of the 36th ACM/IEEE Conference on Design Automation. ACM Press, New York, 1999, S. 817–822.
[BaLe99b]
Steven Bashford und Rainer Leupers. Phase-Coupled Mapping of Data
Flow Graphs to Irregular Data Paths. Design Automation for Embedded
Systems 4(2/3), 1999, S. 119–165.
[Boes98]
Boris Boesler. Codeerzeugung aus Abhängigkeitsgraphen. Diplomarbeit, Universität Karlsruhe (TH), Fakultät für Informatik, Institut für
Programmstrukturen und Datenorganisation, Juni 1998.
[Bund97]
Alan Bundy (Hrsg.).
Verlag, Berlin. 1997.
Artificial Intelligence Techniques.
Springer-
[CFRW+ 91] Ron Cytron, Jeanne Ferrante, Barry K. Rosen, Mark N. Wegman und
F. Kenneth Zadeck. Efficiently Computing Static Single Assignment
Form and the Control Dependence Graph. ACM Transactions on Programming Languages and Systems 13(4), Oktober 1991, S. 451–490.
[ChLa97]
Gerald Cheong und Monica S. Lam. An Optimizer for Multimedia Instruction Sets. In Proceedings of the Second SUIF Compiler Workshop.
Stanford University, USA, August 1997.
[ErKr91]
M. Anton Ertl und Andreas Krall. Optimal instruction scheduling using
constraint logic programming. In Programming Language Implementation and Logic Programming (PLILP), Passau, 1991. Springer LNCS
528, S. 75–86.
[FaLi97]
Martin Farach und Vincenzo Liberatore. On Local Register Allocation. Technischer Bericht TR97-33, Center for Discrete Mathematics &
Theoretical Computer Science, Piscataway, New Jersey, 1997.
[Farr88]
Henri Farreny. AI and expertise: heuristic search, inference engines,
automatic proving. Ellis Horwood Limited, Chichester, England. 1988.
[GaJo90]
Michael R. Garey und David S. Johnson. Computers and Intractability.
W. H. Freeman & Co., New York. November 1990.
[Gins93]
Matthew L. Ginsberg. Essentials of artificial intelligence. Morgan
Kaufman Publishers Inc., San Mateo, California. 1993.
62
Literaturverzeichnis
[GlGB02]
Sabine Glesner, Rubino Geiß und Boris Boesler. Verified Code Generation for Embedded Systems. In Proceedings of the COCV-Workshop
(Compiler Optimization meets Compiler Verification). Electronic Notes
in Theoretical Computer Science (ENTCS), 5th European Conferences
on Theory and Practice of Software (ETAPS 2002), April 2002.
[HaDe98]
Silvina Zimi Hanono und Srinivas Devadas. Instruction Selection, Resource Allocation, and Scheduling in the AVIV Retargetable Code Generator. In Proceedings of the 36th ACM/IEEE Conference on Design
Automation. ACM Press, New York, 1998, S. 510–515.
[Hano99]
Silvina Zimi Hanono. Aviv: A Retargetable Code Generator for Embedded Processors. PhD-Dissertation, Massachusetts Institute of Technology, Juni 1999.
[HeGr83]
John L. Hennessy und Thomas R. Gross. Postpass Code Optimization
of Pipeline Constraints. ACM Transactions on Proggramming Languages and Systems 5(3), Juli 1983, S. 422–448.
[HoAu99]
Jan Hoogerbrugge und Lex Augusteijn. Instruction scheduling for TriMedia. Journal of Instruction-Level Parallelism 1(1), Februar 1999.
[Käst97]
Daniel Kästner. Instruktionsanordnung und Registerallokation auf der
Basis ganzzahliger linearer Programmierung für den digitalen Signalprozessor ADSP-2106x. Diplomarbeit, Universität des Saarlands, 1997.
[Käst00]
Daniel Kästner. Retargetable Postpass Optimization by Integer Linear
Programming. Dissertation, Universität des Saarlands, Oktober 2000.
[Käst01]
Daniel Kästner. ILP-based Approximations for Retargetable Code Optimization. In Proceedings of the 5th International Conference on Optimization: Techniques and Applications (ICOTA 2001), 2001.
[KeBe01]
Christoph W. Keßler und Andrzej Bednarski. A Dynamic Programming
Approach to Optimal Integrated Code Generation. In ACM SIGPLAN
2001 Workshop on Languages, Compilers and Tools for Embedded Systems (LCTES’2001), Juni 2001.
[KLMT+ 96] Venkat Konda, Hugh Lauer, Katsunobu Muroi, Kenichi Tanaka, Hirono
Tsubota, Chris Wilson und Ellen Xu. A SIMDizing C Compiler for the
Mitsubishi Electric Neuro4 Processor Array. In Proceedings of the First
SUIF Compiler Workshop. Stanford University, USA, Januar 1996.
[Lamp73]
Leslie Lamport. The Coordinate Method for the Parallel Execution
of DO Loops. In T. Feng (Hrsg.), Proceedings of the 1973 Sagamore
Conference on Parallel Processing, 1973, S. 1–12.
[Lamp74]
Leslie Lamport. The Parallel Execution of DO Loops. Communications
of the ACM 17(2), Februar 1974, S. 83–93.
[LeBa00]
Rainer Leupers und Steven Bashford. Graph-based code selection techniques for embedded processors. ACM Transactions on Design Automation of Electronic Systems (TODAES) 5(4), Oktober 2000, S. 794–814.
Literaturverzeichnis
63
[Leup99]
Rainer Leupers. Compiler Optimization for Media Processors. In JeanYves Roger, Brian Stanford-Smith und Paul T. Kidd (Hrsg.), Business
and Work in the Information Society: New Technologies and Applications. IOS Press, 1999.
[Leup00]
Rainer Leupers. Code Selection for Media Processors with SIMD Instructions. In Proceedings of the Conference on Design Automation
and Test in Europe (DATE). IEEE Computer Society, Los Alamitos,
California, 2000, S. 4–8.
[Lind02]
Götz Lindenmaier. libFIRM – A Library for Compiler Optimization
Research Implementing FIRM. Technischer Bericht 2002-5, Universität
Karlsruhe (TH), Fakultät für Informatik, September 2002.
[LLMD+ 01] Markus Lorenz, Rainer Leupers, Peter Marwedel, Thorsten Dräger und
Gerhard Fettweis. Low-Energy DSP Code Generation Using a Genetic
Algorithm. In Proceedings of the IEEE International Conference on
Computer Design: VLSI In Computers & Processors (ICCD ’01). IEEE
Computer Society, September 2001, S. 431–437.
[LoWD02]
Markus Lorenz, Lars Wehmeyer und Thorsten Dräger. Energy aware
Compilation for DSPs with SIMD instructions. In Proceedings of the
Joint Conference on Languages, Compilers and Tools for Embedded
Systems: Software and Compilers for Embedded Systems. ACM Press,
New York, 2002, S. 94–101.
[MaGo95]
Peter Marwedel und Gert Goossens (Hrsg.). Code Generation for Embedded Processors. Kluwer Academic Publishers. 1995.
[MaKC99]
Rashindra Manniesing, Ireneusz Karkowski und Henk Corporaal. Evaluation of a Potential for Automatic SIMD Parallelization of Embedded
Applications. In M. Boasson, J. A. Kaandorp, J. F. M. Tonino und
M. G. Vosselman (Hrsg.), ASCI ’99: Proceedings. 5th Annual Conference of the Advanced School for Computing and Imaging (Heijen, June
1999). Advanced School for Computing and Imaging, Delft, Juni 1999.
[MSTM+ 97] Bart Mesman, Marino T.J. Strik, Adwin H. Timmer, Jef L. van Meerbergen und Jochen A.G. Jess. An Integrated Approach to Register
Binding and Scheduling. In Proceedings of the ProRISC Workshop on
Circuits, Systems and Signal Processing 1997. STW Technology Foundation, Utrecht, 1997, S. 365–374.
[Müll95]
Thomas Müller. Effiziente Verfahren zur Befehlsanordnung. Dissertation, Universität Karlsruhe (TH), Fakultät für Informatik, Institut für
Programmstrukturen und Datenorganisation, 1995.
[NKWA96] Albert Nymeyer, Joost-Pieter Katoen, Ymte Westra und Henk Alblas.
Code Generation = A∗ + BURS. In Tibor Gyimóthy (Hrsg.), Compiler Construction (CC’96), Band 1060 der Lecture Notes in Computer
Science, Linköping, Sweden, 1996. Springer-Verlag, Berlin, S. 160–177.
64
Literaturverzeichnis
[NoND95]
Steven Novack, Alex Nicolau und Nikil Dutt. A Unified Code Generation Approach using Mutation Scheduling. In Code Generation for
Embedded Processors, S. 203–218. Kluwer Academic Publishers, 1995.
[NoNi95]
Steven Novack und Alexandru Nicolau. Mutation Scheduling: A Unified
Approach to Compiling for Fine-Grain Parallelism. In Keshav Pingali,
Utpal Banerjee, David Gelernter, Alexandru Nicolau und David A. Padua (Hrsg.), Languages and Compilers for Parallel Computing, 7th International Workshop, LCPC’94, Ithaca, NY, USA, August 8-10, 1994,
Proceedings. Springer-Verlag, 1995, S. 16–30.
[NyKa97]
Albert Nymeyer und Joost-Pieter Katoen. Code generation based on
formal BURS theory and heuristic search. Acta Informatica 34(8), 1997,
S. 597–635.
[Pele88]
Eduardo Pelegrı́-Llopart. Rewrite Systems, Pattern Matching, and
Code Generation. Technischer Bericht UCB/CSD 88/423, University
of California, Berkeley, Juni 1988.
[Phil00]
Philips Semiconductors. Databook TM-1300 Media Processor, September 2000.
[PiME99]
Carlos A. Pinto, Bart Mesman und Koen van Eijk. Register File Capacity Satisfaction during Scheduling. In Proceedings of the ProRisc/IEEE
Benelux Workshop on Circuits, Systems and Signal Processing. STW
Technology Foundation, Utrecht, November 1999, S. 1–8.
[Seth75]
Ravi Sethi. Complete Register Allocation Problems. SIAM Journal on
Computing 4(3), September 1975, S. 226–248.
[SrGo00]
N. Sreraman und R. Govindarajan. A Vectorizing Compiler for Multimedia Extensions. International Journal of Parallel Programming
28(4), August 2000, S. 363–400.
[TrLB99]
Martin Trapp, Götz Lindenmaier und Boris Boesler. Documentation of
the Intermediate Representation FIRM. Technischer Bericht 1999-44,
Universität Karlsruhe (TH), Fakultät für Informatik, Dezember 1999.
[WGHB95] Tom Wilson, Gary Grewal, Shawn Henshall und Dilip Banerji. An ILPbased approach to code generation. In Code Generation for Embedded
Processors, S. 103–118. Kluwer Academic Publishers, 1995.
[Wolf96]
Michael J. Wolfe. High performance compilers for parallel computing.
Addison-Wesley, Redwood City, California. 1996.
[ZeWe01]
Thomas Zeitlhofer und Bernhard Wess. Integrated scheduling and register assignment for VLIW-DSP architectures. In Proceedings of the 14th
IEEE International ASIC/SOC Conference, September 2001, S. 339–
343.
Index
65
Index
Adressarithmetik, 58
Alphabet mit Rang, 21
Auswahlfunktion, 20
Befehlsanordnung, 27
Befehlsausgabefunktion, 31
Bottom Up Rewrite System, s. BURS
BURS, 23
auf DAGs, 23
auf zyklischen Graphen, 23
Codeerzeugung, 23
zustandsbehaftet, 32
Codegenerator
Modulstruktur, 44
Varianten, 49
Codegeneratorspezifikation
Regel mit Befehlsmuster, 39
Regelbeispiel, 39
Constraint Logic Programming
Phasenkopplung, 7
Dynamisches Programmieren
Phasenkopplung, 9
Firm, 19, 20
Ganzzahlige lineare Programmierung,
s. Integer Linear Programming
Genetische Algorithmen
Phasenkopplung, 8
Grundterm, 22
Grundtermersetzungssystem, 22
Ersetzung, 23
Ersetzungssequenz, 23
Regelanwendung, 23
zustandsbehaftet, 30
Ersetzung, 30
Ersetzungssequenz, 30
Regelanwendung, 30
Halde, 43
Heap, 43
Identifikation SIMD
Ganzzahlige lineare
Programmierung, s.
Identifikation SIMD, Integer
Linear Programming
Integer Linear Programming, 14
Integriert mit Phasenkopplung, 32
Musterersetzung, 14
Teilbefehle + Nachbearbeitung,
28, 32
Vektorisierung, 13
Integer Linear Programming
Identifikation SIMD, 14
Phasenkopplung, 5
Kostenbehaftetes
Grundtermersetzungssystem,
s. Grundtermersetzungssystem
Kostenbehaftetes
Termersetzungssystem, s.
Grundtermersetzungssystem
Maschinenbefehl
Abstrakter Datentyp, 37
Maschinensimulation, 28, 31
Schnittstelle, 37
Maschinenzustand, 27, 31
Abstrakter Datentyp, 38
Mealy-Automat, 30
Messergebnisse, 49
Modulstruktur des Codegenerators,
44
Moduseinstellungen, 58
Musterersetzung auf Quelltextebene
Identifikation SIMD, 14
Mutation Scheduling
Phasenkopplung, 10
Operator, 21
Phasenkopplung
Constraint Logic Programming, 7
Dynamisches Programmieren, 9
66
Ganzzahlige lineare
Programmierung, s.
Phasenkopplung, Integer
Linear Programming
Genetische Algorithmen, 8
Heuristische Suche, 9
Integer Linear Programming, 5
Integrierte Identifikation SIMD,
32
Mutation Scheduling, 10
Register als Funktionseinheiten,
12
Software-Pipelining, 11
TM-1000-Compiler, 12
zustandsbehaftetes BURS, 32
Phi-Auswahlfunktion, 20
Positionen in Termen, 22
Software-Pipelining
Phasenkopplung, 11
SSA, 19
Firm, 19, 20
Phi-Auswahlfunktion, 20
Schleifen, 20
Speicherzugriffe, 20
Statische Einmalzuweisung, 19
zyklische Graphen, 20
Static Single Assignment, s. SSA
Suche, 23
Äquivalenz von Suchknoten, 33
Algorithmus
‘ein Weg, dann vom Besten
aus’, 41
A, 26
A∗ , 26
Aα∗ , 26
Ae∗ , 26
Best first, 26
ein Weg, dann vom Besten
”
aus“, 27, 34
Grundalgorithmus, 24
heuristisch, 25
informiert, 25
Mischstrategie, 35
One-Then-Best-Backtracking,
27, 34, 41
uninformiert, 25
Approximation, 24
Codeerzeugung, 23
Index
Phasenkopplung, 9
Suchgraph, 23
Äquivalenz von Knoten, 33
Initialknoten, 24
Kostenfunktion, 24
Verfeinerte Äquivalenz von
Knoten, 34
Vergleich von Knoten, 40
Zielknoten, 24
Suchknoten, 24
Suchproblem, 24
Verfeinerte Äquivalenz von
Suchknoten, 34
Suchproblem, 24
Teilterm, 22
Term, 22
Termersetzungssystem, s.
Grundtermersetzungssystem
Testgraphen, 48
Testumgebung, 47
TriMedia
TM-1000, 12
TM-1300, 47
Variable, 21
Vektorisierung, 13
Identifikation SIMD, 13
Zustandsbehaftetes BURS, s. BURS,
zustandsbehaftet
Zustandsbehaftetes
Termersetzungssystem, s.
Grundtermersetzungssystem,
zustandsbehaftet
Herunterladen