1.06k likes | 1.24k Vues
Optimierungstechniken in modernen Compilern. Optimierungstechniken für DSPs und Mikrocontroller. Eigenschaften von DSP-Architekturen. Irregulärer Datenpfad: Spezialregister Abhängig von der verwendeten Operation wird ein bestimmtes Register benötigt. Eingeschränkte Form von ILP vorhanden
E N D
Optimierungstechniken in modernen Compilern Optimierungstechniken für DSPs und Mikrocontroller
Eigenschaften von DSP-Architekturen • Irregulärer Datenpfad: • Spezialregister • Abhängig von der verwendeten Operation wird ein bestimmtes Register benötigt. • Eingeschränkte Form von ILP vorhanden • Instruktionsformat erzeugt zusätzliche Einschränkungen • Pipeline ist eventuell vorhanden X-Speicher Y-Speicher ax ay af mx mf my ALU ALU +/- ar mr
Drei Phasen bei Zielcodeerzeugung • Codeauswahl (CS): • Finden einer Abbildung der Zwischencodebefehle auf die Maschinenbefehle der Zielarchitektur. • Registerallokation (RA): • Finden geeigneter Speicherklassen für die Variablen des Zwischencodes. • Ablaufplanung und Bindung (SC): • Finden einer geeigneten Ausführungsreihenfolge der Operationen und einer geeigneten Ausführungseinheit für jede Operation.
Problem der Reihenfolge • RA vor SC: • RA erzeugt zusätzliche Abhängigkeiten durch die genutzten Register. • SC vor RA: • Hoher Registerdruck kann entstehen. • Evtl. erforderlicher Spill-Code muss erneut geplant werden: Ablaufplanung besteht deshalb aus Prepass und Postpass. • CS vor SC: • Schlecht parallelisierbarer Code kann entstehen. • SC vor CS: • Nicht alle durch die Architektur vorgegebenen Einschränkungen können bei der Parallelisierung beachtet werden. Konsequenz: Code ist schlecht parallelisiert; Ressourcen bleiben ungenutzt. • RA vor CS: • Durch Registerzuordnung können gewisse Zieloperationen nicht mehr genutzt werden, weil sich die Werte in den falschen Registern befinden. • CS vor RA: • Einschränkung der Registerallokation, weil Ergebnisse in ganz bestimmte Register geschrieben werden. Konsequenz kann hoher Registerdruck sein.
Optimierungstechniken für DSPs und Mikrocontroller Ablaufplanung
Klassifizierung von Planungsalgorithmen • Lokal – Planung der Operationen innerhalb eines Basisblocks: • Ressourcenbeschränkt: List-Scheduling • Zeitbeschränkt: ASAP, ALAP, Force-Directed-Scheduling • Global – Planung der Operationen über Basisblockgrenzen hinaus: • Azyklisch – Planung der Operationen in schleifenfreiem Code: • Strukturorientiert: Region-based-scheduling, percolation-scheduling, global-scheduling • Profilorientiert: Trace-Schedling, Superblock-Scheduling, Hyperblock-Schedling • Zyklisch – Planung der Operationen in Schleifen: • Loop-Unrolling • Modulo-Scheduling
Lokale Ablaufplanung • Gegeben ist sequentieller Code für einen Basisblock - z.B. DAG - mit abgeschlossener Registerallokation. • Ziel: Festlegen eines Ausführungszeitpunktes (v) für jede Operation v, so dass keine Datenabhängigkeiten verletzt werden unter Berücksichtigung einer der folgenden zwei grundsätzlichen Nebenbedingungen: • Ressourcenbeschränktes Scheduling (z.B. List-Scheduling): • Gegeben ist eine Menge von Ressourcen. • Schedule darf zu keinem Zeitpunkt mehr Ressourcen erfordern als vorhanden sind. • Optimierungsziel: Minimierung der Schedulelänge. • Zeitbeschränktes Scheduling (z.B: ASAP, ALAP, Force-Directed-Scheduling): • Gegeben ist eine Schedulelänge. • Schedule darf die gegebene Schedulelänge nicht überschreiten. • Optimierungsziel: Minimierung des Ressourcenbedarfs.
Modellierung der Abhängigkeiten in einem Basisblock durch einen DAG • Totale Ordnung einer Anweisungsfolge im 3-Adress-Code wird zu einer partiellen Ordnung abgeschwächt. • G = (N, E, A, ord, label) sei ein gerichteter azyklischer Graph (DAG): • Knoten repräsentieren Operationen in den 3-Adress-Code-Anweisungen. • Kanten in E repräsentieren durch skalare Variablen entstehenden Flussabhängigkeiten. • Kanten in A repräsentieren durch Speicherzugriffe entstehende Datenabhängigkeiten. • ord : E modelliert die Reihenfolge der eingehenden Kanten (Operanden) eines Knotens. Bei ord(e) < ord(e') ist e linker und e' rechter Operand. • label : N {const k, store, load, write a, read a, | k , a +, ist Operation im 3-Adress-Code} ist eine Beschriftung der Knoten mit Operationen.
Konstruktion eines DAGs zu einem Basisblock • Eingabe: Basisblock als Folge von 3-Adress-Code-Anweisungen ir0,…,irn • Ausgabe: DAG (N, E, A, ord, label) • Algorithmus: • Hilfsfunktionen: N := , E := , A := , ord := , label := S := // Enthält für die aktuelle Situation bei der Übersetzung für jede Variable // des Zwischencodes u.a. den Knoten im DAG, der ihren Wert berechnet for i = 0 to n do switch(iri) case "x := y z": TranslateBinStmt(iri); break; case "x := y : TranslateUnaStmt(iri); break; case "x := y" : TranslateCopy(iri); break; case "@x := y" : TranslateStore(iri); break; case "x := @y" : TranslateLoad(iri); break; end od Für jedes (a,n,W) S mit a ist Programmvariable erzeuge Knoten m mit label(m) = write a, N := N {m}, E := E {(n,m)}, A := A {(h,m) | label(h) = read a oder label(h) = load oder label(h) = store} findVar(var) if (var,n,x) S then return n else return 0 fi findLabel(label,l,r) if n N mit Beschriftung label und ((l,n) E oder l = 0) und ((r,n) E oder r = 0) then return n else return 0 fi
Übersetzung von Kopieranweisungen TranslateConst(x := k) if findLabel(const k,0,0) = 0 then Erzeuge Knoten n mit label(n) = const k N := N {n} fi n := findLabel(const k,0,0) S := S {(x,n,W)} TranslateCopy(x := y) if findVar(y) = 0 then Erzeuge Knoten n mit label(n) = read y // passiert nur, wenn y Programmvariable N := N {n} S := S {(y,n,R)} fi l := findVar(y) S := S – {(x,n,k) | n N und k {R,W}) S := S {(x,l,W)}
Übersetzung binärer und unärer Operationen TranslateUnaStmt(x := y) l := findVar(y) // immer erfolgreich if findLabel(, l) then m := findLabel(, l) else Erzeuge neuen Knoten m mit label(m) = N := N {m} E := E {(l,m)} fi S := S – {(x,n,k) | n N und k {R,W}) S := S {(x,m,W)} TranslateBinStmt(x := y z) l := findVar(y) r := findVar(z) if n N mit label(n) = und (l,n) E und (r,n) E und not (ord(r,n) < ord(l,n)) then m := n else Erzeuge einen Knoten m mit Beschriftung N := N {m} E := E {(l,m),(r,m)} ord((l,m)) := 0; ord((r,m)) := 0, falls kommutativ, sonst ord((r,m)) := 1 fi S := S – {(x,n,k) | n N und k {R,W}) S := S {(x,m,W)}
Übersetzung von Speicherzugriffen TranslateLoad(x := @y) l := findVar(y) Erzeuge neuen Knoten n mit label(n)=load N := N {n} E := E {(l,n)} S := S – {(x,n,k) | n N und k {R,W}) S := S {(x,n,W)} A := A {(k,n) | k N und label(k) = store oder label(k) = write a} TranslateStore(@x := y) l := findVar(x) r := findVar(y) Erzeuge neuen Knoten n mit label(n)=store N := N {n} E := E {(l,n),(r,n)}; ord((l,n)):=0; ord((r,n)):=1; A := A {(k,n) | k N und label(k)=store oder label(k)=load oder label(k) = read a oder label(k) = write a}
Beispiel S Beispiel: a[i] = b[i] + a[j] (i,1,R) (t0,1,W) read j t0 := i t1 := 4 t2 := t1 * t0 t3 := &b t4 := t3 + t2 t5 := @t4 t6 := j t7 := 4 t8 := t7 * t6 t9 := &a t10 := t9 + t8 t11 := @t10 t12 := t5 + t11 t13 := i t14 := 4 t15 := t14 * t13 t16 := &a t17 := t16 + t15 @t17 := t12 read i const 4 (t1,2,W) 7 1 2 (t2,3,W) (t3,4,W) * const &b * const &a (t4,5,W) 3 4 8 9 (t5,6,W) (j,7,R) + + + (t6,7,W) 5 13 10 (t7,2,W) (t8,8,W) (t9,9,W) load load 6 11 (t10,10,W) (t11,11,W) + (t12,12,W) 12 (t13,1,W) (t14,2,W) (t15,3,W) (t16,9,W) store 14 (t17,13,W)
ASAP/ALAP • ASAP (As Soon As Possible) und ALAP (As Late As Possible) liefern keine besonders guten Ergebnisse. • Einsatz eher in der High-Level-Synthese. • Können beim ressourcenbeschränkten List-Scheduling aber zum steuern der Heuristik verwendet werden:
Ressourcenmodellierung durch Reservierungstabellen • Reservierungstabelle Tv für jede Operationsart v: • Tv R {0,...,l}, wobei R die Menge aller Ressourcen ist. • (r,t) Tv bedeutet, dass die Ressource r genau t Takte nach dem Starten der Operation v benötigt wird. • Zu einer Operationsart kann eine Menge von Reservierungstabellen v (R {0,...,l}) gehören, die eine Ausführung der Operation auf verschiedenen Ressourcen beschreiben. • Globale Reservierungstabelle TG R {0,...,m}, wobei m die maximale Länge des Ablaufplans ist. • Eine Operation der Art v kann zum Zeitpunkt p gestartet werden, falls: • eine Reservierungstabelle Tv v existiert und • für alle (r,t) Tv gilt: (r,p+t) TG. • Zu jeder Operationsart v ist durch die Funktion delay(v) festgelegt, wie viele Takte nach dem Starten einer Operation der Art v eine datenabhängige Operation gestartet werden kann.
Maschinenmodell für ressourcenbeschränkte Ablaufplanung • Modellierung der Reservierungstabellen: • R = {ALU1, ALU2, ALU3, FPU, MMU}. • ALU = {{(ALU1,0)}, {(ALU2,0)}, {(ALU3,0)}} delay(ALU) = 1 • MUL = {{(ALU1,0), (ALU1,1)}} delay(MUL) = 2 • FPU = {{(FPU,0),(FPU,1),(FPU,2)}} delay(FPU) = 3 • MMU = {{(MMU,0),(MMU,1)}} delay(MMU) = 2 Schematischer Aufbau des zugehörigen VLIW Prozessors: Speicher Steuerwerk FE/DE Registerbank DE/EX ALU ALU ALU FPU MMU Speicher EX/MEM
Ablaufplan • Gegeben ist ein DAG G = (N, E, A, ord, label), dessen Knotenbeschriftung Operationsarten sind. • A kann auch Datenabhängigkeiten modellieren, die z.B. durch eine abgeschlossene Registerallokation entstehen. • Ein Ablaufplan : N legt für jede Operation einen Startzeitpunkt fest. Der Ablaufplan ist gültig, wenn: • Für alle v N: (v) 0 und • Aus (u,v) E A folgt: (v) (u) + delay(label(u)) • Es darf keine Ressourcenkonflikte zwischen den Operationen geben. • Länge eines Schedules : length() = max{(v) + delay(label(v)) | v N} • Finden eines Schedules minimaler Länge ist NP-vollständig. • List-Scheduling ist eine Heuristik, die in der Praxis sehr gute Ergebnisse liefert.
List-Scheduling-Algorithmus Eingabe: DAG (N, E, A, ord, label) und delay Ausgabe: Schedule maxDelay := max{delay(t) | t T}; i = 0; ready(0) = {u | vN: (v,u) (EA)}; ready(k) = für alle 1 k maxDelay; scheduled = ; while (scheduled N) do while ein Knoten von aus ready(0) ohne Ressourcenkonflikt zum Zeitpunkt i gestartet werden do Wähle einen Knoten v ready(0) der ohne Konflikt zum Zeitpunkt i starten kann (v) := i; scheduled := scheduled {v}; foreach u (N – ready(0) – ... – ready(maxDelay) - scheduled) do if((v,u) (EA) and w N: (w,u) (EA) w scheduled) then ready(delay(v)) := ready(delay(v)) {u} fi od od i = i + 1; for k = 0 to maxDelay-1 do ready(k) := ready(k+1) od; ready(maxDelay) := od
Beispiel • Modellierung der Reservierungstabellen: • R = {ALU1, ALU2, ALU3, FPU, MMU}. • ALU = {{(ALU1,0)}, {(ALU2,0)}, {(ALU3,0)}} delay(ALU) = 1 • MUL = {{(ALU1,0), (ALU1,1)}} delay(MUL) = 2 • FPU = {{(FPU,0),(FPU,1),(FPU,2)}} delay(FPU) = 3 • MMU = {{(MMU,0),(MMU,1)}} delay(MMU) = 2 ALU MUL ALU 0 1 2 Globale Ressourcentabelle: Ready ALU ALU1 ALU2 ALU3 FPU MMU 3 1 0 2 0, 1, 2 1 ALU 3 3 4 4 4 5 5 MUL 5 5
Globales Scheduling • Verschieben von Operationen aus einem Basisblock in einen anderen. • Neben Beachtung der Datenabhängigkeiten auch Beachtung der Steuerflussabhängigkeiten erforderlich: • Alle Operationen, die im ursprünglichen Programm ausgeführt werden, müssen auch im optimierten Programm ausgeführt werden. • Spekulativ ausgeführte Operationen im optimierten Programm, dürfen keine ungewollten Seiteneffekte verursachen.
Wiederholung Definitionen • Block x dominiert Block y (x dom y) genau dann im Steuerflussgraphen, wenn jeder Pfad vom Startknoten zum Block y durch Block x führt. • Block x postdominiert Block y (x pdom y) genau dann im Steuerflussgraphen, wenn jeder Pfad vom Block y zum Stoppknoten durch Block x führt. • Block x und Block y sind genau dann steuerflussäquivalent, wenn x dom y und y pdom x. x x dom y und x dom z. z pdom x und z pdom y. y x und z sind steuerflussäquivalent. z
Aufwärtsverschiebung von Operationen • Eine Operation u wird aus einem Basisblock s in einen Basisblock d verschoben und d ist Steuerflussvorfahre von s. • Voraussetzung: • Es werden keine Datenabhängigkeiten verletzt. • Die durch u definierte Variable ist in d nicht lebendig. • Unterscheidung folgender Fälle: • s und d sind steuerflussäquivalent: • Verschiebung ist unproblematisch. • d dom s und s nicht pdom d: • u kann ausgeführt werden, obwohl es nicht ausgeführt werden sollte (spekulkative Ausführung). • Nur zulässig, wenn u keine Seiteneffekte verursacht. • Nur sinnvoll, wenn u in d "umsonst" ausgeführt werden kann. • d nicht dom s und s pdom d: • Auf allen Pfaden nach s muss an den Positionen, von denen aus d nicht mehr erreicht werden kann eine Kopie der Operation u eingefügt werden (Korrekturcode). Dabei ist zu beachten: • Die Operanden der Kopie von u müssen dieselben sein, wie im Block s. • Das Ergebnis von u darf keinen noch benötigten Wert überschreiben. • Das Ergebnis von u wird nicht überschrieben, bevor es den Block s erreicht. • Einige Ausführungspfade im Programm können langsamer werden. • Nur sinnvoll, wenn die optimierten Pfade häufiger ausgeführt werden als die verlangsamten Pfade.
Warum ändern sich Datenabhängigkeiten? • Code Motion kann Datenabhängigkeiten verändern: • In dem Beispiel entsteht eine WAW-Abhängigkeit: x=1 kann nicht vor x=2 verschoben werden. • Außerdem ändert sich der Lebendigkeitsbereich von x: x=1 kann nach der Transformation nicht in den Block mit x=2 verschoben werden. ... x = 2 ... x = 1 x = 2 x = 1 ... ...
Beispiel Architektureigenschaften: load hat Latenz von 2; jede andere Operation 1. Zwei Operationen können parallel ausgeführt werden. load [R1] R6 nop beqz R6, B3 load [R2] R7 nop store R7 [R3] load [R1] R6 || load [R4] R8 load [R2] R7 beqz R6, B3 || add r8, r8 r8 store R7 [R3] load [R4] R8 nop add r8, r8 r8 store R8 [R5] store R8 [R5] Ursprünglicher Steuerflussgraph Transformierter Steuerflussgraph
Abwärtsverschiebung von Operationen • Eine Operation u wird aus einem Basisblock s in einen Basisblock d verschoben und d ist Steuerflussnachfahre von s. • Voraussetzung: • Es werden keine Datenabhängigkeiten verletzt. • Unterscheidung folgender Fälle: • s und d sind steuerflussäquivalent: • Verschiebung ist unproblematisch. • s nicht dom d und d pdom s: • u kann ausgeführt werden, obwohl es nicht ausgeführt werden sollte (spekulkative Ausführung). • Nur zulässig, wenn u keine Seiteneffekte verursacht. • Alternativ: Kopien der Blöcke von s nach d erstellen und nur in die Kopie von d die Operation u verschieben. • s dom d und d nicht pdom s: • Auf allen Pfaden von s, muss an den Positionen, von denen aus d nicht mehr erreicht werden kann, eine Kopie der Operation u eingefügt werden (Korrekturcode). Dabei ist zu beachten: • Die Operanden der Kopie von u müssen dieselben sein, wie im Block s. • Das Ergebnis von u darf keinen noch benötigten Wert überschreiben. • Einige Ausführungspfade im Programm können langsamer werden. • Nur sinnvoll, wenn die optimierten Pfade häufiger ausgeführt werden als die verlangsamten Pfade.
Beispiel load [R1] R6 || load [R4] R8 load [R2] R7 beqz R6, B3 || add r8, r8 r8 store R7 [R3] store R8 [R5] Anlegen einer Kopie und löschen des leeren Basisblocks. load [R1] R6 || load [R4] R8 load [R2] R7 beqz R6, B3 || add r8, r8 r8 store R8 [R5] store R7 [R3] || store R8 [R5]
Zusammenfassung Codeverschiebung • Verschiebung zwischen steuerflussäquivalenten Blöcken: • Kein Korrekturcode und keine spekulative Ausführung von Operationen. • Aufwärts-/Abwärtsverschiebung von s nach d, wobei s nicht pdom/dom d: • Spekulative Ausführung von Operationen, die auf einigen Pfaden überflüssigerweise ausgeführt werden. • Aufwärts-/Abwärtsverschiebung von s nach d, wobei s nicht dom/pdom d: • Korrekturcode erforderlich. Ausführung auf einigen Pfaden kann verlangsamt werden. • Aufwärts-/Abwärtsverschiebung von s nach d, wobei s nicht dom/pdom d und s nicht pdom/dom d: • Kombination der letzten beiden Fälle mit allen Nachteilen.
Region-Based-Scheduling • Voraussetzung: Steuerflussgraph kann hierarchisch aus Regionen aufgebaut werden. • Region-Based-Scheduling unterstützt: • Aufwärtsverschiebung von Operationen aus s in einen steuerflussäquivalenten Basisblock d. • Aufwärtsverschiebung von Operationen aus s um eine Verzweigung zu dominierenden Vorgängerblöcken d; d.h. d dom s und s nicht pdom d. • In beiden Fällen ist kein Korrekturcode erforderlich.
Region • Eine Region in einem Steuerflussgraph (N,E) ist ein Subgraph (N',E') für den gilt: • N' N • E' E • Es existiert ein Knoten h N' mit N' dom(h) • Falls von einem Knoten m ein Knoten n N' erreicht werden kann, ohne dass auf diesem Pfad der Knoten h betreten wird, dann ist m N'. • E' ist die Menge aller Kanten zwischen den Knoten in N', die nicht h als Ziel haben. • Es ergibt sich eine Hierarchie von Regionen in einer Funktion: • die gesamte Funktion ist eine Region, • jede Schleife in der Funktion bildet eine eigene Region. • Rücksprungkanten einer Schleife zu ihrem Kopf h werden ignoriert, wodurch der Steuerflussgraph azyklisch wird.
Beispiel Region B1 B1 B1 B2 B2 R1 R3 B4 B3 B3 B5 B5 R2 R2 B6 B7 B7 B7
Scheduling-Algorithmus Eingabe: Steuerflussgraph und Liste der Regionen Ausgabe: Schedule mit Zuordnung von Operationen zu Basisblöcken for each Region R, wobei innere Regionen zuerst abgearbeitet werden do Berechne Datenabhängigkeiten zwischen den Operationen for each Basisblock B in R in topologischer Sortierung do // Steuerflussäquivalente Blöcke CE = {x | x R und x ist steuerflussäquivalent mit B} // dominierte Nachfolger DS = {x | x R und b CE: b dom x und bCE: (b,x) E} CB = CE DS t = 0 while nicht alle Operationen aus B sind verplant do CI = Operationen in CB, deren Vorgänger bereits verplant wurden for each u CI in Prioritätsreihenfolge do if u hat keine Ressourcenkonflikte then (u) = (t,B); Aktualisiere Ressourcenverwendung Aktualisiere Datenabhängigkeiten fi od t = t + 1; od od od
Optimierungstechniken für DSPs und Mikrocontroller Registerallokation
Basisblocklokale Registerallokation • Ziel: Abbildung der temporären Variablen eines Zwischencodeprogramms auf eine beschränkte Anzahl von Prozessorregistern. • Klassifizierung der Registerallokation: • Lokal: Auf den Basisblock beschränkt. • Global: Für Funktionen oder das gesamte Programm. • Vorgehensweise bei der Registerallokation hängt stark von der Zielarchitektur ab: • Register-/Register-Architektur oder Register-/Speicher-Architektur, • 2-Adress- oder 3-Adress-Architektur, • Universeller Registersatz oder Spezialregistersatz, • Flache oder tiefe Registerhierarchie.
Verwaltungsstrukturen für die Registerallokation • V ist die Menge aller Variablen im 3-Adress-Code. • Registerdeskriptor rd : {0,…,RegNum – 1} (V {w,r}) speichert für jedes Register die Menge der Variablen, deren Werte sich im Register befinden sowie deren Lese-/Schreibzustand. • Speicherdeskriptor: sd: V speichert für jede im Speicher abgelegte Variable die Speicheradresse (absolut für globale und relativ für lokale Variablen). • Belegungssituationen der Verwaltungsstrukturen: • Für jede globale Variable a ist durch sd(a) immer ein Speicherplatz festgelegt. • Bei Übersetzung einer Funktion f ist außerdem für jede lokale Variable a in f durch sd(a) eine relative Adresse festgelegt. • Für eine temporäre Variabel existiert • kein Eintrag in rd oder sd, • nur ein Eintrag in rd oder • nur ein Eintrag in sd oder • ein Eintrag in rd und sd. • Für eine Programmvariable existiert • immer ein Eintrag in sd • möglicherweise auch ein Eintrag in rd; dann befindet sich der aktuelle Wert der Variablen im Register.
Hilfsfunktionen • Hilfsfunktionen für eine Variable v: • isLocal(v) = True gdw. der Speicherplatz für v im Stapel ist. • addr(v) ist die Adresse des Speicherplatzes von v oder die relative Adresse, die während des Aufbaus der Symboltabelle festgelegt wurde. • getNextFreeLocalAddress(): Liefert die nächste freie relative Adresse im Stapel • getFreeReg(): liefert den Namen eines Registers, in das ein neuer Wert geschrieben werden kann. • getVarInReg(v): Erzeugt den erforderlichen Zielcode, um den Wert der Variablen v in einem Register bereitzustellen. • lockReg(r): Verhindert, dass der Inhalt des Registers r bei folgenden Registeranforderungen ausgelagert wird. • unlockReg(r): Klar • setRegDeskr(r,x): Danach gilt (x,w) = rd(r) und für alle i: 1 i RegNum und i r (x,w) rd(i) und (x,r) rd(i). • delete(x,r): Danach gilt: (x,w) rd(r) und (x,r) rd(r). • clear(): Löscht Einträge im Speicher- und Registerdeskriptor. • saveRegs(): Sichert Register im Speicher, die aktualisierte Werte von Programmvariablen enthalten.
Implementierung von getFreeReg Eingabe: keine Ausgabe: Name eines Registers, dessen Inhalt überschrieben werden kann Algorithmus getFreeReg: Falls ein i existiert mit 1 i RegNum und rd(i) = dann return i Falls ein i existiert mit 1 i RegNum und für alle (v,x) rd(i) gilt x = r, dann rd(i) := , return i Wähle ein s mit 1 s RegNum und s ist nicht gesperrt Spill(s) return s Eingabe: Registernummer s Ausgabe: Zielcode zum Auslagern des Registerwertes Algorithmus Spill: for each (v,w) rd(s) do if sd(v) undefiniert then addr = getNextFreeLocalAddr() outCode("mov s,[bp-addr]") sd(v) := addr else if v ist global then outCode("mov s,[sd(v)]") else outCode("mov s,[bp-sd(v)]") fi fi od rd(s) :=
Beispiel: getFreeReg • Aufruf: getFreeReg() mit Registerdeskriptor: • Aufruf: getFreeReg() mit Registerdeskriptor: Registerdeskriptor: Erzeugter Spillcode: Keiner Neuer Registerdeskriptor: i i rd(i) rd(i) 0 (t0,w), (a,r) 0 (t0,w), (a,r) 1 1 Rückgabewert: r1 2 (t2,w), (c,r) 2 (t2,w), (c,r) … … 15 (t15,w), (p,r) 15 (t15,w), (p,r) Registerdeskriptor: Erzeugter Spillcode: mov r1,[bp-sd(t1)] mov r1,[sd(a)] Neuer Registerdeskriptor: i i rd(i) rd(i) 0 (t0,w), (t16,w) 0 (t0,w), (t16,w) 1 1 (t1,w), (a,w) Rückgabewert: r1 2 (t2,w), (t18,w) 2 (t2,w), (t18,w) … … 15 (t15,w), (t31,w) 15 (t15,w), (t31,w)
Übersetzung binärer/unärer Anweisungen Eingabe: 3-Adress-Code-Anweisung x := y z Ausgabe: Zielcode Algorithmus: l := getVarInReg(y); lockReg(l); r := getVarInReg(z); lockReg(r); if isTemp(y) then Delete(y,l); if isTemp(z) then Delete(z,r); t := getFreeReg(x); unlock(l); unlock(r); asmmnem := Assembleropertion für outCode("asmmnem l,r,t"); setRegDeskr(t,x) Eingabe: 3-Adress-Code-Anweisung x := y Ausgabe: Zielcode Algorithmus: r := getVarInReg(y); lookReg(r); if isTemp(y) then Delete(y,r); t := getFreeReg(); unlook(r); asmmnem := Assembleropertion für outCode("asmmnem r,t"); setRegDeskr(t,x)
Beispiel • Übersetzung von t20 := t1 + t16; Aufruf von getVarInReg(t1) und getVarInReg(t16): • Aufruf von getFreeReg() Registerdeskriptor: Erzeugter Spillcode: mov r0,[bp-sd(t0)] mov r0,[sd(a)] mov [bp-sd(t16)],r0 Neuer Registerdeskriptor: i i rd(i) locked rd(i) locked 0 0 (t16,r) 1 0 (t0,w), (a,w) 1 (t1,w), (b,w) 1 (t1,w), (b,w) 0 1 Rückgabewert: r1 für t1 r0 für t16 0 2 (t2,w), (c,w) 0 2 (t2,w), (c,w) … … 0 15 (t15,w), (p,w) 0 15 (t15,w), (p,w) Neuer Registerdeskriptor: Erzeugter Spillcode: Keiner Registerdeskriptor: i i rd(i) locked rd(i) locked Rückgabewert: r0 0 1 0 (t20,w) 0 1 (b,w) 1 (b,w) 1 0 Zielcode: add r1,r0,r0 2 (t2,w), (c,w) 0 2 (t2,w), (c,w) 0 … … 15 (t15,w), (p,w) 0 15 (t15,w), (p,w) 0
Übersetzung von Labels und Sprunganweisungen Aktualisieren der Werte im Speicher. Eingabe: 3-Adress-Code-Anweisung label: Ausgabe: Zielcode Algorithmus: SaveRegs(); outCode("label:"); Clear(); … a := t7 label: t8 := a … Einsprung von verschiedenen Position möglich; Belegung der Register unklar. Eingabe: 3-Adress-Code-Anweisung goto label Ausgabe: Zielcode Algorithmus: SaveRegs(); outCode("jmp label"); Sprung zu einer Position an der der Registerdeskriptor gelöscht wird; Aktualisieren der Wert eim Speicher nötig. … a := t7 goto label10 label9: … Hier wird die Registerallokation fortgesetzt. Eingabe: 3-Adress-Code-Anweisung if x then goto l Ausgabe: Zielcode Algorithmus: t := getVarInReg(x); Delete(x,t) SaveRegs(); outCode("BNZ t,l"); Sprung zu einer Position an der der Registerdeskriptor gelöscht wird; Aktualisieren der Wert eim Speicher nötig. … a := t7 if t8 then goto label20 b := t9 … Fortsetzung der Registeralloka-tion. Belegung der Register für jede Programmausführung fest. Kein Sichern erforderlich.
Offensichtliche Verbesserungen • Lade-/Speicheranweisungen an jedem Anfang/Ende eines Basisblocks sind erforderlich. • Konseqnenz: Globale Planung der Register erforderlich. • CISC-Architekturen werden nur schlecht unterstützt, da jede 3-Adress-Code-Anweisung nach einem festen Schema übersetzt wird. • Konsequenz: Zielcodeauswahl flexibler gestalten. • Anordnung der Zielcodeoperationen ist nur vom Quelltext und der Übersetzung des Syntaxbaums in 3-Adress-code abhängig • Konsequenz: Bessere Anordnung der Operationen finden, um Verwendung von Prozessorregistern zu minimieren.
Integrated Prepass Scheduling • Ausführung der Ablaufplanung vor der Registerallokation. • Ablaufplanung mittel List-Scheduling nach zwei Strategien: • CSP: Ablaufplanung die bevorzugt Operationen plant, die keine Konflikte in der Pipeline verursachen. • CSR: Ablaufplanung, die bevorzugt Operationen plant, die den Registerdruck verringern. • Ablaufplanung protokolliert Anzahl der lebendigen Werte während der Planung mit. • Schwellwert minRegs, gibt an, ab wann bevorzugt Operationen geplant werden, die den Registerdruck verringern: • Solange mehr als minReg Register vorhanden sind, wird mit CSP geplant. • Sobald Anzahl der freien Register unter den Schwellwert minRegs sinkt, wird CSR verwendet.
Motivation Globale Registerallokation • Problem der lokalen Registerallokation: • Laden/Sichern der Werte von Programmvariablen am Anfang/Ende von jedem Basisblock. • Konsequenz: Überflüssige Lade-/Speicheroperationen. • Lösung: Globale Registerallokation für Programme in 3-Adress-Code-Form: • Halten von Werten in Registern über Basisblockgrenzen hinaus unter Beachtung des Steuerflusses. • Es muss abgesichert sein, dass der Wert einer Variablen, der an verschiedenen Programmpositionen definiert wird, sich immer im gleichen Register befindet. • Modellierung durch ein Graphfärbungsproblem; Lösung durch Heuristik. • Globale Registerallokation für Programme in SSA-Form: • Modelliertes Graphfärbungsproblem ist optimal lösbar. • Behandlung der -Funktionen erfordert zusätzliche Kopieroperationen.
Modellierung der Globalen Registerallokation durch Graphfärbung • Geeignet für Prozessorarchitekturen mit universellem Registersatz; K sei die Anzahl der Register. • Grundlage ist die Information über die lebendigen Variablen an den Programmpositionen im Steuerflussgraphen. • Sind zwei verschiedene Variablen u und v an derselben Position lebendig, dann können ihre Wert nicht im selben Register gehalten werden. • Modellierung durch einen ungerichteten Graphen (Interferenzgraphen) I = (V,E) : • V ist eine Knotenmenge, wobei jeder Knoten genau eine Variable im Zwischencode repräsentiert. • E {{u,v} | u,v V und u v}, wobei {u,v} E gdw. es existiert eine Programmposition p, an der u und v gemeinsam lebendig sind. • Eine Färbung I : V Kdes Interferenzgraphen (mit {u,v} E I(u) I(v)) entspricht einer Registerallokation, in der der Wert der Variablen v im Register I(v) gespeichert wird. • Zu jedem ungerichteten Graphen existiert ein Programm, das diesen Graphen als Interferenzgraph besitzt.
Beispiel Steuerflussgraph/Interferenzgraph () (d) (a,d) (a,d,c) d := 0 a := 1 c := 3 u v t (a,d,c) (a,c,f,d) w e f := c (c,d) (c,d) (c,d,r) (d,s,r) (d,t) (d,e) (a,c,f,d) (c,d) (d,u) (d,v) (d,w) (d,e) d:= d+1 r := 2*d s := 3*c t := r+s e := t+5 d:= a+f u := c v := u+1 w := v+1 e := w a d s f c r z (d,e) (d,c,e) (d,c,a) c:= d+3 a := e*c (a,d) (z) z:= a+d
Färben des Interferenzgraphen • Finden einer Reihenfolge v1,…,vn, in der die Knoten (zusammen mit adjazenten Kanten) aus dem Interferenzgraphen entfernt werden. • Einfügen der Knoten in der Reihenfolge vn,…,v1 und dabei Zuordnung einer Farbe, die verschieden von den Farben der bis dahin wieder eingefügten Nachbarn ist. • Heuristik zum Finden der Reihenfolge: • Entferne als nächstes einen Knoten, der weniger als K Nachbarn hat. • Dieser Knoten hat beim Einfügen dann auch weniger als K Nachbarn und kann damit sicher gefärbt werden. • Es ergibt sich folgender Algorithmus graphCol: Eingabe: Interferenzgraph (V',E') Ausgabe: Färbung V := V'; E := E'; i := 1; while es gibt ein v V mit |{{w,u} | {w,u} E und w = v}| < K do vi := v; i := i+1; V := V – {v}; E := E – {{w,u} | {w,u} E und w = v}; od := if V then return ; while i > 1 do i := i-1; V := V {vi}; E := E {{vi,w} | {vi,w} E' und w V} (vi) := k, wobei k {0,…,K-1} – {(w) | {vi,w} E} od return
Beispiel 1 u u v t v t w e w e s a d a d s f c r z f c r z I = (V, E), R = {0,1,2,3} w v u t e z s r c f a d
Spillen • Was, wenn der Interferenzgraph nicht zum leeren Graphen reduziert werden kann? • Falls im Algorithmus graphColV und kein Knoten mit weniger als |K| Nachbarn existiert, dann Spillentscheidung treffen: • Pessimistische Annahme: Färbung ist nicht möglich, weil die Nachbarn der verbleibenden Knoten mit mindestens K verschiedenen Farben gefärbt werden. • Optimistische Annahme: Färbung ist trotzdem möglich, weil es Nachbarn der verbleibenden Knoten gibt, die mit derselben Farbe gefärbt werden: R = {0,1}
Modifizierter Algorithmus bei optimistischer Strategie • Erst, wenn beim Färben festgestellt wird, dass keine Farbe verfügbar ist, wird eine Spillentscheidung getroffen: Eingabe: Interferenzgraph (V',E') Ausgabe: Färbung V := V'; E := E'; i := 1; while V do Wähle einen Knoten v V mit |{{w,u} | {w,u} E und w = v}| ist minimal vi := v; i := i+1; V := V – {v}; E := E – {{w,u} | {w,u} E und w = v}; od := while i > 1 do i := i-1; V := V {vi}; E := E {{vi,w} | {vi,w} E' und w V} if {0,…,K-1} – {(w) | {vi,w} E} = then return ; (vi) := k, wobei k {0,…,K-1} – {(w) | {vi,w} E} od return
Beispiel optimistische vs. pessimistische Strategie Pessimistische Annahme Optimistische Annahme u v t w e s a d a a d d f c r z f f c c I = (V, E), R = {0,1,2} Algorithmus graphCol bricht erst hier ab. Algorithmus graphCol bricht hier ab. w v u t e z s r c f d a w v u t e z s r