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