TECHNISCHE UNIVERSITÄT MÜNCHEN FAKULTÄT FÜR INFORMATIK Compilerbau I Dr. Michael Petter, Dr. Axel Simon SoSe 2012 1 / 184 Themengebiet: Die semantische Analyse 2 / 184 Semantische Analyse Scanner und Parser akzeptieren Programme mit korrekter Syntax. nicht alle lexikalisch und syntaktisch korrekten Programme machen Sinn 3 / 184 Semantische Analyse Scanner und Parser akzeptieren Programme mit korrekter Syntax. nicht alle lexikalisch und syntaktisch korrekten Programme machen Sinn der Compiler kann einige dieser sinnlosen Programme erkennen diese Programme werden als fehlerhaft zurückgewiesen Sprachdefinition definiert was fehlerhaft bedeutet 3 / 184 Semantische Analyse Scanner und Parser akzeptieren Programme mit korrekter Syntax. nicht alle lexikalisch und syntaktisch korrekten Programme machen Sinn der Compiler kann einige dieser sinnlosen Programme erkennen diese Programme werden als fehlerhaft zurückgewiesen Sprachdefinition definiert was fehlerhaft bedeutet semantische Analysen sind hierzu erforderlich, die Bezeichner eindeutig machen; die Typen von Variablen ermitteln; 3 / 184 Semantische Analyse Scanner und Parser akzeptieren Programme mit korrekter Syntax. nicht alle lexikalisch und syntaktisch korrekten Programme machen Sinn der Compiler kann einige dieser sinnlosen Programme erkennen diese Programme werden als fehlerhaft zurückgewiesen Sprachdefinition definiert was fehlerhaft bedeutet semantische Analysen sind hierzu erforderlich, die Bezeichner eindeutig machen; die Typen von Variablen ermitteln; semantische Analysen dienen auch dazu Möglichkeiten zur Programm-Optimierung zu finden; über möglicherweise fehlerhafte Programme zu warnen 3 / 184 Semantische Analyse Scanner und Parser akzeptieren Programme mit korrekter Syntax. nicht alle lexikalisch und syntaktisch korrekten Programme machen Sinn der Compiler kann einige dieser sinnlosen Programme erkennen diese Programme werden als fehlerhaft zurückgewiesen Sprachdefinition definiert was fehlerhaft bedeutet semantische Analysen sind hierzu erforderlich, die Bezeichner eindeutig machen; die Typen von Variablen ermitteln; semantische Analysen dienen auch dazu Möglichkeiten zur Programm-Optimierung zu finden; über möglicherweise fehlerhafte Programme zu warnen ; semantische Analysen attributieren den Syntaxbaum 3 / 184 Die semantische Analyse Kapitel 1: Attributierte Grammatiken 4 / 184 Attributierte Grammatiken viele Berechnungen der semantischen Analyse als auch der Code-Generierung arbeiten auf dem Syntaxbaum. was an einem (i.d.R. Nicht-Terminal) Knoten berechnet wird, hängt nur von dem Typ des Knotens ab eine lokale Berechnung greift auf bereits berechnete Informationen von Nachbarknoten zu und berechnen daraus neue Informationen für den lokalen Knoten und andere Nachbarknoten 5 / 184 Attributierte Grammatiken viele Berechnungen der semantischen Analyse als auch der Code-Generierung arbeiten auf dem Syntaxbaum. was an einem (i.d.R. Nicht-Terminal) Knoten berechnet wird, hängt nur von dem Typ des Knotens ab eine lokale Berechnung greift auf bereits berechnete Informationen von Nachbarknoten zu und berechnen daraus neue Informationen für den lokalen Knoten und andere Nachbarknoten Definition Eine attributierte Grammatik ist eine CFG erweitert um: Attribute für jedes Nichtterminal; lokale Attribut-Gleichungen. 5 / 184 Attributierte Grammatiken viele Berechnungen der semantischen Analyse als auch der Code-Generierung arbeiten auf dem Syntaxbaum. was an einem (i.d.R. Nicht-Terminal) Knoten berechnet wird, hängt nur von dem Typ des Knotens ab eine lokale Berechnung greift auf bereits berechnete Informationen von Nachbarknoten zu und berechnen daraus neue Informationen für den lokalen Knoten und andere Nachbarknoten Definition Eine attributierte Grammatik ist eine CFG erweitert um: Attribute für jedes Nichtterminal; lokale Attribut-Gleichungen. damit die Eingabe-Werte eines Knotens bei der Berechnung bereits vorliegen, müssen die Knoten des Syntaxbaums in einer bestimmten Reihenfolge durchlaufen werden 5 / 184 Beispiel: Berechnung des Prädikats empty[r] Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b): . . * | 0 a 2 1 b | a 3 a 4 b 6 / 184 Beispiel: Berechnung des Prädikats empty[r] Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b): . . * f | f 0 2 f a 1 | a f b 3 f a 4 b 6 / 184 Beispiel: Berechnung des Prädikats empty[r] Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b): . . * f f | f 0 2 f a 1 f | a f b 3 f a 4 b 6 / 184 Beispiel: Berechnung des Prädikats empty[r] Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b): . t f * . f f | f 0 2 f a 1 f | a f b 3 f a 4 b 6 / 184 Beispiel: Berechnung des Prädikats empty[r] Betrachte den Syntaxbaum des regulären Ausdrucks (a|b)*a(a|b): f . t f * . f f | f 0 2 f a 1 f | a f b 3 f a 4 b ; Werte von empty[r] werden von unten nach oben berechnet. 6 / 184 Idee zur Implementierung für jeden Knoten führen wir ein Attribut empty ein. die Attribute werden in einer depth-first Traversierung berechnet: an einem Blatt lässt sich der Wert des Attributs unmittelbar ermitteln das Attribut an einem inneren Knoten hängt nur von den Attributen der Nachfolger ab das empty ist ein synthetisches Attribut kann durch pre- oder post-order Traversierung berechnet werden 7 / 184 Idee zur Implementierung für jeden Knoten führen wir ein Attribut empty ein. die Attribute werden in einer depth-first Traversierung berechnet: an einem Blatt lässt sich der Wert des Attributs unmittelbar ermitteln das Attribut an einem inneren Knoten hängt nur von den Attributen der Nachfolger ab das empty ist ein synthetisches Attribut kann durch pre- oder post-order Traversierung berechnet werden Im Allgemeinen: Definition Ein Attribut ist synthetisch: Wert wird von den Blättern zur Wurzel propagiert inherit: Wert wird von der Wurzel zu den Blättern propagiert 7 / 184 Berechnungsregeln für empty Wie das Attribut lokal zu berechnen ist, ergibt sich aus dem Typ des Knotens: für Blätter: r andernfalls: i x definieren wir empty[r1 j r2 ] empty[r1 r2 ] empty[r1 ] empty[r1 ?] = = = = empty[r] = (x ). empty[r1 ] _ empty[r2 ] empty[r1 ] ^ empty[r2 ] t t 8 / 184 Spezifizierung von allgemeinen Attributsystemen Das empty Attributs ist rein synthetisch, daher kann es durch einfache strukturelle Induktion definiert werden. wir benötigen einen einfachen und flexiblen Mechanismus, mit dem wir über allgemeine Attribute an einem Knoten und seinen Nachfolgern reden können. der Einfachheit halber benutzen wir fortlaufende Indizes: empty[0] : empty[i] : das Attribut des aktuellen Knotens das Attribut des i-ten Sohns (i > 0) 9 / 184 Spezifizierung von allgemeinen Attributsystemen Das empty Attributs ist rein synthetisch, daher kann es durch einfache strukturelle Induktion definiert werden. wir benötigen einen einfachen und flexiblen Mechanismus, mit dem wir über allgemeine Attribute an einem Knoten und seinen Nachfolgern reden können. der Einfachheit halber benutzen wir fortlaufende Indizes: empty[0] : empty[i] : das Attribut des aktuellen Knotens das Attribut des i-ten Sohns (i > 0) ... im Beispiel: x : ? : j : : : empty[0] empty[0] empty[0] empty[0] empty[0] := := := := := (x ) empty[1] _ empty[2] empty[1] ^ empty[2] t t 9 / 184 Beobachtungen: Die lokalen Berechnungen der Attributwerte müssen von einem globalen Algorithmus zusammen gesetzt werden Dazu benötigen wir: 1 2 eine Besuchsreihenfolge der Knoten des Baums; eine lokale Berechnungsreihenfolgen Die Auswertungsstrategie muss mit den Attribut-Abhängigkeiten kompatibel sein 10 / 184 Beobachtungen: Die lokalen Berechnungen der Attributwerte müssen von einem globalen Algorithmus zusammen gesetzt werden Dazu benötigen wir: 1 2 eine Besuchsreihenfolge der Knoten des Baums; eine lokale Berechnungsreihenfolgen Die Auswertungsstrategie muss mit den Attribut-Abhängigkeiten kompatibel sein Wir geben Abhängigkeiten als gerichtete Kanten an: empty empty | empty ; Pfeil zeigt in die Richtung des Informationsflusses 10 / 184 Beobachtung: Zur Ermittlung einer Auswertungsstrategie reicht es nicht, sich die lokalen Attribut-Abhängigkeiten anzusehen. Es kommt auch darauf an, wie sie sich global zu einem Abhängigkeitsgraphen zusammen setzen. Im Beispiel sind die Abhängigkeiten stets von den Attributen der Söhne zu den Attributen des Vaters gerichtet. post-order depth-first Traversierung möglich ; Die Variablen-Abhängigkeiten können aber auch komplizierter sein. 11 / 184 Simultane Berechnung von mehreren Attributen Berechne empty, first, next von regulären Ausdrücken: x : empty[0] first[0] : root: f e empty[0] first[0] next[0] next[1] x := (x ) := fx j x 6= g == (keine Gleichung für next ) := empty[1] := first[1] := ; := next[0] f e f e root n n n 12 / 184 RE Auswertung: Regeln für Alternative j : empty[0] first[0] next[1] next[2] f f e := := := := | e n f n e n 13 / 184 RE Auswertung: Regeln für Alternative j : empty[0] first[0] next[1] next[2] f f e empty[1] _ empty[2] first[1] [ first[2] next[0] next[0] := := := := | e n f n e n 13 / 184 RE Auswertung: Regeln für Konkatenation : empty[0] first[0] next[1] next[2] := := := := f f e e n . n f e n 14 / 184 RE Auswertung: Regeln für Konkatenation : empty[0] first[0] next[1] next[2] f f e empty[1] ^ empty[2] first[1] [ (empty[1] ? first[2] : ;) first[2] [ (empty[2] ? next[0]: ;) next[0] := := := := e n . n f e n 14 / 184 RE Auswertung: Regeln für Kleene-Stern und ‘?’ f e f e : empty[0] first[0] next[1] := := := ? : empty[0] first[0] next[1] := := := * n f e n f e ? n n 15 / 184 RE Auswertung: Regeln für Kleene-Stern und ‘?’ f e f e : empty[0] first[0] next[1] := t := first[1] := first[1] [ next[0] ? : empty[0] first[0] next[1] := t := first[1] := next[0] * n f e n f e ? n n 15 / 184 Problem bei allgemeinen Attributen Eine Auswertungsstrategie kann es nur dann geben, wenn die Variablen-Abhängigkeiten in jedem attributierten Baum azyklisch sind Es ist DEXPTIME-vollständig, herauszufinden, ob keine zyklischen Variablenabhängigkeiten vorkommen können [Jazayeri, Odgen, Rounds, 1975] Beispiel: Grammatik S ! a j b; Attributenabhängigkeiten: h h h i a j i k S j j k h i b j k 16 / 184 Berechnung der Abhängigkeiten Grammatik: S ! a j b. h h h i a j i k S j j k h i b j k 17 / 184 Berechnung der Abhängigkeiten Grammatik: S ! a j b. h h h a i j j S i j k k h b i j k Betrachte Abhängigkeiten in jedem möglichen Baum: h h S j i a j k h h S j i b j k 17 / 184 Berechnung der Abhängigkeiten Grammatik: S ! a j b. h h h a i j j S i j k k h b i j k Betrachte Abhängigkeiten in jedem möglichen Baum: h h S j i a j k h Keiner der Graphen enthält einen Zyklus kann berechnet werden. h S j i b j k ; jeder Ableitungsbaum 17 / 184 Stark azyklische Attributierung Ziel: Berechne hinreichende Bedingung, dass Attributierung azyklisch ist. Idee: Berechne einen Abhängigkeitsgraphen für jedes Symbol N. Wir starten mit der lokalen Ordung vN = ! Betrachte jede Produktion N ! S1 Sn von N Für jeden möglichen Sohn-Knoten Si an i-ter Stelle finde Abhängigkeiten zwischen a[0] : : : z[0] von Si füge diese in vN ein zwischen a[i] : : : z[i] Hat vN einen Zykel, brechen wir erfolglos ab. Lässt sich vN für kein N mehr vergrößern, hören wir auf. Im Beispiel: h h i S j j k 18 / 184 Stark azyklische Attributierung Ziel: Berechne hinreichende Bedingung, dass Attributierung azyklisch ist. Idee: Berechne einen Abhängigkeitsgraphen für jedes Symbol N. Wir starten mit der lokalen Ordung vN = ! Betrachte jede Produktion N ! S1 Sn von N Für jeden möglichen Sohn-Knoten Si an i-ter Stelle finde Abhängigkeiten zwischen a[0] : : : z[0] von Si füge diese in vN ein zwischen a[i] : : : z[i] Hat vN einen Zykel, brechen wir erfolglos ab. Lässt sich vN für kein N mehr vergrößern, hören wir auf. Im Beispiel: h h i S j j k 18 / 184 Von den Abhängigkeiten zur Auswertungsstrategie Mögliche Strategien: 19 / 184 Von den Abhängigkeiten zur Auswertungsstrategie Mögliche Strategien: 1 Der Benutzer soll die Strategie spezifizieren 19 / 184 Von den Abhängigkeiten zur Auswertungsstrategie Mögliche Strategien: 1 Der Benutzer soll die Strategie spezifizieren 2 Berechne eine Strategie anhand der Abhängigkeiten Berechne eine linear Ordnung aus der partielle Ordnung ! bzw. v. Werte die Attribute anhand dieser linearen Ordnung aus. Die lokalen Abhängigkeitsgraphen zusammen mit der linearen Ordnung erlauben die Berechnung einer Strategie. n f e 19 / 184 Von den Abhängigkeiten zur Auswertungsstrategie Mögliche Strategien: 1 Der Benutzer soll die Strategie spezifizieren 2 Berechne eine Strategie anhand der Abhängigkeiten Berechne eine linear Ordnung aus der partielle Ordnung ! bzw. v. Werte die Attribute anhand dieser linearen Ordnung aus. Die lokalen Abhängigkeitsgraphen zusammen mit der linearen Ordnung erlauben die Berechnung einer Strategie. n f e 3 Betrachte fixe Strategie und erlaube nur entsprechende Attribute Frage: Wie berechnet man eine sinnvolle Linearisierung? 19 / 184 Linearisierung der Abhängigkeitsordnung Mögliche automatische Strategien: 20 / 184 Linearisierung der Abhängigkeitsordnung Mögliche automatische Strategien: 1 Bedarfsgetriebene Auswertung Beginne mit der Berechnung eines Attributs. Sind die Argument-Attribute noch nicht berechnet, berechne rekursiv deren Werte Besuche die Knoten des Baum nach Bedarf 20 / 184 Linearisierung der Abhängigkeitsordnung Mögliche automatische Strategien: 1 Bedarfsgetriebene Auswertung Beginne mit der Berechnung eines Attributs. Sind die Argument-Attribute noch nicht berechnet, berechne rekursiv deren Werte Besuche die Knoten des Baum nach Bedarf 2 Auswertung in Pässen: Minimiere die Anzahl der Besuche an jedem Knoten. Organisiere die Auswertung in Durchläufen durch den Baum. Berechne für jeden Durchlauf eine Besuchsstrategie für die Knoten zusammen mit einer lokalen Strategie für jeden Knoten-Typ Betrachte Beispiel für bedarfsgetriebene Auswertung 20 / 184 Beispiel bedarfsgetriebene Auswertung Berechne next der Blätter von ((ajb) a(ajb)): j : next[1] next[2] := next[0] := next[0] : next[1] next[2] := first[2] [ (empty[2] ? next[0]: ;) := next[0] . . * | a 0 a b 1 | 2 a 3 b 4 21 / 184 Beispiel bedarfsgetriebene Auswertung Berechne next der Blätter von ((ajb) a(ajb)): j : next[1] next[2] := next[0] := next[0] : next[1] next[2] := first[2] [ (empty[2] ? next[0]: ;) := next[0] . n . * | a 0 a b 1 n | 2 a n 3 n b n 4 21 / 184 Beispiel bedarfsgetriebene Auswertung Berechne next der Blätter von ((ajb) a(ajb)): j : next[1] next[2] := next[0] := next[0] : next[1] next[2] := first[2] [ (empty[2] ? next[0]: ;) := next[0] n . | a 0 n . * a b n e f 1 e f 2 a n 3 | n e f b n 4 21 / 184 Beispiel bedarfsgetriebene Auswertung Berechne next der Blätter von ((ajb) a(ajb)): j : next[1] next[2] := next[0] := next[0] : next[1] next[2] := first[2] [ (empty[2] ? next[0]: ;) := next[0] n . | a 0 n . * a b n e f 1 e f 2 a n 3 | n e f b n 4 21 / 184 Bedarfsgetriebene Auswertung Diskussion: Die Reihenfolge hängt i.a. vom zu attributierenden Baum ab. Der Algorithmus muss sich merken, welche Attribute er bereits berechnete Der Algorithmus besucht manche Knoten unnötig oft. Der Algorithmus ist nicht-lokal 22 / 184 Bedarfsgetriebene Auswertung Diskussion: Die Reihenfolge hängt i.a. vom zu attributierenden Baum ab. Der Algorithmus muss sich merken, welche Attribute er bereits berechnete Der Algorithmus besucht manche Knoten unnötig oft. Der Algorithmus ist nicht-lokal Ansatz nur im Prinzip günstig: Berechnungsstrategie ist dynamisch: Fehlersuche schwierig Berechnung aller Attribute ist oft billiger Meistens werden alle Attribute in allen Knoten benötigt 22 / 184 Diskussion Dann gilt: in jedem Durchlauf wird mindestens ein Attribut in einer stark azyklischen Attributierung berechnet man braucht folglich für stark azyklische Attributierungen maximal so viele Pässe, wie es Attribute gibt hat man einen Baum-Durchlauf zur Berechnung eines Attributes, kann man überprüfen, ob er geeignet ist, gleichzeitig weitere Attribute auszuwerten Optimierungsproblem ; 23 / 184 Diskussion Dann gilt: in jedem Durchlauf wird mindestens ein Attribut in einer stark azyklischen Attributierung berechnet man braucht folglich für stark azyklische Attributierungen maximal so viele Pässe, wie es Attribute gibt hat man einen Baum-Durchlauf zur Berechnung eines Attributes, kann man überprüfen, ob er geeignet ist, gleichzeitig weitere Attribute auszuwerten Optimierungsproblem ; ... im Beispiel: empty und first lassen sich gemeinsam berechnen. next muss in einem weiteren Pass berechnet werden ; lasse den Benutzer festlegen, wie Attribute ausgewertet werden 23 / 184 Implementierung der lokalen Auswertung: Pre-Order vs. Post-Order Betrachte Beispiel: Nummerierung der Blätter eines Baums: . . * | a 0 a b 1 | 2 a 3 b 4 24 / 184 Praktische Implementierung der Nummerierung Idee: Führe Hilfsattribute pre und post ein mit pre reichen wir einen Zählerstand nach unten (inherites Attribut) mit post reichen wir einen Zählerstand wieder nach oben (synthetisches Attribut) Root: pre[0] pre[1] post[0] := 0 := pre[0] := post[1] Node: pre[1] pre[2] post[0] := pre[0] := post[1] := post[2] Leaf: post[0] := pre[0] + 1 25 / 184 Die lokalen Attribut-Abhängigkeiten: pre pre post post pre pre post post die Attributierung ist offenbar stark azyklisch 26 / 184 Die lokalen Attribut-Abhängigkeiten: pre pre post post pre pre post post die Attributierung ist offenbar stark azyklisch ein innerer Knoten berechnet inherite Attribute bevor in einen Kindknoten abgestiegen wird (pre-order traversal) synthetische Attribute nach der Rückkehr von einem Kindknoten (post-order traversal) 26 / 184 Die lokalen Attribut-Abhängigkeiten: pre pre post post pre pre post post die Attributierung ist offenbar stark azyklisch ein innerer Knoten berechnet inherite Attribute bevor in einen Kindknoten abgestiegen wird (pre-order traversal) synthetische Attribute nach der Rückkehr von einem Kindknoten (post-order traversal) man kann alle Attribute in einer depth-first Traversierung von links nach rechts auswerten (mit pre- und post-order Berechnung) So etwas nennen wir L-Attributierung. 26 / 184 L-Attributierung Definition Ein Knoten mit Attributen A ist L-attributiert wenn jedes inherite Attribut ai [n] nur von aj [m] abhängt wobei m < n und ai ; aj 2 A. 27 / 184 L-Attributierung Definition Ein Knoten mit Attributen A ist L-attributiert wenn jedes inherite Attribut ai [n] nur von aj [m] abhängt wobei m < n und ai ; aj 2 A. Ursprung: eine L-attributierte Grammatik kann während des Parsens ausgewertet werden 27 / 184 L-Attributierung Definition Ein Knoten mit Attributen A ist L-attributiert wenn jedes inherite Attribut ai [n] nur von aj [m] abhängt wobei m < n und ai ; aj 2 A. Ursprung: eine L-attributierte Grammatik kann während des Parsens ausgewertet werden Idee: partitioniere alle Attribute A = A1 [ : : : [ An so dass für alle Attribute in Ai jeder Knoten L-attributiert ist für jede Attributmenge Ai führe eine depth-first Traversierung durch ; Bestimme Attribute mit Hinblick auf wenige Traversierungen 27 / 184 Praktische Benutzung Symboltabellen, Typ-Überprüfung / Inferenz und (einfache) Codegenerierung können durch Attributierung berechnet werden In diesen Anwendungen werden stets Syntaxbäume annotiert. Die Knotenbeschriftungen entsprechen den Regeln einer kontextfreien Grammatik Knoten können in Typen eingeteilt werden — entsprechend den Nichtterminalen auf der linken Seite Unterschiedliche Nichtterminale benötigen evt. unterschiedliche Mengen von Attributen. 28 / 184 Praktische Implementierung In objekt-orientierten Sprachen benutze visitor pattern: Klasse mit Methode für jedes Nicht-Terminal der Grammatik public abstract class Regex { public abstract void accept(Visitor v); } Durch Überschreiben der folgenden Methoden wird eine Attribut-spezifische Auswertung implementiert public interface Visitor { public void visit(Dot re) { re.children(this); } public void visit(Bar re) { re.children(this); } ... public void visit(Token tok) {} } Vordefiniert ist die Traversierung des Ableitungsbaumes public class OrEx extends Regex { RegEx l,r; public void accept(Visitor v) { v.visit(this); } public void children(Visitor v) { l.accept(v); r.accept(v); }} 29 / 184 Die semantische Analyse Kapitel 2: Symboltabellen 30 / 184 Symboltabellen Betrachte folgenden Java Kode: void foo() { int A; void bar() { double A; A = 0.5; write(A); } A = 2; bar(); write(A); } innerhalb des Rumpfs von bar wird die Definition von A durch die lokale Definition verdeckt für die Code-Erzeugung benötigen wir für jede Benutzung eines Bezeichners die zugehörige Definitionsstelle statische Bindung bedeutet, dass die Definition eines Namens A an allen Programmpunkten innerhalb des gesamten Blocks gültig ist. sichtbar ist sie aber nur in denjenigen Teilbereichen, in denen keine weitere Definition von A gültig ist 31 / 184 Gültigkeitsbereiche von Bezeichnern void foo() { int A; void bar() { double A; A = 0.5; write(A); } A = 2; bar(); write(A); 9 > > > > > > > > > > > > > > > > = > > > > > > > > > > > > > > > > ; Gültigkeitsbereich von int A } 32 / 184 Gültigkeitsbereiche von Bezeichnern void foo() { int A; void bar() { double A; A = 0.5; write(A); 9 > = > ; Gültigkeitsbereich von double A } A = 2; bar(); write(A); } 32 / 184 Gültigkeitsbereiche von Bezeichnern void foo() { int A; void bar() { double A; A = 0.5; write(A); 9 > = > ; Gültigkeitsbereich von double A } A = 2; bar(); write(A); } Verwaltungs von Bezeichnern kann kompliziert werden... 32 / 184 Sichtbarkeitsregeln in objektorientierten Prog.spr. 1 2 3 4 5 6 7 8 9 10 public class Foo { protected int x = 17; protected int y = 5; private int z = 42; public int b() { return 1; } } class Bar extends Foo { protected double y = 0.5; public int b(int a) { return a; } } Beobachtung: 33 / 184 Sichtbarkeitsregeln in objektorientierten Prog.spr. 1 2 3 4 5 6 7 8 9 10 public class Foo { protected int x = 17; protected int y = 5; private int z = 42; public int b() { return 1; } } class Bar extends Foo { protected double y = 0.5; public int b(int a) { return a; } } Beobachtung: private Members sind nur innerhalb der aktuellen Klasse sichtbar protected Members sind innerhalb der Klasse, in den Unterklassen sowie innerhalb des gesamten package sichtbar Methoden b gleichen Namens sind stets verschieden, wenn ihre Argument-Typen verschieden sind overloading ; 33 / 184 Dynamische Auflösung von Funktionen public class Foo { protcted int foo() { return 1; } } class Bar extends Foo { protected int foo() { return 2; } public int test(boolean b) { Foo x = (b) ? new Foo() : new Bar(); return x.foo(); } } Beobachtungen: 34 / 184 Dynamische Auflösung von Funktionen public class Foo { protcted int foo() { return 1; } } class Bar extends Foo { protected int foo() { return 2; } public int test(boolean b) { Foo x = (b) ? new Foo() : new Bar(); return x.foo(); } } Beobachtungen: der Typ von x ist Foo oder Bar, je nach Wert von b x.foo() ruft entweder foo in Zeile 2 oder Zeile 5 auf 34 / 184 Dynamische Auflösung von Funktionen public class Foo { protcted int foo() { return 1; } } class Bar extends Foo { protected int foo() { return 2; } public int test(boolean b) { Foo x = (b) ? new Foo() : new Bar(); return x.foo(); } } Beobachtungen: der Typ von x ist Foo oder Bar, je nach Wert von b x.foo() ruft entweder foo in Zeile 2 oder Zeile 5 auf Entscheidung wird zur Laufzeit gefällt (hat nichts mit Namensauflösung zu tun) 34 / 184 Überprüfung von Bezeichnern Aufgabe: Finde zu jeder Benutzung eines Bezeichners die zugehörige Deklaration Idee: 1 ersetze Bezeichner durch eindeutige Nummern 2 finde zu jedem Benutzung eines Bezeichners die Deklaration 35 / 184 Überprüfung von Bezeichnern Aufgabe: Finde zu jeder Benutzung eines Bezeichners die zugehörige Deklaration Idee: 1 ersetze Bezeichner durch eindeutige Nummern das Vergleichen von Nummern ist schneller das Ersetzen von gleichen Namen durch eine Nummer spart Speicher 2 finde zu jedem Benutzung eines Bezeichners die Deklaration 35 / 184 Überprüfung von Bezeichnern Aufgabe: Finde zu jeder Benutzung eines Bezeichners die zugehörige Deklaration Idee: 1 ersetze Bezeichner durch eindeutige Nummern das Vergleichen von Nummern ist schneller das Ersetzen von gleichen Namen durch eine Nummer spart Speicher 2 finde zu jedem Benutzung eines Bezeichners die Deklaration für jede Deklaration eines Bezeichners muss zur Laufzeit des Programs Speicher reserviert werden bei Programmiersprachen ohne explizite Deklaration wird implizit eine Deklaration bei der ersten Benutzung eines Bezeichners erzeugt 35 / 184 (1) Ersetze Bezeichner durch eindeutige Namen Idee für Algorithmus: Eingabe: Folge von Strings Ausgabe: 1 Folge von Nummern 2 Tabelle, die zu Nummern die Strings auflistet Wende diesen Algorithmus im Scanner auf jeden Bezeichner an. 36 / 184 Beispiel für die Anwendung des Algorithmus Eingabe: das das Ausgabe: 0 schwein schwein 1 ist dem dem 1 schwein menschen 2 3 4 0 1 2 3 4 5 6 das schwein ist dem was menschen wurst 0 1 3 ist 5 was wurst 2 6 und 37 / 184 Spezifikation der Implementierung Idee: benutze eine partielle Abbildung: S : String!int verwalte einen Zähler int count = 0; für die Anzahl der bereits gefundenen Wörter Damit definieren wir eine Funktion int getIndex(String w): int getIndex(String w) f if (S (w) undefined) f S = S fw 7! countg; return count++; else return S (w); g 38 / 184 Datenstrukturen für partielle Abbildungen Ideen: Liste von Paaren O(1) O(n) (w; i) 2 String int : ; zu teuer 39 / 184 Datenstrukturen für partielle Abbildungen Ideen: Liste von Paaren O(1) O(n) (w; i) balancierte Bäume : O(log(n)) O(log(n)) 2 String int : ; zu teuer ; zu teuer 39 / 184 Datenstrukturen für partielle Abbildungen Ideen: Liste von Paaren O(1) O(n) (w; i) balancierte Bäume : O(log(n)) O(log(n)) Hash Tables : O(1) O(1) 2 String int : ; zu teuer ; zu teuer zumindest im Mittel 39 / 184 Datenstrukturen für partielle Abbildungen Ideen: Liste von Paaren O(1) O(n) (w; i) balancierte Bäume : O(log(n)) O(log(n)) Hash Tables : O(1) O(1) 2 String int : ; zu teuer ; zu teuer zumindest im Mittel Caveat: In alten Compilern war der Scanner das langsamste Glied in der Kette. Heute der Zeitverbrauch des Scanners eher gering. 39 / 184 Implementierung mit Hash-Tabellen lege ein Feld M von hinreichender Größe m an wähle eine Hash-Funktion H : String ! [0; m 1] mit den Eigenschaften: H (w) ist leicht zu berechnen H streut die vorkommenden Wörter gleichmäßig über [0; m Mögliche Wahlen (~x = hx0 ; : : : xr H0 (~x) = H1 (~x) = H2 (~x) = 1 1] i): (xP0 + xr 1 ) % m ( ri=01 xi pi ) % m (x0 + p (x1 + p (: : : + p xr für eine Primzahl p (z.B. 31) 1 ))) % m Das Argument-Wert-Paar (w; i) legen wir dann in M [H (w)] ab 40 / 184 Berechnung einer Hash-Tabelle am Beispiel Mit m=7 und H0 erhalten wir: 0 1 2 3 4 5 6 schwein 1 menschen 5 was 4 ist 2 wurst 6 das 0 dem 3 Um den Wert des Worts w zu finden, müssen wir w mit allen Worten x vergleichen, für die H (w) = H (x) 41 / 184 Überprüfung von Bezeichnern: (2) Symboltabellen Überprüfe die korrekte Benutzung von Bezeichnern: Durchmustere den Syntaxbaum in einer geeigneten Reihenfolge, die jede Definition vor ihren Benutzungen besucht die jeweils aktuell sichtbare Definition zuletzt besucht für jeden Bezeichner verwaltet man einen Keller der gültigen Definitionen trifft man bei der Durchmusterung auf eine Deklaration eines Bezeichners, schiebt man sie auf den Keller verlässt man den Gültigkeitsbereich, muss man sie wieder vom Keller werfen trifft man bei der Durchmusterung auf eine Benutzung, schlägt man die letzte Deklaration auf dem Keller nach findet man keine Deklaration, haben wir einen Fehler gefunden 42 / 184 Beispiel: Keller von Symboltabellen { int a, b; b = 5; if (b>3) { int a, c; a = 3; c = a + 1; b = c; } else { int c; c = a + 1; b = c; } b = a + b; Abbildung von Namen auf Zahlen: 0 1 2 a b c } 43 / 184 Der zugehörige Syntaxbaum d Deklaration b Basis-Block a Zuweisung b b d b d b 0 int a b 1 int if = > 1 b 5 b b b 1 3 d b d a 2 int a d 0 int = 2 int = 1 1 2 2 44 / 184 Der zugehörige Syntaxbaum d Deklaration b Basis-Block a Zuweisung b b d b d b 0 int a b 1 int if = > 1 b 5 b b b 1 3 d b d a 2 int a d 0 int = 2 int = 1 1 2 2 44 / 184 Praktische Implementierung: Sichtbarkeit Das Auflösen von Bezeichnern kann durch L-attributierte Grammatik erfolgen 45 / 184 Praktische Implementierung: Sichtbarkeit Das Auflösen von Bezeichnern kann durch L-attributierte Grammatik erfolgen Benutzt man eine Listen-Implementierung der Symboltabelle und verwaltet man Marker für Blöcke, kann man auf das Entfernen von Deklarationen am Ende eines Blockes verzichten a b vor if-Anweisung 45 / 184 Praktische Implementierung: Sichtbarkeit Das Auflösen von Bezeichnern kann durch L-attributierte Grammatik erfolgen Benutzt man eine Listen-Implementierung der Symboltabelle und verwaltet man Marker für Blöcke, kann man auf das Entfernen von Deklarationen am Ende eines Blockes verzichten a b a c a b vor if-Anweisung then-Zweig 45 / 184 Praktische Implementierung: Sichtbarkeit Das Auflösen von Bezeichnern kann durch L-attributierte Grammatik erfolgen Benutzt man eine Listen-Implementierung der Symboltabelle und verwaltet man Marker für Blöcke, kann man auf das Entfernen von Deklarationen am Ende eines Blockes verzichten a b a c a b c a b vor if-Anweisung then-Zweig else-Zweig 45 / 184 Praktische Implementierung: Sichtbarkeit Das Auflösen von Bezeichnern kann durch L-attributierte Grammatik erfolgen Benutzt man eine Listen-Implementierung der Symboltabelle und verwaltet man Marker für Blöcke, kann man auf das Entfernen von Deklarationen am Ende eines Blockes verzichten a b a c a b c a b vor if-Anweisung then-Zweig else-Zweig Anstelle erst die Namen durch Nummern zu ersetzen und dann die Zuordnung von Benutzungen zu Definitionen vorzunehmen, kann man auch gleich eindeutige Nummern vergeben Anstatt ein eindeutige Nummern zu vergeben wird in der Praxis auch gerne ein Zeiger zum Deklarationsknoten gespeichert 45 / 184 Mehrfach- und Vorwärtsdeklarationen Manche Programmiersprachen verbieten eine Mehrfachdeklaration des selben Namens innerhalb eines Funktionsrumpfes. 46 / 184 Mehrfach- und Vorwärtsdeklarationen Manche Programmiersprachen verbieten eine Mehrfachdeklaration des selben Namens innerhalb eines Funktionsrumpfes. weise jedem Block eine eindeutige Nummer zu speichere für jede Deklaration auch die Nummer des Blocks, zu der sie gehört 46 / 184 Mehrfach- und Vorwärtsdeklarationen Manche Programmiersprachen verbieten eine Mehrfachdeklaration des selben Namens innerhalb eines Funktionsrumpfes. weise jedem Block eine eindeutige Nummer zu speichere für jede Deklaration auch die Nummer des Blocks, zu der sie gehört gibt es eine weitere Deklaration des gleichen Namens mit derselben Blocknummer, muss ein Fehler gemeldet werden 46 / 184 Mehrfach- und Vorwärtsdeklarationen Manche Programmiersprachen verbieten eine Mehrfachdeklaration des selben Namens innerhalb eines Funktionsrumpfes. weise jedem Block eine eindeutige Nummer zu speichere für jede Deklaration auch die Nummer des Blocks, zu der sie gehört gibt es eine weitere Deklaration des gleichen Namens mit derselben Blocknummer, muss ein Fehler gemeldet werden Wie zuvor können eindeutige Block Nummern auch durch Zeiger auf die Blöcke implementiert werden. 46 / 184 Mehrfach- und Vorwärtsdeklarationen Manche Programmiersprachen verbieten eine Mehrfachdeklaration des selben Namens innerhalb eines Funktionsrumpfes. weise jedem Block eine eindeutige Nummer zu speichere für jede Deklaration auch die Nummer des Blocks, zu der sie gehört gibt es eine weitere Deklaration des gleichen Namens mit derselben Blocknummer, muss ein Fehler gemeldet werden Wie zuvor können eindeutige Block Nummern auch durch Zeiger auf die Blöcke implementiert werden. Um rekursive Datenstrukturen zu ermöglichen erlauben Sprachen Vorwärtsdeklarationen: struct list1; struct list0 { int info; struct list1* next; } struct list1 { int info; struct list0* next; } 46 / 184 Deklaration von Funktionsnamen Auf gleiche Weise müssen auch Funktionsnamen verwaltet werden. bei einer rekursiven Funktion muss der Name vor der Durchmusterung des Rumpfes in die Tabelle eingetragen werden int fac(int i) { return i*fac(i-1); } 47 / 184 Deklaration von Funktionsnamen Auf gleiche Weise müssen auch Funktionsnamen verwaltet werden. bei einer rekursiven Funktion muss der Name vor der Durchmusterung des Rumpfes in die Tabelle eingetragen werden int fac(int i) { return i*fac(i-1); } bei wechselseitig rekursiven Funktionsdefinitionen gilt dies für alle Funktionsnamen. Beispiel in ML und C: fun | | and | | odd 0 = false odd 1 = true odd x = even (x-1) even 0 = true even 1 = false even x = odd (x-1) int even(int x); int odd(int x) { return (x==0 ? 0 : (x==1 ? 1 : even(x-1))); } int even(int x) { return (x==0 ? 1 : (x==1 ? 0 : odd(x-1))); } 47 / 184 Überladung von Namen Ähnliche Abhängigkeiten gelten für objekt-orientierte Sprachen: bei objekt-orientierter Sprache mit Vererbung zwischen Klassen sollte die übergeordnete Klasse vor der Unterklasse besucht werden 48 / 184 Überladung von Namen Ähnliche Abhängigkeiten gelten für objekt-orientierte Sprachen: bei objekt-orientierter Sprache mit Vererbung zwischen Klassen sollte die übergeordnete Klasse vor der Unterklasse besucht werden bei Überladung muss simultan die Signatur verglichen werden eventuell muss eine Typüberprüfung vorgenommen werden 48 / 184 Überladung von Namen Ähnliche Abhängigkeiten gelten für objekt-orientierte Sprachen: bei objekt-orientierter Sprache mit Vererbung zwischen Klassen sollte die übergeordnete Klasse vor der Unterklasse besucht werden bei Überladung muss simultan die Signatur verglichen werden eventuell muss eine Typüberprüfung vorgenommen werden Sobald Namen von Bezeichnern aufgelöst sind, kann die semantische Analyse fortsetzen mit der Typprüfung (oder Typinferenz, siehe Vorlesung „Programmiersprachen”). 48 / 184 Mehrere Sorten von Bezeichnern Einige Programmiersprachen unterscheiden verschiedene Bezeichner: C: Variablennamen und Typnamen Java: Klassen, Methoden, Felder Haskell: Typnamen, Variablen, Operatoren mit Prioritäten 49 / 184 Mehrere Sorten von Bezeichnern Einige Programmiersprachen unterscheiden verschiedene Bezeichner: C: Variablennamen und Typnamen Java: Klassen, Methoden, Felder Haskell: Typnamen, Variablen, Operatoren mit Prioritäten In einigen Fällen verändern Deklarationen in der Sprache die Bedeutung eines Bezeichners: Der Parser informiert den Scanner über eine neue Deklaration Der Scanner generiert verschiedene Token für einen Bezeichner Der Parser generiert einen Syntaxbaum, der von den bisherigen Deklarationen abhängt 49 / 184 Mehrere Sorten von Bezeichnern Einige Programmiersprachen unterscheiden verschiedene Bezeichner: C: Variablennamen und Typnamen Java: Klassen, Methoden, Felder Haskell: Typnamen, Variablen, Operatoren mit Prioritäten In einigen Fällen verändern Deklarationen in der Sprache die Bedeutung eines Bezeichners: Der Parser informiert den Scanner über eine neue Deklaration Der Scanner generiert verschiedene Token für einen Bezeichner Der Parser generiert einen Syntaxbaum, der von den bisherigen Deklarationen abhängt Interaktion zwischen Parser und Scanner: problematisch! 49 / 184 Fixity-Deklaration in Haskell Haskell erlaubt beliebige binäre Operatoren aus (?!^&|=+-_*/)+ . In Haskell Standard-Bibliothek: infixr infixl infixl infix 8 7 6 4 ^ *,/ +,==,/= Grammatik ist generisch: Exp0 ::= Exp0 LOp0 Exp1 | Exp1 ROp0 Exp0 | Exp1 Op0 Exp1 | Exp1 .. . Exp9 ::= Exp9 LOp9 Exp | | | Exp ::= | Exp ROp9 Exp9 Exp Op9 Exp Exp ident j num ( Exp0 ) 50 / 184 Fixity-Deklaration in Haskell Haskell erlaubt beliebige binäre Operatoren aus (?!^&|=+-_*/)+ . In Haskell Standard-Bibliothek: infixr infixl infixl infix 8 7 6 4 ^ *,/ +,==,/= Grammatik ist generisch: Exp0 ::= Exp0 LOp0 Exp1 | Exp1 ROp0 Exp0 | Exp1 Op0 Exp1 | Exp1 .. . Exp9 ::= Exp9 LOp9 Exp | | | Exp ::= | Exp ROp9 Exp9 Exp Op9 Exp Exp ident j num ( Exp0 ) Parser trägt Deklarationen in Tabelle ein Scanner erzeugt: Operator - wird zum Token LOp6 . Operator * wird zum Token LOp7 . Operator == wird zum Token Op4 . etc. Parser erkennt 3-4*5-6 als (3-(4*5))-6 50 / 184 Fixity-Deklarationen in Haskell: Beobachtungen Nicht ohne Probleme: Scanner hat einen Zustand, der vom Parser bestimmt wird nicht mehr kontextfrei, braucht globale Datenstruktur ; 51 / 184 Fixity-Deklarationen in Haskell: Beobachtungen Nicht ohne Probleme: Scanner hat einen Zustand, der vom Parser bestimmt wird nicht mehr kontextfrei, braucht globale Datenstruktur ; ein Stück Kode kann mehrere Semantiken haben syntaktische Korrektheit hängt evtl. von importierten Modulen ab 51 / 184 Fixity-Deklarationen in Haskell: Beobachtungen Nicht ohne Probleme: Scanner hat einen Zustand, der vom Parser bestimmt wird nicht mehr kontextfrei, braucht globale Datenstruktur ; ein Stück Kode kann mehrere Semantiken haben syntaktische Korrektheit hängt evtl. von importierten Modulen ab Fehlermeldungen des Parsers schwer verständlich 51 / 184 Fixity-Deklarationen in Haskell: Beobachtungen Nicht ohne Probleme: Scanner hat einen Zustand, der vom Parser bestimmt wird nicht mehr kontextfrei, braucht globale Datenstruktur ; ein Stück Kode kann mehrere Semantiken haben syntaktische Korrektheit hängt evtl. von importierten Modulen ab Fehlermeldungen des Parsers schwer verständlich Im GHC Haskell Compiler werden alle Operatoren als LOp0 gelesen und der AST anschliessend transformiert. 51 / 184 Typbezeichner und Variablen in C Die C Grammatik unterscheidet zwischen Terminal typedef-name und identifier. Betrachte folgende Liste von Deklarationen: typedef struct { int x,y } point_t; point_t origin; Relevante C Grammatik: declaration ! declaration-specifier ! | declarator ! (declaration-specifier)+ declarator ; static j volatile typedef void j char j char typedef-name identifier j 52 / 184 Typbezeichner und Variablen in C Die C Grammatik unterscheidet zwischen Terminal typedef-name und identifier. Betrachte folgende Liste von Deklarationen: typedef struct { int x,y } point_t; point_t origin; Relevante C Grammatik: declaration ! declaration-specifier ! | declarator ! (declaration-specifier)+ declarator ; static j volatile typedef void j char j char typedef-name identifier j Problem: Parser trägt point_t in Typen-Tabelle ein wenn die declaration Regel reduziert wird 52 / 184 Typbezeichner und Variablen in C Die C Grammatik unterscheidet zwischen Terminal typedef-name und identifier. Betrachte folgende Liste von Deklarationen: typedef struct { int x,y } point_t; point_t origin; Relevante C Grammatik: declaration ! declaration-specifier ! | declarator ! (declaration-specifier)+ declarator ; static j volatile typedef void j char j char typedef-name identifier j Problem: Parser trägt point_t in Typen-Tabelle ein wenn die declaration Regel reduziert wird Parserzustand hat mindestens ein Lookahead-Token 52 / 184 Typbezeichner und Variablen in C Die C Grammatik unterscheidet zwischen Terminal typedef-name und identifier. Betrachte folgende Liste von Deklarationen: typedef struct { int x,y } point_t; point_t origin; Relevante C Grammatik: declaration ! declaration-specifier ! | declarator ! (declaration-specifier)+ declarator ; static j volatile typedef void j char j char typedef-name identifier j Problem: Parser trägt point_t in Typen-Tabelle ein wenn die declaration Regel reduziert wird Parserzustand hat mindestens ein Lookahead-Token Scanner hat point_t in zweiter Zeile also schon als identifier gelesen 52 / 184 Typbezeichner und Variablen in C: Lösungen Relevante C Grammatik: declaration declaration-specifier declarator ! (declaration-specifier)+ declarator ; ! static j volatile typedef | void j char j char typedef-name ! identifier j Lösung schwierig: 53 / 184 Typbezeichner und Variablen in C: Lösungen Relevante C Grammatik: declaration declaration-specifier declarator ! (declaration-specifier)+ declarator ; ! static j volatile typedef | void j char j char typedef-name ! identifier j Lösung schwierig: versuche, Lookahead-Token im Parser zu ändern 53 / 184 Typbezeichner und Variablen in C: Lösungen Relevante C Grammatik: declaration declaration-specifier declarator ! (declaration-specifier)+ declarator ; ! static j volatile typedef | void j char j char typedef-name ! identifier j Lösung schwierig: versuche, Lookahead-Token im Parser zu ändern füge folgende Regel zur Grammatik hinzu: typedef-name ! identifier 53 / 184 Typbezeichner und Variablen in C: Lösungen Relevante C Grammatik: declaration declaration-specifier declarator ! (declaration-specifier)+ declarator ; ! static j volatile typedef | void j char j char typedef-name ! identifier j Lösung schwierig: versuche, Lookahead-Token im Parser zu ändern füge folgende Regel zur Grammatik hinzu: typedef-name ! identifier registriere den Typnamen früher 53 / 184 Typbezeichner und Variablen in C: Lösungen Relevante C Grammatik: declaration declaration-specifier declarator ! (declaration-specifier)+ declarator ; ! static j volatile typedef | void j char j char typedef-name ! identifier j Lösung schwierig: versuche, Lookahead-Token im Parser zu ändern füge folgende Regel zur Grammatik hinzu: typedef-name ! identifier registriere den Typnamen früher separiere Regel für typedef Produktion 53 / 184 Typbezeichner und Variablen in C: Lösungen Relevante C Grammatik: declaration declaration-specifier declarator ! (declaration-specifier)+ declarator ; ! static j volatile typedef | void j char j char typedef-name ! identifier j Lösung schwierig: versuche, Lookahead-Token im Parser zu ändern füge folgende Regel zur Grammatik hinzu: typedef-name ! identifier registriere den Typnamen früher separiere Regel für typedef Produktion rufe alternative declarator Produktion auf, die identifier als Typenamen registriert 53 / 184 Ausblick Implementierung von Symboltabellen für C nächste Woche: Überprüfung von Typen 54 / 184 Die semantische Analyse Kapitel 3: Typ-Überprüfung 55 / 184 Ziel der Typ-Überprüfung In modernen (imperativen / objektorientierten / funktionalen) Programmiersprachen besitzen Variablen und Funktionen einen Typ, z.B. int, void*, struct { int x; int y; }. Typen sind nützlich für: die Speicherverwaltung; die Vermeidung von Laufzeit-Fehlern In imperativen / objektorientierten Programmiersprachen muss der Typ bei der Deklaration spezifiziert und vom Compiler die typ-korrekte Verwendung überprüft werden. 56 / 184 Typ-Ausdrücke Typen werden durch Typ-Ausdrücke beschrieben. Die Menge T der Typausdrücke enthält: 1 Basis-Typen: int, char, float, void, ... 2 Typkonstruktoren, die auf Typen angewendet werden Beispiele für Typkonstruktoren: Verbunde: struct { t1 a1 ; : : : tk ak ; } Zeiger: t * Felder: t [] in C kann/muss zusätzlich eine Größe spezifiziert werden die Variable muss zwischen t und [n] stehen Funktionen: t (t1 ; : : : ; tk ) in C muss die Variable zwischen t und (t1 ; : : : ; tk ) stehen. in ML würde man diesen Typ anders herum schreiben: t1 : : : tk ! t wir benutzen (t1 ; : : : ; tk ) als Tupel-Typen 57 / 184 Typ-Namen Ein Typ-Name ist ein Synonym für einen Typ-Ausdruck. In C kann man diese mit Hilfe von typedef einführen. Typ-Namen sind nützlich als Abkürzung: typedef struct { int x; int y; } point_t; zur Konstruktion rekursiver Typen: Erlaubt in C: Lesbarer: struct list { int info; struct list* next; } typedef struct list list_t; struct list { int info; list_t* next; } list_t* head; struct list* head; 58 / 184 Typ-Prüfung Aufgabe: Gegeben: eine Menge von Typ-Deklarationen = ft1 x1 ; : : : tm xm ; g Überprüfe: Kann ein Ausdruck e mit dem Typ t versehen werden? Beispiel: struct list { int info; struct list* next; }; int f(struct list* l) { return 1; }; struct { struct list* c;}* b; int* a[11]; Betrachte den Ausdruck: *a[f(b->c)]+2; 59 / 184 Typ-Prüfung am Syntax-Baum Prüfe Ausdruck *a[f(b->c)]+2: + 2 [ ] ( ) a f : c b Idee: traversiere den Syntaxbaum bottom-up für Bezeichner schlagen wir in den richtigen Typ nach Konstanten wie 2 oder 0:5 sehen wir den Typ direkt an die Typen für die inneren Knoten erschießen wir mit Hilfe von Typ-Regeln 60 / 184 Typ-Systeme Formal betrachten wir Aussagen der Form: `e : == (In der Typ-Umgebung t hat e den Typ t) Axiome: Const: Var: ` c : tc ` x : (x ) (tc Typ der Konstante c) (x Variable) Regeln: Ref: `e : t ` &e : t Deref: ` e : t ` e : t 61 / 184 Typ-System für C-ähnliche Sprachen Weitere Regeln für diverse Typausdrücke: Array: Array: Struct: App: Op: Cast: ` e1 : t ` e2 : int ` e1 [e2 ] : t ` e1 : t [ ] ` e2 : int ` e1 [e2 ] : t ` e : struct ft1 a1 ; : : : tm am ; g ` e:ai : ti ` e : t (t1 ; : : : ; tm ) ` e1 : t1 : : : ` em : ` e(e1 ; : : : ; em ) : t ` e1 : int ` e2 : int ` e1 + e2 : int ` e : t1 t1 in t2 konvertierbar ` (t2 ) e : t2 tm 62 / 184 Beispiel: Typ-Prüfung Ausdruck *a[f(b->c)]+2 und =f struct list { int info; struct list* next; }; int f(struct list* l); struct { struct list* c;}* b; int* a[11]; g: + 2 [ ] ( ) a f : c b 63 / 184 Beispiel: Typ-Prüfung Ausdruck *a[f(b->c)]+2: + int int int [ ] 2 int [ ] ( ) a int (struct list ) int f int : struct fstruct list c;g struct fstruct list c;g b struct list c 64 / 184 Gleichheit von Typen Zusammenfassung Typprüfung: Welche Regel an einem Knoten angewendet werden muss, ergibt sich aus den Typen für die bereits bearbeiteten Kinderknoten Dazu muss die Gleichheit von Typen festgestellt werden. Typgleichheit in C: struct A {} und struct B {} werden als verschieden betrachtet ; der Compiler kann die Felder von A und B nach Bedarf umordnen (in C nicht erlaubt) um einen Verband A um weitere Felder zu erweitern, muss er eingebettet werden: typedef struct B { struct A a; int field_of_B; } extension_of_A; Nach typedef int C; haben C und int den gleichen Typ. 65 / 184 Strukturelle Typ-Gleichheit Alternative Interpretation von Gleichheit (gilt nicht in C): Semantisch können wir zwei rekursive Typen t1 ; t2 als gleich betrachten, falls sie die gleiche Menge von Pfaden zulassen. Beispiel: struct list { int info; struct list* next; } struct list1 { int info; struct { int info; struct list1* next; }* next; } Sei struct list* l oder struct list1* l. Beide erlauben l->info l->next->info jedoch hat l jeweils einen anderen Typen in C. 66 / 184 Algorithmus zum Test auf Semantische Gleichheit Idee: Verwalte Äquivalenz-Anfragen für je zwei Typausdrücke Sind die beiden Ausdrücke syntaktisch gleich, ist alles gut Andernfalls reduziere die Äquivalenz-Anfrage zwischen Äquivalenz-Anfragen zwischen (hoffentlich) einfacheren anderen Typausdrücken Nehmen wir an, rekursive Typen würden mit Hilfe von Typ-Gleichungen der Form: A=t eingeführt (verzichte auf ein ). Dann definieren wir folgende Regeln: 67 / 184 Regeln für Wohlgetyptheit t t s t A t A= s s struct fs1 a1 ; ... sm am ; g s1 t1 t s t struct ft1 a1 ; ... tm am ; g sm tm 68 / 184 Beispiel: A B = struct fint info; A next; g = struct fint info; struct fint info; B next; g next; g Wir fragen uns etwa, ob gilt: struct fint info; A next; g = B Dazu konstruieren wir: 69 / 184 Beweis Beispiel: A B = = f f f struct int info; A struct int info; struct int info; B structfint info; A next; g structfint info; A next; g int next; g next; g next; g B structfint info; : : : next; g A int A structfint info; A next; g int int ::: structfint info; B next; g structfint info; B next; g A B A B structfint info; A next; g B 70 / 184 Implementierung Stoßen wir bei der Konstruktion des Beweisbaums auf eine Äquivalenz-Anfrage, auf die keine Regel anwendbar ist, sind die Typen ungleich Die Konstruktion des Beweisbaums kann dazu führen, dass die gleiche Äquivalenz-Anfrage ein weiteres Mal auftritt Taucht eine Äquivalenz-Anfrage ein weiteres Mal auf, sind die Typen der Anfrage per Definition gleich Terminierung? 71 / 184 Implementierung Stoßen wir bei der Konstruktion des Beweisbaums auf eine Äquivalenz-Anfrage, auf die keine Regel anwendbar ist, sind die Typen ungleich Die Konstruktion des Beweisbaums kann dazu führen, dass die gleiche Äquivalenz-Anfrage ein weiteres Mal auftritt Taucht eine Äquivalenz-Anfrage ein weiteres Mal auf, sind die Typen der Anfrage per Definition gleich Terminierung? die Menge D aller deklarierten Typen ist endlich es gibt höchstens jDj2 viele Äquivalenzanfragen wiederholte Anfragen sind automatisch erfüllt ; Terminierung ist gesichert 71 / 184 Überladung und Koersion Manche Operatoren wie z.B. + sind überladen: + besitzt mehrere mögliche Typen Zum Beispiel: int +(int,int), float +(float, float) aber auch float* +(float*, int), int* +(int, int*) je nach Typ hat der Operator + ein unterschiedliche Implementation welche Implementierung ausgewählt wird, entscheiden die Argument-Typen 72 / 184 Überladung und Koersion Manche Operatoren wie z.B. + sind überladen: + besitzt mehrere mögliche Typen Zum Beispiel: int +(int,int), float +(float, float) aber auch float* +(float*, int), int* +(int, int*) je nach Typ hat der Operator + ein unterschiedliche Implementation welche Implementierung ausgewählt wird, entscheiden die Argument-Typen Koersion: Erlaube auch die Anwendung von + auf int und float. anstatt + für alle Argument-Kombinationen zu definieren, werden die Typen der Argumente konvertiert Konvertierung kann Code erzeugen (z.B. Umwandlung von int nach float) Konvertiert wird in der Regel auf die Supertypen, d.h. 5+0.5 hat Typ float (da float int) 72 / 184 Koersion von Integer-Typen in C: Promotion C enthält spezielle Koersionsregeln für Integer: Promotion unsigned char signed char ::: unsigned short signed short int unsigned int wobei eine Konvertierung über alle Zwischentypen gehen muss. 73 / 184 Koersion von Integer-Typen in C: Promotion C enthält spezielle Koersionsregeln für Integer: Promotion unsigned char signed char ::: unsigned short signed short int unsigned int wobei eine Konvertierung über alle Zwischentypen gehen muss. Subtile Fehler möglich! Berechne Zeichenverteilung in char* str: char* str = "..."; int dist[256]; memset(dist, 0, sizeof(dist)); while (*str) { dist[(unsigned) *str]++; str++; }; Beachte: unsigned bedeutet unsigned int. 73 / 184 Teiltypen Auf den arithmetischen Basistypen char, int, long, etc. gibt es i.a. eine reichhaltige Teiltypen-Beziehungen. Dabei bedeutet t1 t2 , dass die Menge der Werte vom Typ t1 1 2 3 eine Teilmenge der Werte vom Typ t2 sind; in einen Wert vom Typ t2 konvertiert werden können; die Anforderungen an Werte vom Typ t2 erfüllen. 74 / 184 Teiltypen Auf den arithmetischen Basistypen char, int, long, etc. gibt es i.a. eine reichhaltige Teiltypen-Beziehungen. Dabei bedeutet t1 t2 , dass die Menge der Werte vom Typ t1 1 2 3 eine Teilmenge der Werte vom Typ t2 sind; in einen Wert vom Typ t2 konvertiert werden können; die Anforderungen an Werte vom Typ t2 erfüllen. Erweitere Teiltypen-Beziehungen der Basistypen auf komplexe Typen 74 / 184 Beispiel: Teiltypen Betrachte: string extractInfo( struct { string info; } x) { return x.info; } Wir möchten dass extractInfo für alle Argument-Strukturen funktioniert, die eine Komponente string info besitzen Wann t1 t2 gelten soll, beschreiben wir durch Regeln Die Idee ist vergleichbar zur Anwendbarkeit auf Unterklassen (aber allgemeiner) 75 / 184 Regeln für Wohlgetyptheit von Teiltypen t t s t A t A= s s t s t struct fs1 a1 ; ... sm am ; g struct ftj1 aj1 ; ... tjk ajk ; g s1 tj1 sm tjk 76 / 184 Regeln und Beispiel für Teiltypen s0 (s1 ; : : : ; sm ) s0 t0 t1 s1 t0 (t1 ; : : : ; tm ) tm sm Beispiele: struct fint a; int b; g int (int) int (float) struct ffloat a; g float (float) float (int) 77 / 184 Regeln und Beispiel für Teiltypen s0 (s1 ; : : : ; sm ) s0 t0 t1 s1 t0 (t1 ; : : : ; tm ) tm sm Beispiele: struct fint a; int b; g int (int) int (float) struct ffloat a; g float (float) float (int) Achtung: 77 / 184 Regeln und Beispiel für Teiltypen s0 (s1 ; : : : ; sm ) s0 t0 t0 (t1 ; : : : ; tm ) t1 s1 tm sm Beispiele: struct fint a; int b; g int (int) int (float) 6 struct ffloat a; g float (float) float (int) Achtung: Bei Funktionen gilt: Rückgabewerte sind in normaler Teiltypen-Beziehung bei Argumenten dreht sich die Anordnung der Typen um 77 / 184 Ko- und Kontravarianz Definition Sei s0 (s1 ; : : : sn ) t0 (t1 ; : : : tn ) zwei Funktionstypen, in Teiltyp-Beziehung. Dann gilt: t0 Kontravarianz der Argumente si ti für 1 < i n Kovarianz der Rückgabewerte s0 78 / 184 Ko- und Kontravarianz Definition Sei s0 (s1 ; : : : sn ) t0 (t1 ; : : : tn ) zwei Funktionstypen, in Teiltyp-Beziehung. Dann gilt: t0 Kontravarianz der Argumente si ti für 1 < i n Kovarianz der Rückgabewerte s0 Betrachte Beispiel aus funktionalen Sprachen: int ! float ! int int ! int ! float 78 / 184 Ko- und Kontravarianz Definition Sei s0 (s1 ; : : : sn ) t0 (t1 ; : : : tn ) zwei Funktionstypen, in Teiltyp-Beziehung. Dann gilt: t0 Kontravarianz der Argumente si ti für 1 < i n Kovarianz der Rückgabewerte s0 Betrachte Beispiel aus funktionalen Sprachen: int ! float ! int int ! int ! float Diese Regeln können wir direkt benutzen, um auch für rekursive Typen die Teiltyp-Relation zu entscheiden 78 / 184 Teiltypen: Anwendung der Regeln (I) Prüfe ob S1 R1 : R1 S1 R2 S2 = = = = struct fint a; struct fint a; struct fint a; struct fint a; R1 (R1 ) f ; g int b; S1 (S1 ) f ; g R2 (S2 ) f ; g int b; S2 (R2 ) f ; g S1 R1 a int int f S1 (S1 ) R1 (R1 ) S1 R1 R1 S1 79 / 184 Teiltypen: Anwendung der Regeln (II) Prüfe ob S2 S1 : R1 S1 R2 S2 = = = = struct struct struct struct fint a; R1 (R1 ) f ; g fint a; int b; S1 (S1 ) f ; g fint a; R2 (S2 ) f ; g fint a; int b; S2 (R2 ) f ; g S2 S1 a, b int int f S2 (R2 ) S1 (S1 ) S2 S1 S1 R2 a int int f S1 (S1 ) R2 (S2 ) S1 R2 S2 S1 80 / 184 Teiltypen: Anwendung der Regeln (III) Prüfe ob S2 R1 : R1 S1 R2 S2 = = = = struct struct struct struct fint a; R1 (R1 ) f ; g fint a; int b; S1 (S1 ) f ; g fint a; R2 (S2 ) f ; g fint a; int b; S2 (R2 ) f ; g S 2 R1 a int int f S2 (R2 ) R1 (R1 ) S 2 R1 R1 R2 a int int f R1 (R1 ) R2 (S2 ) R1 R2 S2 R1 81 / 184 Diskussion Um die Beweisbäume nicht in den Himmel wachsen zu lassen, werden Zwischenknoten oft ausgelassen Strukturelle Teiltypen sind sehr mächtig und deshalb nicht ganz leicht zu durchschauen. Java verallgemeinert Strukturen zu Objekten / Klassen. Teiltyp-Beziehungen zwischen Klassen müssen explizit deklariert werden Durch Vererbung wird sichergestellt, dass Unterklassen über die (sichtbaren) Komponenten der Oberklasse verfügen Überdecken einer Komponente mit einer anderen gleichen Namens ist möglich — aber nur, wenn diese keine Methode ist 82 / 184 Themengebiet: Die Synthesephase 83 / 184 Codegenerierung: Überblick Vom AST des Programs erzeugen wir induktiv Instruktionen: für jedes Nicht-Terminal Symbol in der Grammatik gibt es eine Regel zur Generierung von Machineninstruktionen der Code ist nur ein weiteres Attribut im Syntaxbaum die Codegenerierung benutzt die schon berechneten Attribute 84 / 184 Codegenerierung: Überblick Vom AST des Programs erzeugen wir induktiv Instruktionen: für jedes Nicht-Terminal Symbol in der Grammatik gibt es eine Regel zur Generierung von Machineninstruktionen der Code ist nur ein weiteres Attribut im Syntaxbaum die Codegenerierung benutzt die schon berechneten Attribute Um die Codeerzeugung zu spezifizieren, benötigt man die Semantik der Ausgangssprache (C Standard) die Semantik der Maschineninstruktionen 84 / 184 Codegenerierung: Überblick Vom AST des Programs erzeugen wir induktiv Instruktionen: für jedes Nicht-Terminal Symbol in der Grammatik gibt es eine Regel zur Generierung von Machineninstruktionen der Code ist nur ein weiteres Attribut im Syntaxbaum die Codegenerierung benutzt die schon berechneten Attribute Um die Codeerzeugung zu spezifizieren, benötigt man die Semantik der Ausgangssprache (C Standard) die Semantik der Maschineninstruktionen ; Wir definieren zunächst die Machineninstruktionen 84 / 184 Die Synthesephase Kapitel 1: Die Register C-Maschine 85 / 184 Die Register C-Maschine (R-CMa) Wir erzeugen Code für die C-Maschine. Die Register C-Maschine ist eine virtuelle Machine. es existiert kein Prozessor, der die Instruktionen der CMA ausführen kann aber es steht ein Interpreter zur Verfügung es seht auch eine Visualisierungsumgebung zur Verfügung die R-CMa kennt keine double, float, char, short oder long Typen die R-CMa hat kennt keine Instruktionen um mit dem Betriebssystem zu kommunizieren (Eingabe/Ausgabe) die R-CMa hat eine unendliche Anzahl an Registern 86 / 184 Die Register C-Maschine (R-CMa) Wir erzeugen Code für die C-Maschine. Die Register C-Maschine ist eine virtuelle Machine. es existiert kein Prozessor, der die Instruktionen der CMA ausführen kann aber es steht ein Interpreter zur Verfügung es seht auch eine Visualisierungsumgebung zur Verfügung die R-CMa kennt keine double, float, char, short oder long Typen die R-CMa hat kennt keine Instruktionen um mit dem Betriebssystem zu kommunizieren (Eingabe/Ausgabe) die R-CMa hat eine unendliche Anzahl an Registern Die R-CMa ist realistischer als es auf den ersten Blick erscheint: die Einschränkungen können mit wenig Aufwand aufgehoben werden die Java Virtual Machine (JVM) ist sehr ähnlich zur R-CMa ohne Register ein Interpreter für die R-CMa läuft auf jeder Platform 86 / 184 Virtuelle Maschinen Ein virtuelle Maschine definiert sich wie folgt: Jede virtuelle Maschine stellt einen Satz von Instruktionen zur Verfügung. Instruktionen werden auf der virtuellen Hardware ausgeführt. Die virtuelle Hardware ist eine Menge von Datenstrukturen, auf die die Instruktionen zugreifen ... und die vom Laufzeitsystem verwaltet werden. der Interpreter ist Teil des Laufzeitsystems 87 / 184 Komponenten einer virtuellen Maschine Beispiel für Java: C 0 1 PC 0 SP S Eine virtuelle Maschine wie die JVM hat folgende Strukturen: S: (Daten-)Speicher, auf dem nach dem LIFO-Prinzip neue Zellen allokiert werden können Keller/Stack. SP: (= b Stack Pointer) oberste belegte Zelle in S am oberen Ende von S liegt der Heap ; 88 / 184 Komponenten einer virtuellen Maschine Beispiel für Java: C 0 1 PC 0 SP S Eine virtuelle Maschine wie die JVM hat folgende Strukturen: S: (Daten-)Speicher, auf dem nach dem LIFO-Prinzip neue Zellen allokiert werden können Keller/Stack. SP: (= b Stack Pointer) oberste belegte Zelle in S am oberen Ende von S liegt der Heap C ist der Code-Speicher ; Jede Zelle von C kann exakt einen virtuellen Befehl aufnehmen C kann nur gelesen werden PC (= b Program Counter) Adresse des nächsten auszuführenden Befehls PC enthält zu Begin 0 88 / 184 Die Ausführung von Programmen Die Maschine lädt die Instruktion aus C[PC] in ein Instruktions-Register IR und führt sie aus. Vor der Ausführung eines Befehls wird der PC um 1 erhöht. while (true) { IR = C[PC]; PC++; execute (IR); } Der PC muss vor der Ausführung der Instruktion erhöht werden, da diese möglicherweise den PC überschreibt Die Schleife wird durch Ausführung der Instruktion halt verlassen, die die Kontrolle an das Betriebssystem zurückgibt 89 / 184 Die Synthesephase Kapitel 2: Ausdrucksauswertung 90 / 184 Einfache Ausdrücke und Wertzuweisungen Aufgabe: werte den Ausdruck (1 + 7) 3 aus Das heißt: erzeuge eine Instruktionsfolge, die den Wert des Ausdrucks ermittelt und dann oben auf dem Keller ablegt 91 / 184 Einfache Ausdrücke und Wertzuweisungen Aufgabe: werte den Ausdruck (1 + 7) 3 aus Das heißt: erzeuge eine Instruktionsfolge, die den Wert des Ausdrucks ermittelt und dann oben auf dem Keller ablegt Idee: berechne erst die Werte für die Teilausdrücke; merke diese Zwischenergebnisse oben auf dem Keller; wende dann den Operator an 91 / 184 Generelles Prinzip die Argumente für Instruktionen werden oben auf dem Keller erwartet; die Ausführung einer Instruktion konsumiert ihre Argumente; möglicherweise berechnete Ergebnisse werden oben auf dem Keller wieder abgelegt. iconst q q SP++; S[SP] = q; Die Instruktion iconst q legt die int-Konstante q auf dem Stack ab. 92 / 184 Binäre Operatoren Operatoren mit zwei Argumenten werden wie folgt angewandt: 3 8 24 imul SP--; S[SP] = S[SP] S[SP+1]; 93 / 184 Binäre Operatoren Operatoren mit zwei Argumenten werden wie folgt angewandt: 3 8 24 imul SP--; S[SP] = S[SP] S[SP+1]; imul erwartet zwei Argumente oben auf dem Stack, konsumiert sie und legt sein Ergebnis oben auf dem Stack ab. 93 / 184 Binäre Operatoren Operatoren mit zwei Argumenten werden wie folgt angewandt: 3 8 24 imul SP--; S[SP] = S[SP] S[SP+1]; imul erwartet zwei Argumente oben auf dem Stack, konsumiert sie und legt sein Ergebnis oben auf dem Stack ab. analog arbeiten auch die übrigen binären arithmetischen und logischen Instruktionen iadd, isub, idiv, imod, etc. 93 / 184 Zusammensetzung von Instruktionen Beispiel: Erzeuge Code für 1 + 7: iconst 1 iconst 7 iadd Ausführung dieses Codes: iconst 1 1 iconst 7 7 1 iadd 8 94 / 184 Ausdrücke mit Variablen Variablen haben eine Speicherzellen in S: z: y: x: 95 / 184 Ausdrücke mit Variablen Variablen haben eine Speicherzellen in S: z: y: x: Die Zuordnung von Adressen zu Variablen kann während der Erstellung der Symboltabelle erfolgen. Die Adresse wird an dem Deklarationsknoten der Variable gespeichert. 95 / 184 Ausdrücke mit Variablen Variablen haben eine Speicherzellen in S: z: y: x: Die Zuordnung von Adressen zu Variablen kann während der Erstellung der Symboltabelle erfolgen. Die Adresse wird an dem Deklarationsknoten der Variable gespeichert. Jede Benutzung einer Variable hat als Attribut einen Zeiger auf den Deklarationsknoten der Variable. 95 / 184 Ausdrücke mit Variablen Variablen haben eine Speicherzellen in S: z: y: x: Die Zuordnung von Adressen zu Variablen kann während der Erstellung der Symboltabelle erfolgen. Die Adresse wird an dem Deklarationsknoten der Variable gespeichert. Jede Benutzung einer Variable hat als Attribut einen Zeiger auf den Deklarationsknoten der Variable. Im folgenden benutzen wir Übersetzungsfunktionen die eine Funktion nehmen, die für jede Variable x die (Relativ-)Adresse von x liefert. Die Funktion heißt Adress-Umgebung (Address Environment). 95 / 184 Lesen von Variablen Die Instruktion iload k lädt den Wert der Speicherzelle k, relativ zur Spitze vom Stapel. iload k 13 13 13 S[SP+1] = S[SP-k]; SP = SP+1; Beispiel: Berechne x + 2 wobei = fx 7! 1g: iload 1 iconst 2 iadd 96 / 184 Die Synthesephase Kapitel 3: Codeerzeugung für die Register-C-Maschine 97 / 184 Motivation für die Register C-Maschine Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von Universalregistern. 98 / 184 Motivation für die Register C-Maschine Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von Universalregistern. arithmetische Operationen können oft nur auf diesen Registern ausgeführt werden Zugriffe auf den Speicher werden durch Adressen in Registern vorgenommen Register müssen gesichert werden, wenn Funktionen aufgerufen werden 98 / 184 Motivation für die Register C-Maschine Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von Universalregistern. arithmetische Operationen können oft nur auf diesen Registern ausgeführt werden Zugriffe auf den Speicher werden durch Adressen in Registern vorgenommen Register müssen gesichert werden, wenn Funktionen aufgerufen werden Die Übersetzung von Code für einen solchen Prozessor muss: 98 / 184 Motivation für die Register C-Maschine Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von Universalregistern. arithmetische Operationen können oft nur auf diesen Registern ausgeführt werden Zugriffe auf den Speicher werden durch Adressen in Registern vorgenommen Register müssen gesichert werden, wenn Funktionen aufgerufen werden Die Übersetzung von Code für einen solchen Prozessor muss: 1 Variablen und Funktionsargumente in Registern speichern 2 während eines Funktionsaufrufes die entsprechenden Register auf dem Keller sichern 3 beliebige Berechnungen mittels endlich vieler Register ausdrücken 98 / 184 Motivation für die Register C-Maschine Ein moderner RISC-Prozessor besitzt eine fixe Anzahl von Universalregistern. arithmetische Operationen können oft nur auf diesen Registern ausgeführt werden Zugriffe auf den Speicher werden durch Adressen in Registern vorgenommen Register müssen gesichert werden, wenn Funktionen aufgerufen werden Die Übersetzung von Code für einen solchen Prozessor muss: 1 Variablen und Funktionsargumente in Registern speichern 2 während eines Funktionsaufrufes die entsprechenden Register auf dem Keller sichern 3 beliebige Berechnungen mittels endlich vieler Register ausdrücken ; betrachte nur die ersten zwei Punkte (und behandle den letzten Punkt als separates Problem) 98 / 184 Prinzip der Register C-Maschine Die R-CMa besitzt einen Stack/Heap und ein Code-Segment, wie die JVM, und zusätzlich zwei unbegrenzte Mengen an Registern. lokale Register sind R1 ; R2 ; : : : Ri ; : : : globale Register sind R0 ; R 1 ; : : : Rj ; : : : C 0 1 PC 0 SP S Rloc R1 R6 Rglob R0 R 4 99 / 184 Die Registersätze der R-CMa Die zwei Registersätze haben die folgenden Aufgaben: 1 die lokalen Register Ri speichern temporäre Zwischenergebnisse speichern lokale Variablen einer Funktion können effizient auf den Stack gerettet werden 100 / 184 Die Registersätze der R-CMa Die zwei Registersätze haben die folgenden Aufgaben: 1 die lokalen Register Ri speichern temporäre Zwischenergebnisse speichern lokale Variablen einer Funktion können effizient auf den Stack gerettet werden 2 die globalen Register Ri speichern Parameter einer Funktion speichern den Rückgabewert einer Funktion 100 / 184 Die Registersätze der R-CMa Die zwei Registersätze haben die folgenden Aufgaben: 1 die lokalen Register Ri speichern temporäre Zwischenergebnisse speichern lokale Variablen einer Funktion können effizient auf den Stack gerettet werden 2 die globalen Register Ri speichern Parameter einer Funktion speichern den Rückgabewert einer Funktion Achtung: zunächst benutzten wir Register nur für Zwischenergebnisse 100 / 184 Die Registersätze der R-CMa Die zwei Registersätze haben die folgenden Aufgaben: 1 die lokalen Register Ri speichern temporäre Zwischenergebnisse speichern lokale Variablen einer Funktion können effizient auf den Stack gerettet werden 2 die globalen Register Ri speichern Parameter einer Funktion speichern den Rückgabewert einer Funktion Achtung: zunächst benutzten wir Register nur für Zwischenergebnisse Idee für Übersetzung: benutze Registerzähler i: Register Rj mit j < i sind in Benutzung Register Rj mit j i stehen zur Verfügung 100 / 184 Übersetzen von einfachen Anweisungen Behandlung von Variablen in Registern; laden von Konstanten: Instruktion loadc Ri c move Ri Rj Semantik Intuition Ri = c lade Konstante Ri = Rj kopiere Rj nach Ri 101 / 184 Übersetzen von einfachen Anweisungen Behandlung von Variablen in Registern; laden von Konstanten: Instruktion loadc Ri c move Ri Rj Semantik Intuition Ri = c lade Konstante Ri = Rj kopiere Rj nach Ri Wir definieren folgende Übersetzungsschemata (mit x = a): codeiR c codeiR codeiR x x =e = loadc Ri c = move Ri Ra = codeiR e move Ra Ri 101 / 184 Übersetzen von einfachen Anweisungen Behandlung von Variablen in Registern; laden von Konstanten: Instruktion loadc Ri c move Ri Rj Semantik Intuition Ri = c lade Konstante Ri = Rj kopiere Rj nach Ri Wir definieren folgende Übersetzungsschemata (mit x = a): codeiR c codeiR codeiR x x =e = loadc Ri c = move Ri Ra = codeiR e move Ra Ri Beachte: alle Instruktionen benutzen die Intel-Konvention (im Ggs. zur AT&T Konvention): op dst src1 : : : srcn . 101 / 184 Übersetzen von Ausdrücken Sei op = fadd; sub; div; mul; mod; le; gr; eq; leq; geq; and; org. Die R-CMa kennt für jeden Operator op eine Instruktion op Ri Rj Rk wobei Ri das Zielregister, Rj das erste und Rk das zweite Argument ist. Entsprechend erzeugen wir den Code wie folgt: codeiR e1 op e2 = codeiR e1 codeRi+1 e2 op Ri Ri Ri+1 102 / 184 Übersetzen von Ausdrücken Sei op = fadd; sub; div; mul; mod; le; gr; eq; leq; geq; and; org. Die R-CMa kennt für jeden Operator op eine Instruktion op Ri Rj Rk wobei Ri das Zielregister, Rj das erste und Rk das zweite Argument ist. Entsprechend erzeugen wir den Code wie folgt: codeiR e1 op e2 = codeiR e1 codeRi+1 e2 op Ri Ri Ri+1 Beispiel: Übersetze 3*4 mit i = 4: code4R 3*4 = code4R 3 code5R 4 mul R4 R4 R5 102 / 184 Übersetzen von Ausdrücken Sei op = fadd; sub; div; mul; mod; le; gr; eq; leq; geq; and; org. Die R-CMa kennt für jeden Operator op eine Instruktion op Ri Rj Rk wobei Ri das Zielregister, Rj das erste und Rk das zweite Argument ist. Entsprechend erzeugen wir den Code wie folgt: codeiR e1 op e2 = codeiR e1 codeRi+1 e2 op Ri Ri Ri+1 Beispiel: Übersetze 3*4 mit i = 4: code4R 3*4 = loadc R4 3 loadc R5 4 mul R4 R4 R5 102 / 184 Verwaltung temporärer Register Beachte, dass temporäre Register wiederverwendet werden: Übersetze 3*4+3*4 mit t = 4: code4R 3*4+3*4 = code4R 3*4 code5R 3*4 add R4 R4 R5 mit codeiR 3*4 = loadc Ri 3 loadc Ri+1 4 mul Ri Ri Ri+1 ergibt sich code4R 3*4+3*4 = 103 / 184 Verwaltung temporärer Register Beachte, dass temporäre Register wiederverwendet werden: Übersetze 3*4+3*4 mit t = 4: code4R 3*4+3*4 = code4R 3*4 code5R 3*4 add R4 R4 R5 mit codeiR 3*4 = loadc Ri 3 loadc Ri+1 4 mul Ri Ri Ri+1 ergibt sich code4R 3*4+3*4 = loadc R4 3 loadc R5 4 mul R4 R4 R5 loadc R5 3 loadc R6 4 mul R5 R5 R6 add R4 R4 R5 103 / 184 Semantik der Operatoren Die Operatoren haben die folgende Semantik: add Ri Rj Rk sub Ri Rj Rk div Ri Rj Rk mul Ri Rj Rk mod Ri Rj Rk le Ri Rj Rk gr Ri Rj Rk eq Ri Rj Rk leq Ri Rj Rk geq Ri Rj Rk and Ri Rj Rk or Ri Rj Rk Ri = Rj + Rk Ri = Rj Rk Ri = Rj =Rk Ri = Rj Rk Ri = sgn(Rk )k wobei jRj j = njRk j + k ^ n 0; 0 k < jRk j Ri = if Rj < Rk then 1 else 0 Ri = if Rj > Rk then 1 else 0 Ri = if Rj = Rk then 1 else 0 Ri = if Rj Rk then 1 else 0 Ri = if Rj Rk then 1 else 0 Ri = Rj & Rk // bit-wise and Ri = Rj j Rk // bit-wise or 104 / 184 Semantik der Operatoren Die Operatoren haben die folgende Semantik: add Ri Rj Rk sub Ri Rj Rk div Ri Rj Rk mul Ri Rj Rk mod Ri Rj Rk le Ri Rj Rk gr Ri Rj Rk eq Ri Rj Rk leq Ri Rj Rk geq Ri Rj Rk and Ri Rj Rk or Ri Rj Rk Ri = Rj + Rk Ri = Rj Rk Ri = Rj =Rk Ri = Rj Rk Ri = sgn(Rk )k wobei jRj j = njRk j + k ^ n 0; 0 k < jRk j Ri = if Rj < Rk then 1 else 0 Ri = if Rj > Rk then 1 else 0 Ri = if Rj = Rk then 1 else 0 Ri = if Rj Rk then 1 else 0 Ri = if Rj Rk then 1 else 0 Ri = Rj & Rk // bit-wise and Ri = Rj j Rk // bit-wise or Beachte: alle Register und Speicherzellen enthalten Zahlen in Z 104 / 184 Übersetzung unärer Operatoren Die unären Operatoren op = fneg; notg nehmen zwei Register: codeiR op e = codeiR e op Ri Ri 105 / 184 Übersetzung unärer Operatoren Die unären Operatoren op = fneg; notg nehmen zwei Register: codeiR op e = codeiR e op Ri Ri Beachte: Wir verwenden dasselbe Register. 105 / 184 Übersetzung unärer Operatoren Die unären Operatoren op = fneg; notg nehmen zwei Register: codeiR op e = codeiR e op Ri Ri Beachte: Wir verwenden dasselbe Register. Beispiel: Übersetze -4 nach R5 : code5R -4 = code5R 4 neg R5 R5 105 / 184 Übersetzung unärer Operatoren Die unären Operatoren op = fneg; notg nehmen zwei Register: codeiR op e = codeiR e op Ri Ri Beachte: Wir verwenden dasselbe Register. Beispiel: Übersetze -4 nach R5 : code5R -4 = loadc R5 4 neg R5 R5 105 / 184 Übersetzung unärer Operatoren Die unären Operatoren op = fneg; notg nehmen zwei Register: codeiR op e = codeiR e op Ri Ri Beachte: Wir verwenden dasselbe Register. Beispiel: Übersetze -4 nach R5 : code5R -4 = loadc R5 4 neg R5 R5 Die Operatoren haben die folgende Semantik: not Ri Rj neg Ri Rj Ri Ri if Rj Rj = 0 then 1 else 0 105 / 184 Beispiel: Übersetzungschema für Ausdrücke void f(void) { int x,y,z; x = y+z*3; } Sei Adressumgebung = fx 7! 1; y 7! 2; z 7! 3g gegeben. Sei R4 das erste freie Register, d.h. i = 4. Sei folgende Funktion gegeben: code4 x=y+z*3 = code4R y+z*3 move R1 R4 106 / 184 Beispiel: Übersetzungschema für Ausdrücke void f(void) { int x,y,z; x = y+z*3; } Sei Adressumgebung = fx 7! 1; y 7! 2; z 7! 3g gegeben. Sei R4 das erste freie Register, d.h. i = 4. Sei folgende Funktion gegeben: code4 x=y+z*3 = code4R y+z*3 move R1 R4 code4R y+z*3 = move R4 R2 code5R z*3 add R4 R4 R5 106 / 184 Beispiel: Übersetzungschema für Ausdrücke void f(void) { int x,y,z; x = y+z*3; } Sei Adressumgebung = fx 7! 1; y 7! 2; z 7! 3g gegeben. Sei R4 das erste freie Register, d.h. i = 4. Sei folgende Funktion gegeben: code4 x=y+z*3 = code4R y+z*3 move R1 R4 code4R y+z*3 code5R z*3 = move R4 R2 code5R z*3 add R4 R4 R5 = move R5 R3 code6R 3 mul R5 R5 R6 106 / 184 Beispiel: Übersetzungschema für Ausdrücke void f(void) { int x,y,z; x = y+z*3; } Sei Adressumgebung = fx 7! 1; y 7! 2; z 7! 3g gegeben. Sei R4 das erste freie Register, d.h. i = 4. Sei folgende Funktion gegeben: code4 x=y+z*3 = code4R y+z*3 move R1 R4 code4R y+z*3 = move R4 R2 code5R z*3 add R4 R4 R5 code5R z*3 code6R 3 = move R5 R3 code6R 3 mul R5 R5 R6 = loadc R6 3 106 / 184 Beispiel: Übersetzungschema für Ausdrücke void f(void) { int x,y,z; x = y+z*3; } Sei Adressumgebung = fx 7! 1; y 7! 2; z 7! 3g gegeben. Sei R4 das erste freie Register, d.h. i = 4. Sei folgende Funktion gegeben: code4 x=y+z*3 = code4R y+z*3 move R1 R4 code4R y+z*3 code5R z*3 add R4 R4 R5 code5R z*3 ; = move R4 R2 code6R 3 = move R5 R3 code6R 3 mul R5 R5 R6 = loadc R6 3 die Zuweisung x=y+z*3 wird übersetzt als move R4 R2 ; move R5 R3 ; loadc R6 3; mul R5 R5 R6 ; add R4 R4 R5 ; move R1 R4 106 / 184 Die Synthesephase Kapitel 4: Anweisungen und Kontrollstrukturen 107 / 184 Über Anweisungen und Ausdrücke Allgemeine Übersetzungsidee: codei s : geniere Kode für Anweisung s codeiR e : generiere Kode für Ausdruck e in Ri Es gilt: i; i + 1; : : : sind unbenutzte Register 108 / 184 Über Anweisungen und Ausdrücke Allgemeine Übersetzungsidee: codei s : geniere Kode für Anweisung s codeiR e : generiere Kode für Ausdruck e in Ri Es gilt: i; i + 1; : : : sind unbenutzte Register Für den Ausdruck x = e mit x = a hatten wir definiert: codeiR x = e = codeiR e move Ra Ri Allerdings ist x = e auch eine Anweisung: 108 / 184 Über Anweisungen und Ausdrücke Allgemeine Übersetzungsidee: codei s : geniere Kode für Anweisung s codeiR e : generiere Kode für Ausdruck e in Ri Es gilt: i; i + 1; : : : sind unbenutzte Register Für den Ausdruck x = e mit x = a hatten wir definiert: codeiR x = e = codeiR e move Ra Ri Allerdings ist x = e auch eine Anweisung: Definiere: codei e1 = e2 = codeiR e1 = e2 Das temporäre Register Ri wird dabei ignoriert. Allgemeiner: codei e = codeiR e 108 / 184 Über Anweisungen und Ausdrücke Allgemeine Übersetzungsidee: codei s : geniere Kode für Anweisung s codeiR e : generiere Kode für Ausdruck e in Ri Es gilt: i; i + 1; : : : sind unbenutzte Register Für den Ausdruck x = e mit x = a hatten wir definiert: codeiR x = e = codeiR e move Ra Ri Allerdings ist x = e auch eine Anweisung: Definiere: codei e1 = e2 = codeiR e1 = e2 Das temporäre Register Ri wird dabei ignoriert. Allgemeiner: codei e = codeiR e Beobachtung: Die Zuweisung an e1 ist ein Seiteneffekt beim Auswerten des Ausdrucks e1 = e2 . 108 / 184 Übersetzung von Anweisungsfolgen Der Code für eine Anweisungsfolge ist die Konkatenation des Codes for die einzelnen Anweisungen in der Folge: codei (s ss) i code " = codei s = codei ss // leere Folge von Befehlen Beachte: s ist eine Anweisung, ss ist eine Folge von Anweisungen 109 / 184 Sprünge Um von linearer Ausführungsreihenfolge abzuweichen, benötigen wir Sprünge: jump A A PC PC PC = A; 110 / 184 Bedingte Sprünge Ein bedingter Sprung verzweigt je nach Wert in Ri : !0 Ri jumpz Ri A !0 Ri PC PC 0 Ri 0 Ri jumpz Ri A A PC PC if (Ri == 0) PC = A; 111 / 184 Verwaltung von Kontrollfluss Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge erzeugt werden. bei der Übersetung eines if (c) Konstrukts ist nicht klar, an welcher Adresse gesprungen werden muss, wenn c falsch ist 112 / 184 Verwaltung von Kontrollfluss Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge erzeugt werden. bei der Übersetung eines if (c) Konstrukts ist nicht klar, an welcher Adresse gesprungen werden muss, wenn c falsch ist Instruktionsfolgen können unterschiedlich arrangiert werden minimiere dabei die Anzahl der unbedingten Sprünge minimiere so dass weniger innerhalb Schleifen gesprungen wird ersetze weite Sprünge (far jumps) durch nahe Sprünge (near jumps) 112 / 184 Verwaltung von Kontrollfluss Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge erzeugt werden. bei der Übersetung eines if (c) Konstrukts ist nicht klar, an welcher Adresse gesprungen werden muss, wenn c falsch ist Instruktionsfolgen können unterschiedlich arrangiert werden minimiere dabei die Anzahl der unbedingten Sprünge minimiere so dass weniger innerhalb Schleifen gesprungen wird ersetze weite Sprünge (far jumps) durch nahe Sprünge (near jumps) organisiere die Instruktionen in Blöcke ohne Sprünge 112 / 184 Verwaltung von Kontrollfluss Zur Übersetzung von Kontrollfluss Anweisungen müssen Sprünge erzeugt werden. bei der Übersetung eines if (c) Konstrukts ist nicht klar, an welcher Adresse gesprungen werden muss, wenn c falsch ist Instruktionsfolgen können unterschiedlich arrangiert werden minimiere dabei die Anzahl der unbedingten Sprünge minimiere so dass weniger innerhalb Schleifen gesprungen wird ersetze weite Sprünge (far jumps) durch nahe Sprünge (near jumps) organisiere die Instruktionen in Blöcke ohne Sprünge Dazu definieren wir: Definition Ein Basisblock (basic block) besteht aus einer Folge von Anweisungen ss, die keine Sprünge enthält einer ausgehenden Menge von Kanten zu anderen Basisblöcken wobei jede Kante mit einer Bedingung annotiert sein kann 112 / 184 Basisblöcke in der Register C-Maschine Die R-CMa verfügt nur über einen bedingten Sprung, jumpz. code ss c Die ausgehenden Kanten haben eine spezielle Form: 113 / 184 Basisblöcke in der Register C-Maschine Die R-CMa verfügt nur über einen bedingten Sprung, jumpz. code ss c Die ausgehenden Kanten haben eine spezielle Form: 1 eine Kante (unbedingter Sprung), mit jump übersetzt 113 / 184 Basisblöcke in der Register C-Maschine Die R-CMa verfügt nur über einen bedingten Sprung, jumpz. code ss c Die ausgehenden Kanten haben eine spezielle Form: 1 2 eine Kante (unbedingter Sprung), mit jump übersetzt eine Kante mit Bedingung c = 0 (jumpz), eine Kante ohne Bedinung (jump) 113 / 184 Basisblöcke in der Register C-Maschine Die R-CMa verfügt nur über einen bedingten Sprung, jumpz. code ss c Die ausgehenden Kanten haben eine spezielle Form: 1 2 3 eine Kante (unbedingter Sprung), mit jump übersetzt eine Kante mit Bedingung c = 0 (jumpz), eine Kante ohne Bedinung (jump) eine Liste von Kanten (jumpi) und eine default Kante (jump); wird verwendet für switch Anweisungen (kommt später) 113 / 184 Beschreibung von Kontrollfluss Übersetzungen Der Einfachheit halber verwenden wir symbolische Sprungziele zur Beschreibung der Übersetzung von Kontrollflussinstruktionen. Ein zweiten Durchlauf ist nötig, um symbolische Adressen durch absolute Code-Adressen zu ersetzen. 114 / 184 Beschreibung von Kontrollfluss Übersetzungen Der Einfachheit halber verwenden wir symbolische Sprungziele zur Beschreibung der Übersetzung von Kontrollflussinstruktionen. Ein zweiten Durchlauf ist nötig, um symbolische Adressen durch absolute Code-Adressen zu ersetzen. Alternativ könnten direkt relative Sprünge eingeführt werden: relative Sprünge haben Ziele relative zum aktuellen PC ; near jumps) oft reichen kleinere Adressen aus ( der Code wird relokierbar, d. h. kann im Speicher unverändert hin und her geschoben werden (position independent code, PIC) Der erzeugte Code wäre im Prinzip direkt ausführbar. 114 / 184 Beschreibung von Kontrollfluss Übersetzungen Der Einfachheit halber verwenden wir symbolische Sprungziele zur Beschreibung der Übersetzung von Kontrollflussinstruktionen. Ein zweiten Durchlauf ist nötig, um symbolische Adressen durch absolute Code-Adressen zu ersetzen. Alternativ könnten direkt relative Sprünge eingeführt werden: relative Sprünge haben Ziele relative zum aktuellen PC ; near jumps) oft reichen kleinere Adressen aus ( der Code wird relokierbar, d. h. kann im Speicher unverändert hin und her geschoben werden (position independent code, PIC) Der erzeugte Code wäre im Prinzip direkt ausführbar. Basisböcke haben den Vorteil, dass sie später zur Programmoptimierung weiterverwendet werden können. (Z.B. Reduzierung der Anzahl an Sprüngen.) 114 / 184 Bedingte Anweisung, einseitig Betrachten wir zuerst s if ( c ) ss. Wir beschreiben die Code-Erzeugung zunächst ohne Basic-Blöcke. Idee: Lege den Code zur Auswertung von c und ss hintereinander in den Code-Speicher; Füge Sprung-Befehlen so ein, dass ein korrekter Kontroll-Fluss gewährleistet ist codei s = codeiR c jumpz Ri A codei ss A: ::: code R für c jumpz code für ss 115 / 184 Bedingte Anweisung, zweiseitig code c code code tt ee Übersetzung von if ( c ) tt else ee. codei if(c) tt else ee code R für c = codeiR c jumpz Ri A codei tt jump B A: B: jumpz code für tt jump i code ee code für ee 116 / 184 Beispiel für if-Anweisung Sei = fx 7! 4; y 7! 7g und s die Anweisung: if (x>y) { x = x - y; } else { y = y - x; } /* (i) */ /* (ii) */ /* (iii) */ Dann liefert codei s : 117 / 184 Beispiel für if-Anweisung Sei = fx 7! 4; y 7! 7g und s die Anweisung: if (x>y) { x = x - y; } else { y = y - x; } /* (i) */ /* (ii) */ /* (iii) */ Dann liefert codei s : (i) (ii) move Ri R4 move Ri R4 move Ri+1 R7 move Ri+1 R7 gr Ri Ri Ri+1 jumpz Ri A sub Ri Ri Ri+1 (iii) A : move Ri R7 move Ri+1 R4 sub Ri Ri Ri+1 move R4 Ri jump B move R7 Ri B: 117 / 184 Iterative Anweisungen Betrachte schließlich die Schleife s while (e) s0 . Dafür erzeugen wir: codei while(e) s = A : codeiR e jumpz Ri B codei s jump A B: code R für e jumpz code für s’ jump 118 / 184 Beispiel: Übersetzung von Schleifen Sei = fa 7! 7; b 7! 8; c 7! 9g und s die Anweisung: while (a>0) { c = c + 1; a = a - b; } /* (i) */ /* (ii) */ /* (iii) */ Dann liefert codei s die Folge: 119 / 184 Beispiel: Übersetzung von Schleifen Sei = fa 7! 7; b 7! 8; c 7! 9g und s die Anweisung: while (a>0) { c = c + 1; a = a - b; } /* (i) */ /* (ii) */ /* (iii) */ Dann liefert codei s die Folge: (i) A: (ii) (iii) move Ri R7 move Ri R9 move Ri R7 loadc Ri+1 0 loadc Ri+1 1 move Ri+1 R8 gr Ri Ri Ri+1 add Ri Ri Ri+1 jumpz Ri B move R9 Ri sub Ri Ri Ri+1 move R7 Ri jump A B: 119 / 184 for-Schleifen Die for-Schleife s for (e1 ; e2 ; e3 ) s0 ist äquivalent zu der Statementfolge e1 ; while (e2 ) fs0 e3 ; g – sofern s0 keine continue-Anweisung enthält. Darum übersetzen wir: codei for(e1 ; e2 ; e3 ) s = codeiR e1 A : codeiR e2 jumpz Ri B codei s codeiR e3 jump A B: 120 / 184 Das switch-Statement Idee: Unterstütze Mehrfachverzweigung in konstanter Zeit Benutze Sprungtabelle, die an der i-ten Stelle den Sprung an den Anfang der i-tem Alternative enthält. Eine Möglichkeit zur Realisierung besteht in der Einführung von indizierten Sprüngen. q Ri jumpi Ri A q Ri B A+q PC PC PC = A + Ri ; 121 / 184 Aufeinanderfolgende Alternativen Sei ein switch s mit k aufeinanderfolgenden case Fällen gegeben: switch (e) { case c0 : s0 ; break; . . . case ck 1 : sk 1 ; break; default: s; break; } d.h. ci + 1 = ci+1 für i = [0; k 1]. 122 / 184 Aufeinanderfolgende Alternativen Sei ein switch s mit k aufeinanderfolgenden case Fällen gegeben: switch (e) { case c0 : s0 ; break; . . . case ck 1 : sk 1 ; break; default: s; break; } d.h. ci + 1 = ci+1 für i = [0; k 1]. Definiere codei s wie folgt: codei s = codeiR e codei s0 B: .. . jump D .. . C: checki c0 ck A0 : .. . Ak 1 : codei sk 1 B jump A0 .. . jump Ak 1 1 jump D 122 / 184 Aufeinanderfolgende Alternativen Sei ein switch s mit k aufeinanderfolgenden case Fällen gegeben: switch (e) { case c0 : s0 ; break; . . . case ck 1 : sk 1 ; break; default: s; break; } d.h. ci + 1 = ci+1 für i = [0; k 1]. Definiere codei s wie folgt: codei s = codeiR e codei s0 B: .. . jump D .. . C: checki c0 ck A0 : .. . Ak 1 : codei sk 1 B jump A0 .. . jump Ak 1 1 jump D checki l u B prüft ob l Ri < u gilt und springt entsprechend. 122 / 184 Übersetztung des checki Makros Das Makro checki l u B prüft ob l Ri < u. Sei k = u wenn l Ri < u, springt es nach B + Ri wenn Ri < l oder Ri u springt es nach C B: .. . l. l jump A0 .. . jump Ak 1 C: 123 / 184 Übersetztung des checki Makros Das Makro checki l u B prüft ob l Ri < u. Sei k = u wenn l Ri < u, springt es nach B + Ri wenn Ri < l oder Ri Wir definieren: checki l u B u springt es nach C = loadc Ri+1 l geq Ri+2 Ri Ri+1 jumpz Ri+2 E sub Ri Ri Ri+1 loadc Ri+1 k geq Ri+2 Ri Ri+1 jumpz Ri+2 D E : loadc Ri k D : jumpi Ri B B: .. . l. l jump A0 .. . jump Ak 1 C: 123 / 184 Übersetztung des checki Makros Das Makro checki l u B prüft ob l Ri < u. Sei k = u wenn l Ri < u, springt es nach B + Ri wenn Ri < l oder Ri Wir definieren: checki l u B u springt es nach C = loadc Ri+1 l geq Ri+2 Ri Ri+1 jumpz Ri+2 E sub Ri Ri Ri+1 loadc Ri+1 k geq Ri+2 Ri Ri+1 jumpz Ri+2 D E : loadc Ri k D : jumpi Ri B Beachte: ein Sprung jumpi Ri B mit Ri B: .. . l. l jump A0 .. . jump Ak 1 C: = k landet bei C. 123 / 184 Übersetzung der Sprungtabelle Dies ist nur eine Übersetzung für spezielle switch-Anweisungen. Beginnt die Tabelle mit 0 statt mit u, brauchen wir den R-Wert von e nicht vermindern, bevor wir ihn als Index benutzen. Sind sämtliche möglichen Werte von e sicher im Intervall [l; u], können wir auf check verzichten. Kann die Übersetzung von switch-Anweisungen als L-Attributierung implementiert werden? 124 / 184 Übersetzung der Sprungtabelle Dies ist nur eine Übersetzung für spezielle switch-Anweisungen. Beginnt die Tabelle mit 0 statt mit u, brauchen wir den R-Wert von e nicht vermindern, bevor wir ihn als Index benutzen. Sind sämtliche möglichen Werte von e sicher im Intervall [l; u], können wir auf check verzichten. Kann die Übersetzung von switch-Anweisungen als L-Attributierung implementiert werden? schwierig, denn Marke B is unbekannt wenn checki übersetzt wird flexiblere Anordnung wenn Kode-Sequenzen aneinandergehängt werden können im Extrem: Übersetzung mit Basisblöcken 124 / 184 Allgemeine Übersetzung der switch-Anweisung Im Allgemeinen können die Werte der Alternativen weit auseinander liegen. generiere eine Folge von expliziten Tests, wie für die if-Anweisung 125 / 184 Allgemeine Übersetzung der switch-Anweisung Im Allgemeinen können die Werte der Alternativen weit auseinander liegen. generiere eine Folge von expliziten Tests, wie für die if-Anweisung bei n verschiedenen Tests kann binäre Suche angewendet werden O(log n) Tests ; 125 / 184 Allgemeine Übersetzung der switch-Anweisung Im Allgemeinen können die Werte der Alternativen weit auseinander liegen. generiere eine Folge von expliziten Tests, wie für die if-Anweisung bei n verschiedenen Tests kann binäre Suche angewendet werden O(log n) Tests ; hat die Menge der getesteten Werte kleine Lücken ( 3), so ist Sprungtabelle effizienter und platzsparender 125 / 184 Allgemeine Übersetzung der switch-Anweisung Im Allgemeinen können die Werte der Alternativen weit auseinander liegen. generiere eine Folge von expliziten Tests, wie für die if-Anweisung bei n verschiedenen Tests kann binäre Suche angewendet werden O(log n) Tests ; hat die Menge der getesteten Werte kleine Lücken ( 3), so ist Sprungtabelle effizienter und platzsparender eventuell kann man mehrere Sprungtabellen für verschiedene, zusammenhängende Mengen generieren 125 / 184 Allgemeine Übersetzung der switch-Anweisung Im Allgemeinen können die Werte der Alternativen weit auseinander liegen. generiere eine Folge von expliziten Tests, wie für die if-Anweisung bei n verschiedenen Tests kann binäre Suche angewendet werden O(log n) Tests ; hat die Menge der getesteten Werte kleine Lücken ( 3), so ist Sprungtabelle effizienter und platzsparender eventuell kann man mehrere Sprungtabellen für verschiedene, zusammenhängende Mengen generieren ein Suchbaum mit Tabellen kann durch profiling anders angeordnet werden, so dass häufig genommene Pfade weniger Tests erfordern 125 / 184 Übersetzung in Basic Blocks Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion 126 / 184 Übersetzung in Basic Blocks Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion reiche diesen Basic Block zur Übersetzung der Anweisungen runter 126 / 184 Übersetzung in Basic Blocks Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion reiche diesen Basic Block zur Übersetzung der Anweisungen runter eine Zuweisung wird am Ende des Basic Blocks angehängt 126 / 184 Übersetzung in Basic Blocks Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion reiche diesen Basic Block zur Übersetzung der Anweisungen runter eine Zuweisung wird am Ende des Basic Blocks angehängt eine zweiseitige if Anweisung erzeugt drei neue, leere Blöcke: 1 2 3 einen für den then-Zweig, verbunden mit aktuellem Block durch jumpz Kante einen für den else-Zweig, durch jump Kante verbunden einen für die nachfolgenden Anweisungen, verbunden mit thenund else-Zweig durch jump Kante 126 / 184 Übersetzung in Basic Blocks Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion reiche diesen Basic Block zur Übersetzung der Anweisungen runter eine Zuweisung wird am Ende des Basic Blocks angehängt eine zweiseitige if Anweisung erzeugt drei neue, leere Blöcke: 1 2 3 einen für den then-Zweig, verbunden mit aktuellem Block durch jumpz Kante einen für den else-Zweig, durch jump Kante verbunden einen für die nachfolgenden Anweisungen, verbunden mit thenund else-Zweig durch jump Kante ähnlich für andere Konstrukte 126 / 184 Übersetzung in Basic Blocks Problem: Wie verknüpft man die verschiedenen Basic Blöcke? Idee: beim Übersetzen einer Funktion: erzeuge einen leeren Basic Block und speichere den Zeiger im Konten der Funktion reiche diesen Basic Block zur Übersetzung der Anweisungen runter eine Zuweisung wird am Ende des Basic Blocks angehängt eine zweiseitige if Anweisung erzeugt drei neue, leere Blöcke: 1 2 3 einen für den then-Zweig, verbunden mit aktuellem Block durch jumpz Kante einen für den else-Zweig, durch jump Kante verbunden einen für die nachfolgenden Anweisungen, verbunden mit thenund else-Zweig durch jump Kante ähnlich für andere Konstrukte Zur besseren Navigation sollten auch Rückwärtskanten zwischen den Blöcken eingefügt werden. 126 / 184 Die Synthesephase Kapitel 5: Funktionen 127 / 184 Aufbau einer Funktion Die Definition einer Funktion besteht aus einem Namen, mit dem sie aufgerufen werden kann; einer Spezifikation der formalen Parameter; evtl. einem Ergebnistyp; einem Anweisungsteil. In C gilt: codeiR f = loadc _f mit _f Anfangsadresse des Codes für f Beachte: auch Funktions-Namen müssen eine Adresse zugewiesen bekommen da die Größe von Funktionen nicht vor der Übersetzung bekannt ist, müssen die Adressen der Funktionen anschließend eingetragen werden 128 / 184 Speicherverwaltung bei Funktionen int fac(int x) { if (x<=0) return 1; else return x*fac(x-1); } int main(void) { int n; n = fac(2) + fac(1); printf("%d", n); } Zu einem Ausführungszeitpunkt können mehrere Instanzen der gleichen Funktion aktiv sein, d. h. begonnen, aber noch nicht beendet sein. Der Rekursionsbaum im Beispiel: main fac fac fac fac printf fac 129 / 184 Speicherverwaltung von Funktionsvariablen Die formalen Parameter und lokalen Variablen der verschiedenen Aufrufe der selben Funktion (Instanzen) müssen auseinander gehalten werden. Idee zur Implementierung: 130 / 184 Speicherverwaltung von Funktionsvariablen Die formalen Parameter und lokalen Variablen der verschiedenen Aufrufe der selben Funktion (Instanzen) müssen auseinander gehalten werden. Idee zur Implementierung: lege einen speziellen Speicherbereich für jeden Aufruf einer Funktion an. 130 / 184 Speicherverwaltung von Funktionsvariablen Die formalen Parameter und lokalen Variablen der verschiedenen Aufrufe der selben Funktion (Instanzen) müssen auseinander gehalten werden. Idee zur Implementierung: lege einen speziellen Speicherbereich für jeden Aufruf einer Funktion an. in sequentiellen Programmiersprachen können diese Speicherbereiche auf dem Keller verwaltet werden 130 / 184 Speicherverwaltung von Funktionsvariablen Die formalen Parameter und lokalen Variablen der verschiedenen Aufrufe der selben Funktion (Instanzen) müssen auseinander gehalten werden. Idee zur Implementierung: lege einen speziellen Speicherbereich für jeden Aufruf einer Funktion an. in sequentiellen Programmiersprachen können diese Speicherbereiche auf dem Keller verwaltet werden jede Instanz einer Funktion erhält dadurch einen Bereich auf dem Stack 130 / 184 Speicherverwaltung von Funktionsvariablen Die formalen Parameter und lokalen Variablen der verschiedenen Aufrufe der selben Funktion (Instanzen) müssen auseinander gehalten werden. Idee zur Implementierung: lege einen speziellen Speicherbereich für jeden Aufruf einer Funktion an. in sequentiellen Programmiersprachen können diese Speicherbereiche auf dem Keller verwaltet werden jede Instanz einer Funktion erhält dadurch einen Bereich auf dem Stack diese Bereiche heißen Keller-Rahmen (oder stack frame) 130 / 184 Kellerrahmen-Organisation Stapel Präsentation: wächst nach oben, zu höheren Adressen SP bestimmt die letzte, benutzte Stapeladresse SP lokaler Speicher callee FP PCold FPold organisatorische Zellen EPold lokaler Speicher caller 131 / 184 Kellerrahmen-Organisation Stapel Präsentation: wächst nach oben, zu höheren Adressen SP bestimmt die letzte, benutzte Stapeladresse SP lokaler Speicher callee FP PCold FPold organisatorische Zellen EPold lokaler Speicher caller FP = b Frame Pointer; zeigt auf die letzte organisatorische Zelle wird zur Wiederherstellung des letzten Kellerrahmens benutzt 131 / 184 Kellerrahmen-Organisation Stapel Präsentation: wächst nach oben, zu höheren Adressen SP bestimmt die letzte, benutzte Stapeladresse SP lokaler Speicher callee FP PCold FPold organisatorische Zellen EPold lokaler Speicher caller FP = b Frame Pointer; zeigt auf die letzte organisatorische Zelle wird zur Wiederherstellung des letzten Kellerrahmens benutzt EP hatten wir noch nicht; hat mit dem Heap zu tun 131 / 184 Arbeitsteilung beim Funktionsaufruf Definition Sei f die aktuelle Funktion, die die Funktion g aufruft. f heißt caller g heißt callee Der Code für den Aufruf muss auf den Caller und den Callee verteilt werden. Die Aufteilung kann nur so erfolgen, dass der Teil, der von Informationen des Callers abhängt, auch dort erzeugt wird und analog für den Callee. Beobachtung: Den Platz für die aktuellen Parameter kennt nur der Caller: Beispiel: printf 132 / 184 Prinzip vom Funktionsaufruf und Rücksprung Aktionen beim Betreten von g: 1: 2: 3: 4: 5: 6: 7: 8: Berechnung der Anfangsadresse von g Berechnung der aktuellen Parameter Retten aller caller-save Register Retten von FP, EP Setzen des neuen FP Retten von PC und Sprung an den Anfang von g Setzen des neuen EP Allokieren der lokalen Variablen 9 > > > > > > > > saveloc = stehen in f mark 9 > > > = > > > call > > ; ; enter stehen in g alloc Aktionen bei Verlassen von g: 1: 2: 3: 4: 5: Berechnung des Rückgabewerts Rücksetzen der Register FP, EP, SP Rücksprung in den Code von f , d. h. Restauration des PC Wiederherstellen der caller-save Register Aufräumen des Stack 9 = 9 > > = return > > stehen in g ; ; restoreloc stehen in f pop k 133 / 184 Registerverwaltung bei Funktionsaufrufen Die zwei Registersätze (global und lokal) werden wie folgt verwendet: automatische Variablen leben in lokalen Registern Ri Zwischenergebnisse leben auch in lokalen Registern Ri Parameter leben in globalen Registern Ri (mit i 0) globale Variablen: 134 / 184 Registerverwaltung bei Funktionsaufrufen Die zwei Registersätze (global und lokal) werden wie folgt verwendet: automatische Variablen leben in lokalen Registern Ri Zwischenergebnisse leben auch in lokalen Registern Ri Parameter leben in globalen Registern Ri (mit i 0) globale Variablen: wie nehmen erstmal an, es gäbe keine Konvention: 134 / 184 Registerverwaltung bei Funktionsaufrufen Die zwei Registersätze (global und lokal) werden wie folgt verwendet: automatische Variablen leben in lokalen Registern Ri Zwischenergebnisse leben auch in lokalen Registern Ri Parameter leben in globalen Registern Ri (mit i 0) globale Variablen: wie nehmen erstmal an, es gäbe keine Konvention: die i te Argument einer Funktion wird in Register Ri übergeben 134 / 184 Registerverwaltung bei Funktionsaufrufen Die zwei Registersätze (global und lokal) werden wie folgt verwendet: automatische Variablen leben in lokalen Registern Ri Zwischenergebnisse leben auch in lokalen Registern Ri Parameter leben in globalen Registern Ri (mit i 0) globale Variablen: wie nehmen erstmal an, es gäbe keine Konvention: die i te Argument einer Funktion wird in Register Ri übergeben der Rückgabewert einer Funktion wird in R0 gespeichert 134 / 184 Registerverwaltung bei Funktionsaufrufen Die zwei Registersätze (global und lokal) werden wie folgt verwendet: automatische Variablen leben in lokalen Registern Ri Zwischenergebnisse leben auch in lokalen Registern Ri Parameter leben in globalen Registern Ri (mit i 0) globale Variablen: wie nehmen erstmal an, es gäbe keine Konvention: die i te Argument einer Funktion wird in Register Ri übergeben der Rückgabewert einer Funktion wird in R0 gespeichert lokale Register werden von der aufrufenden Funktion gespeichert 134 / 184 Registerverwaltung bei Funktionsaufrufen Die zwei Registersätze (global und lokal) werden wie folgt verwendet: automatische Variablen leben in lokalen Registern Ri Zwischenergebnisse leben auch in lokalen Registern Ri Parameter leben in globalen Registern Ri (mit i 0) globale Variablen: wie nehmen erstmal an, es gäbe keine Konvention: die i te Argument einer Funktion wird in Register Ri übergeben der Rückgabewert einer Funktion wird in R0 gespeichert lokale Register werden von der aufrufenden Funktion gespeichert Definition Sei f eine Funktion die g aufruft. Ein Register Ri heißt caller-saved, wenn f Ri sichert und g es überschreiben darf callee-saved, wenn f Ri nicht sichert und g es vor dem Rücksprung wiederherstellen muss 134 / 184 Übersetzung von Funktionsaufrufen Ein Funktionsaufruf g(e1 ; : : : en ) wird nun wie folgt übersetzt: codeiR g(e1 ; : : : en ) = codeiR g codei+1 e1 R .. . codeiR+n en move R .. . move R 1 Ri+1 n Ri+n saveloc R1 Ri 1 mark call Ri restoreloc R1 Ri 1 move Ri R0 135 / 184 Übersetzung von Funktionsaufrufen Ein Funktionsaufruf g(e1 ; : : : en ) wird nun wie folgt übersetzt: codeiR g(e1 ; : : : en ) = codeiR g codei+1 e1 R .. . codeiR+n en move R .. . move R 1 Ri+1 n Ri+n saveloc R1 Ri 1 mark call Ri restoreloc R1 Ri 1 move Ri R0 Neue Instruktionen: saveloc Ri Rj legt die Register Ri ; Ri+1 : : : Rj auf dem Stapel ab mark rettet organisatorische Zellen call Ri ruft Funktion auf die an Adresse Ri liegt restoreloc Ri Rj nimmt Rj ; Rj 1 ; : : : Ri vom Stapel runter 135 / 184 Übersetzung von Funktionsaufrufen Ein Funktionsaufruf g(e1 ; : : : en ) wird nun wie folgt übersetzt: codeiR g(e1 ; : : : en ) =? codeiR e1 = codeiR g codei+1 e1 move R 1 Ri .. . codeiR en R .. . codeiR+n en move R .. . move R 1 n Ri+1 move R Ri+n saveloc R1 Ri n Ri codeiR g saveloc R1 Ri 1 mark 1 mark call Ri call Ri restoreloc R1 Ri restoreloc R1 Ri 1 1 move Ri R0 move Ri R0 Neue Instruktionen: saveloc Ri Rj legt die Register Ri ; Ri+1 : : : Rj auf dem Stapel ab mark rettet organisatorische Zellen call Ri ruft Funktion auf die an Adresse Ri liegt restoreloc Ri Rj nimmt Rj ; Rj 1 ; : : : Ri vom Stapel runter 135 / 184 Retten von EP und FP Der Befehl mark legt Platz für Rückgabewert und organisatorische Zellen an und rettet FP und EP. FP EP FP EP e e e mark S[SP+1] = EP; S[SP+2] = FP; SP = SP + 2; 136 / 184 Aufrufen einer Funktion Der Befehl call rettet den aktuellen Wert des PC als Fortsetzungs-Adresse und setzt FP und PC. FP q Ri p call Ri q Ri PC PC p q S[SP] = PC; SP = SP+1; FP = SP; PC = Ri; 137 / 184 Rückgabewerte einer Funktion Die globalen Register werden auch benutzt um den Rückgabewert zu übermitteln: codei return e = codeiR e move R0 Ri return 138 / 184 Rückgabewerte einer Funktion Die globalen Register werden auch benutzt um den Rückgabewert zu übermitteln: codei return e = codeiR e move R0 Ri return Alternative ohne Rückgabewert: codei return = return 138 / 184 Rückgabewerte einer Funktion Die globalen Register werden auch benutzt um den Rückgabewert zu übermitteln: codei return e = codeiR e move R0 Ri return Alternative ohne Rückgabewert: codei return = return Globale Register werden ansonsten im Funktionsrumpf nicht benutzt: Vorteil: an jeder Stelle im Rumpf können andere Funktionen aufgerufen werden ohne globale Register retten zu müssen Nachteil: beim Eintritt in eine Funktion müssen die globalen Register gesichert werden 138 / 184 Rücksprung aus einer Funktion Der Befehl return gibt den aktuellen Keller-Rahmen auf. D.h. er restauriert die Register PC, EP und FP. PC FP EP p return PC FP EP p e e PC = S[FP]; EP = S[FP-2]; SP = FP-3; FP = S[SP+2]; 139 / 184 Übersetzung ganzer Funktionen Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args)fdecls ssg = enter q move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: 140 / 184 Übersetzung ganzer Funktionen Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args)fdecls ssg = enter q move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: die Funktion hat n Parameter 140 / 184 Übersetzung ganzer Funktionen Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args)fdecls ssg = enter q move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: die Funktion hat n Parameter die lokalen Variablen sind in den Registern R1 ; : : : Rl gespeichert 140 / 184 Übersetzung ganzer Funktionen Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args)fdecls ssg = enter q move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: die Funktion hat n Parameter die lokalen Variablen sind in den Registern R1 ; : : : Rl gespeichert die Parameter der Funktion stehen in den Registern R 1 ; : : : R n 140 / 184 Übersetzung ganzer Funktionen Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args)fdecls ssg = enter q move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: die Funktion hat n Parameter die lokalen Variablen sind in den Registern R1 ; : : : Rl gespeichert die Parameter der Funktion stehen in den Registern R 1 ; : : : R n 0 ist die um lokale Variablen decls und Funktions-Parameter args erweiterte Umgebung 140 / 184 Übersetzung ganzer Funktionen Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args)fdecls ssg = enter q move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: die Funktion hat n Parameter die lokalen Variablen sind in den Registern R1 ; : : : Rl gespeichert die Parameter der Funktion stehen in den Registern R 1 ; : : : R n 0 ist die um lokale Variablen decls und Funktions-Parameter args erweiterte Umgebung return nicht immer nötig 140 / 184 Übersetzung ganzer Funktionen Die Übersetzung einer Funktion ist damit wie folgt definiert: code1 tr f(args)fdecls ssg = enter q move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: die Funktion hat n Parameter die lokalen Variablen sind in den Registern R1 ; : : : Rl gespeichert die Parameter der Funktion stehen in den Registern R 1 ; : : : R n 0 ist die um lokale Variablen decls und Funktions-Parameter args erweiterte Umgebung return nicht immer nötig Sind die move Instruktionen immer nötig? 140 / 184 Übersetzen ganzer Programme Ein Programm P = F1 ; : : : Fn muss eine main Funktion enthalten. code1 P = loadc R1 _main mark call R1 halt code1 F1 f1 .. . _f1 : _fn : code1 Fn fn 141 / 184 Übersetzen ganzer Programme Ein Programm P = F1 ; : : : Fn muss eine main Funktion enthalten. code1 P = loadc R1 _main mark call R1 halt code1 F1 f1 .. . _f1 : _fn : code1 Fn fn Diskussion: = ; enthält die Adressen der globalen Variablen fi enthält die Adressen der lokalen Variablen 1 2 = x : 2 (x) 1 (x) if x 2 dom(2 ) otherwise 141 / 184 Übersetzung der Fakultätsfunktion Betrachte: int fac(int x) { if (x<=0) then return 1; else return x*fac(x-1); } _fac: i=2 enter 5 move R1 R 1 move R2 R1 loadc R3 0 leq R2 R2 R3 jumpz R2 _A loadc R2 1 move R0 R2 return jump _B _A: i=3 i=4 i=3 3 mark+call save param. if (x<=0) to else return 1 _B: move R2 R1 move R3 R1 loadc R4 1 sub R3 R3 R4 move R 1 R3 loadc R3 _fac saveloc R1 R2 mark call R3 restoreloc R1 R2 move R3 R0 mul R2 R2 R3 move R0 R2 return return x*fac(x-1) x-1 fac(x-1) return x*... code is dead 142 / 184 Themengebiet: Variablen im Speicher 143 / 184 Register versus Speicher Bisher: alle Variablen sind in Registern gespeichert alle Funktionsargumente und Rückgabewerte auch 144 / 184 Register versus Speicher Bisher: alle Variablen sind in Registern gespeichert alle Funktionsargumente und Rückgabewerte auch Beschränkungen: in einer realen Maschine gibt es nur endlich viele Register C erlaubt es, die Adresse von Variablen zu nehmen Felder können nicht übersetzt werden, wegen der Indizierung 144 / 184 Register versus Speicher Bisher: alle Variablen sind in Registern gespeichert alle Funktionsargumente und Rückgabewerte auch Beschränkungen: in einer realen Maschine gibt es nur endlich viele Register C erlaubt es, die Adresse von Variablen zu nehmen Felder können nicht übersetzt werden, wegen der Indizierung Idee: speichere Variablen auch auf dem Keller 144 / 184 Variablen im Speicher Kapitel 1: Datenstrukturen im Speicher 145 / 184 Variablen im Speicher: L-Wert und R-Wert Variablen können auf zwei Weisen verwendet werden. Beispiel: a[x] = y + 1 Für y sind wir am Inhalt der Zelle, für a[x] an der Adresse interessiert. R-Wert von x L-Wert von x = = Inhalt von x Adresse von x Berechne R-Wert und L-Wert im Register Ri : codeiR e codeiL e liefert den Code zur Berechnung des R-Werts von e in der Adress-Umgebung analog für den L-Wert Achtung: Nicht jeder Ausdruck verfügt über einen L-Wert (z.B.: x + 1). 146 / 184 Adressumgebung Eine Variable kann in einem von vier konzeptionell verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 Register: eine Variable ist in lokalem Ri oder globalem Ri 147 / 184 Adressumgebung Eine Variable kann in einem von vier konzeptionell verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 Register: eine Variable ist in lokalem Ri oder globalem Ri Entsprechend definieren wir : Var ! fG; L; Rg Z wie folgt: x = hG; ai: Variable x ist an absoluter Adresse a gespeichert x = hL; ai: Variable x ist an Adresse FP + a gespeichert x = hR; ai: Variable x ist im Register Ra gespeichert 147 / 184 Adressumgebung Eine Variable kann in einem von vier konzeptionell verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 Register: eine Variable ist in lokalem Ri oder globalem Ri Entsprechend definieren wir : Var ! fG; L; Rg Z wie folgt: x = hG; ai: Variable x ist an absoluter Adresse a gespeichert x = hL; ai: Variable x ist an Adresse FP + a gespeichert x = hR; ai: Variable x ist im Register Ra gespeichert Beachte: eine Variable x kann nur einen Eintrag in haben. Allerdings: 147 / 184 Adressumgebung Eine Variable kann in einem von vier konzeptionell verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 Register: eine Variable ist in lokalem Ri oder globalem Ri Entsprechend definieren wir : Var ! fG; L; Rg Z wie folgt: x = hG; ai: Variable x ist an absoluter Adresse a gespeichert x = hL; ai: Variable x ist an Adresse FP + a gespeichert x = hR; ai: Variable x ist im Register Ra gespeichert Beachte: eine Variable x kann nur einen Eintrag in haben. Allerdings: könnte unterschiedlich sein an verschiedenen Programmpunkten 147 / 184 Adressumgebung Eine Variable kann in einem von vier konzeptionell verschiedenen Bereichen existieren. 1 Global: eine Variable ist global 2 Lokal: eine Variable liegt auf dem aktuellen Kellerrahmen 3 Register: eine Variable ist in lokalem Ri oder globalem Ri Entsprechend definieren wir : Var ! fG; L; Rg Z wie folgt: x = hG; ai: Variable x ist an absoluter Adresse a gespeichert x = hL; ai: Variable x ist an Adresse FP + a gespeichert x = hR; ai: Variable x ist im Register Ra gespeichert Beachte: eine Variable x kann nur einen Eintrag in haben. Allerdings: könnte unterschiedlich sein an verschiedenen Programmpunkten d.h. x kann einem Register zugewiesen sein und an einem anderen Punkt einer Speicherzelle 147 / 184 Notwendigkeit von Variablen im Speicher Globale Variablen: könnten programm-weit den Registern R1 : : : Rn zugeordnet sein z: y: x: Weiterhin: 148 / 184 Notwendigkeit von Variablen im Speicher Globale Variablen: könnten programm-weit den Registern R1 : : : Rn zugeordnet sein z: y: x: separate Übersetzung schwierig, da Funktionscode von n abhängt Weiterhin: 148 / 184 Notwendigkeit von Variablen im Speicher Globale Variablen: könnten programm-weit den Registern R1 : : : Rn zugeordnet sein z: y: x: separate Übersetzung schwierig, da Funktionscode von n abhängt besser: speichere globale Variablen im Speicher Weiterhin: 148 / 184 Notwendigkeit von Variablen im Speicher Globale Variablen: könnten programm-weit den Registern R1 : : : Rn zugeordnet sein z: y: x: separate Übersetzung schwierig, da Funktionscode von n abhängt besser: speichere globale Variablen im Speicher Weiterhin: eine Variable x (int oder struct), deren Adresse genommen wurde, muss im Speicher allokiert werden, d.h. x = hL; oi oder x = hG; oi 148 / 184 Notwendigkeit von Variablen im Speicher Globale Variablen: könnten programm-weit den Registern R1 : : : Rn zugeordnet sein z: y: x: separate Übersetzung schwierig, da Funktionscode von n abhängt besser: speichere globale Variablen im Speicher Weiterhin: eine Variable x (int oder struct), deren Adresse genommen wurde, muss im Speicher allokiert werden, d.h. x = hL; oi oder x = hG; oi auf ein Feld (array) wird immer mittels Zeiger zugegriffen, muss also auch im Speicher allokiert werden 148 / 184 Notwendigkeit von Variablen im Speicher Globale Variablen: könnten programm-weit den Registern R1 : : : Rn zugeordnet sein z: y: x: separate Übersetzung schwierig, da Funktionscode von n abhängt besser: speichere globale Variablen im Speicher Weiterhin: eine Variable x (int oder struct), deren Adresse genommen wurde, muss im Speicher allokiert werden, d.h. x = hL; oi oder x = hG; oi auf ein Feld (array) wird immer mittels Zeiger zugegriffen, muss also auch im Speicher allokiert werden Optimierung: Speichere Elemente eines struct in Registern, während Zeigerzugriffe diese nicht verändern können 148 / 184 Übersetzung von Zuweisungen Zuweisungen wie x=2*y wurden bisher übersetzt durch: das Ermitteln das R-Wertes von 2*y in Register Ri , das Kopieren des Inhalts von Ri in das Register (x) Formal: Sei (x) = hR; ji, dann gilt: codeiR x = e2 = codeiR e2 move Rj Ri 149 / 184 Übersetzung von Zuweisungen Zuweisungen wie x=2*y wurden bisher übersetzt durch: das Ermitteln das R-Wertes von 2*y in Register Ri , das Kopieren des Inhalts von Ri in das Register (x) Formal: Sei (x) = hR; ji, dann gilt: codeiR x = e2 = codeiR e2 move Rj Ri Aber: undefiniertes Resultat, falls x = hL; ai oder x = hG; ai. 149 / 184 Übersetzung von Zuweisungen Zuweisungen wie x=2*y wurden bisher übersetzt durch: das Ermitteln das R-Wertes von 2*y in Register Ri , das Kopieren des Inhalts von Ri in das Register (x) Formal: Sei (x) = hR; ji, dann gilt: codeiR x = e2 = codeiR e2 move Rj Ri Aber: undefiniertes Resultat, falls x = hL; ai oder x = hG; ai. Idee: Berechne den R-Werte von e2 im Register Ri , berechne den L-Werte von e1 im Register Ri+1 und schreibe e2 and Adresse e1 mit einem store Befehl. 149 / 184 Übersetzung von L-Werten Neue Instruktion: store Ri Rj mit Semantik S[Ri ] = Rj 13 Rj Ri store Ri Rj 13 Definition für Anweisungen: codei e = codeiR e Wie wird x = e (mit x = hG; ai) nun übersetzt? 150 / 184 Übersetzung von L-Werten Neue Instruktion: store Ri Rj mit Semantik S[Ri ] = Rj 13 Rj Ri store Ri Rj 13 Definition für Anweisungen: codei e = codeiR e Wie wird x = e (mit x = hG; ai) nun übersetzt? Daher definiere für den Fall dass e1 = x und x = hR; ji nicht gilt: codeiR e1 = e2 = codeiR e2 codei+1 e1 L store Ri+1 Ri 150 / 184 Übersetzung von L-Werten Neue Instruktion: store Ri Rj mit Semantik S[Ri ] = Rj 13 Rj Ri store Ri Rj 13 Definition für Anweisungen: codei e = codeiR e Wie wird x = e (mit x = hG; ai) nun übersetzt? Daher definiere für den Fall dass e1 = x und x = hR; ji nicht gilt: codeiR e1 = e2 = codeiR e2 codei+1 e1 L store Ri+1 Ri Berechne den L-Wert einer Variable wie folgt: codeiL x = loadc Ri a 150 / 184 Reservierung von Speicher für lokale Variablen Gegeben: eine Funktion mit k lokalen int Variablen, deren Adresse genommen wurden. alloc k k pop k alloc k pop k SP = SP + k; SP = SP - k; Der Befehl alloc k reserviert auf dem Keller Platz für die lokalen Variablen, pop k gibt sie wieder frei. 151 / 184 Zugriff auf lokale Variablen Zugriffe auf lokale Variablen erfolgt relativ zum aktuellen FP. Darum modifizieren wir codeL für Variablen-Namen. Für x = hL; ai definieren wir codeiL x = loadrc Ri a if x = hL; ai Der Befehl loadrc Ri k berechnet die Summe von FP und k. f FP f+k Ri k loadrc Ri k Ri = FP + k 152 / 184 Allgemeiner L-Wert von Variablen Die Adresse einer Variablen wird wie folgt in Ri berechnet: codeiL x= loadc Ri a loadrc Ri a if x = hG; ai if x = hL; ai 153 / 184 Allgemeiner L-Wert von Variablen Die Adresse einer Variablen wird wie folgt in Ri berechnet: codeiL x= loadc Ri a loadrc Ri a if x = hG; ai if x = hL; ai Achtung: für x = hR; ji ist codeiL nicht definiert! 153 / 184 Allgemeiner L-Wert von Variablen Die Adresse einer Variablen wird wie folgt in Ri berechnet: codeiL x= loadc Ri a loadrc Ri a if x = hG; ai if x = hL; ai Achtung: für x = hR; ji ist codeiL nicht definiert! Beobachtung: 153 / 184 Allgemeiner L-Wert von Variablen Die Adresse einer Variablen wird wie folgt in Ri berechnet: codeiL x= loadc Ri a loadrc Ri a if x = hG; ai if x = hL; ai Achtung: für x = hR; ji ist codeiL nicht definiert! Beobachtung: intuitiv: ein Register hat keine Adresse 153 / 184 Allgemeiner L-Wert von Variablen Die Adresse einer Variablen wird wie folgt in Ri berechnet: codeiL x= loadc Ri a loadrc Ri a if x = hG; ai if x = hL; ai Achtung: für x = hR; ji ist codeiL nicht definiert! Beobachtung: intuitiv: ein Register hat keine Adresse ein Register darf während der Codegenerierung nie als L-Wert auftreten 153 / 184 Allgemeiner L-Wert von Variablen Die Adresse einer Variablen wird wie folgt in Ri berechnet: codeiL x= loadc Ri a loadrc Ri a if x = hG; ai if x = hL; ai Achtung: für x = hR; ji ist codeiL nicht definiert! Beobachtung: intuitiv: ein Register hat keine Adresse ein Register darf während der Codegenerierung nie als L-Wert auftreten dies erfordert eine Fallunterscheidung bei der Übersetzung von Zuweisungen 153 / 184 Makro-Befehle zum Zugriff auf lokale Variablen Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj . 154 / 184 Makro-Befehle zum Zugriff auf lokale Variablen Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj . Damit: loadrc Ri a; load Rj Ri : setze Rj auf x mit x = hL; ai. 154 / 184 Makro-Befehle zum Zugriff auf lokale Variablen Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj . Damit: loadrc Ri a; load Rj Ri : setze Rj auf x mit x = hL; ai. Allgemein: Lade Variable x in Register Ri : codeiR x = 8 < loada Ri a loadr R a : move Ri R i j if x = hG; ai if x = hL; ai if x = hR; ii 154 / 184 Makro-Befehle zum Zugriff auf lokale Variablen Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj . Damit: loadrc Ri a; load Rj Ri : setze Rj auf x mit x = hL; ai. Allgemein: Lade Variable x in Register Ri : codeiR x = 8 < loada Ri a loadr R a : move Ri R i j if x = hG; ai if x = hL; ai if x = hR; ii Analog: Für Schreiboperationen definiere: storer a Rj storea a Rj loadrc Ri a store Ri Rj loadc Ri a store Ri Rj D.h. storea a Rj ist Makro. Definiere Spezialfall (mit x = hG; ai): codeiR x = e2 = codeiR e2 codei+1 x L store Ri+1 Ri 154 / 184 Makro-Befehle zum Zugriff auf lokale Variablen Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj . Damit: loadrc Ri a; load Rj Ri : setze Rj auf x mit x = hL; ai. Allgemein: Lade Variable x in Register Ri : codeiR x = 8 < loada Ri a loadr R a : move Ri R i j if x = hG; ai if x = hL; ai if x = hR; ii Analog: Für Schreiboperationen definiere: storer a Rj storea a Rj loadrc Ri a store Ri Rj loadc Ri a store Ri Rj D.h. storea a Rj ist Makro. Definiere Spezialfall (mit x = hG; ai): codeiR x = e2 = codeiR e2 loadc Ri+1 a store Ri+1 Ri 154 / 184 Makro-Befehle zum Zugriff auf lokale Variablen Definiere: Der Befehl load Ri Rj setzt Ri auf den Wert an Adresse Rj . Damit: loadrc Ri a; load Rj Ri : setze Rj auf x mit x = hL; ai. Allgemein: Lade Variable x in Register Ri : codeiR x = 8 < loada Ri a loadr R a : move Ri R i j if x = hG; ai if x = hL; ai if x = hR; ii Analog: Für Schreiboperationen definiere: storer a Rj storea a Rj loadrc Ri a store Ri Rj loadc Ri a store Ri Rj D.h. storea a Rj ist Makro. Definiere Spezialfall (mit x = hG; ai): codeiR x = e2 = codeiR e2 storea a Ri 154 / 184 Datentransfer Instruktionen der R-CMa Lesezugriffe und Schreibzugriffe der R-CMa : Instruktion load Ri Rj loada Ri c loadr Ri c store Ri Rj storea c Ri storer c Ri Semantik Ri S[Rj ] Ri S[c] Ri S[FP + c] S[Ri ] Rj S[c] Ri S[FP + c] Ri Intuition lade Wert von Adresse lade globale Variable lade lokale Variable speichere Wert an Adresse schreibe globale Variable schreibe lokale Variable Instruktionen zur Adressberechnung: Instruktion loadc Ri c loadrc Ri c Semantik Intuition Ri c lade Konstante Ri FP + c lade Konstante rel. zu FP Instruktionen für den allgemeinen Datentransfer: Instruktion move Ri Rj move Ri k Rj Semantik Ri Rj [S[SP + i] S[Rj + i]]ki=01 Ri SP; SP SP + k Intuition transferiere Wert zw. Registern kopiere k Werte auf den Keller 155 / 184 Bestimmung der Adress-Umgebung Variablen können verschiedenen Tag in Symboltabelle haben: 1 globale Variablen, die außerhalb von Funktionen definiert werden; 2 lokale (automatische) Variablen, die innerhalb von Funktionen definiert werden; 3 Register (automatische) Variablen, die innerhalb von Funktionen definiert werden. Beispiel: int x, y; void f(int v, int w) { int a; if (a>0) { int b; g(&b); } else { int c; } } v x y v w a b c h h h h h h h (v) , , , , , , , i i i i i i i 156 / 184 Bestimmung der Adress-Umgebung Variablen können verschiedenen Tag in Symboltabelle haben: 1 globale Variablen, die außerhalb von Funktionen definiert werden; 2 lokale (automatische) Variablen, die innerhalb von Funktionen definiert werden; 3 Register (automatische) Variablen, die innerhalb von Funktionen definiert werden. Beispiel: int x, y; void f(int v, int w) { int a; if (a>0) { int b; g(&b); } else { int c; } } v x y v w a b c (v) hG,0i hG,1i h R , -1 i h R , -2 i hR,1i hL ,0i hR,2i 156 / 184 Funktionsargumente auf dem Keller C erlaubt sogenannte variadic functions unbekannte Anzahl an Parametern: R 1 ; R 2 ; : : : Problem: callee kann auf Register nicht mit Index zugreifen Beispiel: int printf(const char * format, ...); char *s = "Hello %s!\nIt’s %i to %i!\n"; int main(void) { printf(s ,"World", 5, 12); return 0; } 157 / 184 Funktionsargumente auf dem Keller C erlaubt sogenannte variadic functions unbekannte Anzahl an Parametern: R 1 ; R 2 ; : : : Problem: callee kann auf Register nicht mit Index zugreifen Beispiel: int printf(const char * format, ...); char *s = "Hello %s!\nIt’s %i to %i!\n"; int main(void) { printf(s ,"World", 5, 12); return 0; } Idee: schiebe variadic Parameter von rechts nach links auf Keller Der erste Parameter liegt direkt unterhalb von PC, FP, EP Für einen Prototypen f (1 x1 ; : : : ; k xk , ...) setzen wir: x1 xk+1 wäre hL; 7! hR; 1i 2 jk+1 ji 7! hR; ki xk+i wäre hL; xk 2 jk+1 j ::: jk+i ji 157 / 184 Funktionsargumente auf dem Keller C erlaubt sogenannte variadic functions unbekannte Anzahl an Parametern: R 1 ; R 2 ; : : : Problem: callee kann auf Register nicht mit Index zugreifen Beispiel: int printf(const char * format, ...); char *s = value "Hello %s!\nIt’s %i to %i!\n"; s ”World” int main(void) { 5 printf(s ,"World", 5, 12); 12 return 0; (pi ) hR; hL; hL; hL; 1i 3i 4i 5i } Idee: schiebe variadic Parameter von rechts nach links auf Keller Der erste Parameter liegt direkt unterhalb von PC, FP, EP Für einen Prototypen f (1 x1 ; : : : ; k xk , ...) setzen wir: x1 xk+1 wäre hL; 7! hR; 1i 2 jk+1 ji 7! hR; ki xk+i wäre hL; xk 2 jk+1 j ::: jk+i ji 157 / 184 Variablen im Speicher Kapitel 2: Felder und Zeiger 158 / 184 Felder Beispiel: int[11] a; Das Feld a enthält 11 Elemente und benötigt darum 11 Zellen. a[10] a ist die Adresse des Elements a[0]. a[0] Definiere Funktion j j um den Platzbedarf eines Typs zu berechnen: jtj = 1 k jt 0 j falls t ein Basistyp falls t t0 [k] Dann ergibt sich für die Deklaration d = 1 xi = xi 1 + jti x1 1 j t1 x1 ; : : : tk xk ; für i > 1 j j kann zur Übersetzungszeit berechnet werden, also auch . Beachte: j j ist nötig zur Implementierung von C’s sizeof Operator 159 / 184 Übersetzung von Felderzugriffen Erweitere codeL und codeR auf Ausdrücke mit indizierten Feldzugriffen. Sei t[c] a; die Deklaration eines Feldes a. 160 / 184 Übersetzung von Felderzugriffen Erweitere codeL und codeR auf Ausdrücke mit indizierten Feldzugriffen. Sei t[c] a; die Deklaration eines Feldes a. Um die Adresse des Elements a[i]zu bestimmen, müssen wir a + jtj (R-Wert von i) ausrechnen. Folglich: codeiL e2 [e1 ] = codeiR e1 codei+1 e2 R loadc Ri+2 jtj mul Ri+1 Ri+1 Ri+2 add Ri Ri Ri+1 160 / 184 Übersetzung von Felderzugriffen Erweitere codeL und codeR auf Ausdrücke mit indizierten Feldzugriffen. Sei t[c] a; die Deklaration eines Feldes a. Um die Adresse des Elements a[i]zu bestimmen, müssen wir a + jtj (R-Wert von i) ausrechnen. Folglich: codeiL e2 [e1 ] = codeiR e1 codei+1 e2 R loadc Ri+2 jtj mul Ri+1 Ri+1 Ri+2 add Ri Ri Ri+1 Bemerkung: In C ist ein Feld ein Zeiger. Ein deklariertes Feld a ist eine Zeiger-Konstante, deren R-Wert die Anfangsadresse von a ist. Formal setzen wir für ein Feld e codeiR e = codeiL e In C sind äquivalent (als L-Werte, nicht vom Typ): 2[a] a[2] a+2 160 / 184 Strukturen (Records) Achtung: Komponenten-Namen von Strukturen dürfen sich überlappen. Hier: Komponenten-Umgebung st bezieht sich auf den gerade übersetzten Struktur-Typ st. Sei struct { int a; int b; } x; Teil einer Deklarationsliste. x erhält die erste freie Zelle des Platzes für die Struktur als Relativ-Adresse. Für die Komponenten vergeben wir Adressen relativ zum Anfang der Struktur, hier a 7! 0, b 7! 1. 161 / 184 Strukturen (Records) Achtung: Komponenten-Namen von Strukturen dürfen sich überlappen. Hier: Komponenten-Umgebung st bezieht sich auf den gerade übersetzten Struktur-Typ st. Sei struct { int a; int b; } x; Teil einer Deklarationsliste. x erhält die erste freie Zelle des Platzes für die Struktur als Relativ-Adresse. Für die Komponenten vergeben wir Adressen relativ zum Anfang der Struktur, hier a 7! 0, b 7! 1. Sei allgemein t struct { t1 v1 ;: : : ; tk vk }. Dann sei jtj := k X = jti j st v1 := 0 st vi := st vi 1 + jti 1 j für i > 1 i 1 Damit erhalten wir: codeiL (e:c) = codeiL e loadc Ri+1 (st c) add Ri Ri Ri+1 161 / 184 Zeiger in C Mit Zeiger (-Werten) rechnen, heißt in der Lage zu sein, 1 Zeiger zu erzeugen, d.h. Zeiger auf Speicherzellen zu setzen; sowie 2 Zeiger zu dereferenzieren, d. h. durch Zeiger auf die Werte von Speicherzellen zugreifen. Erzeugen von Zeigern: Die Anwendung des Adressoperators & liefert einen Zeiger auf eine Variable, d. h. deren Adresse (= b L-Wert). Deshalb: codeiR &e = codeiL e Beispiel: Sei struct { int a; int b; } x; mit = fx 7! 13g und st = fa 7! 0; b 7! 1g gegeben. Dann ist codeiL (x:b) = loadc Ri+1 13 loadc Ri 1 add Ri Ri Ri+1 162 / 184 Dereferenzieren von Zeigern Die Anwendung des Operators * auf den Ausdruck e liefert den Inhalt der Speicherzelle, deren Adresse der L-Wert von e ist: codeiR e = codeiL e load Ri Ri Beispiel: Betrachte für struct t { int a[7]; struct t *b; }; int i,j; struct t *pt; den Ausdruck e ((pt -> b) -> a)[i+1] Wegen e->a (*e).a gilt: codeiL (e ! a) = codeiL e loadc Ri+1 ( a) add Ri Ri Ri+1 163 / 184 Übersetzung von Dereferenzierungen (I) Sei = fi 7! 1; j 7! 2; pt 7! 3; a 7! 0; b 7! 7 g. b: struct t { int a[7]; struct t *b; }; int i,j; struct t *pt; Übersetze e ((pt -> b) -> a)[i+1] b: pt: j: i: Dann ist: codeiL e = codeiL ((pt ! b) ! a) codeRi+1 (i + 1) loadc Ri+2 1 mul Ri+1 Ri+1 Ri+2 add Ri Ri Ri+1 a: a: = codeiL ((pt ! b) ! a) loada Ri+1 1 loadc Ri+2 1 add Ri+1 Ri+1 Ri+2 loadc Ri+2 1 mul Ri+1 Ri+1 Ri+2 add Ri Ri Ri+1 164 / 184 Übersetzung von Dereferenzierungen (II) Für dereferenzierte Felder (*e).a ist der R-Wert gleich der Dereferenzierung des L-Wert von e plus Offset von a. Deshalb erhalten wir: codeiL ((pt ! b) ! a) = codeiL (pt ! b) loadc Ri+1 0 add Ri Ri Ri+1 = Damit ergibt sich insgesamt die Folge: loada Ri 3 loadc Ri+1 7 add Ri Ri Ri+1 load Ri Ri loadc Ri+1 0 add Ri Ri Ri+1 loada Ri+1 1 loadc Ri+2 1 add Ri+1 Ri+1 Ri+2 loada Ri 3 loadc Ri+1 7 add Ri Ri Ri+1 load Ri Ri loadc Ri+1 0 add Ri Ri Ri+1 loadc Ri+2 1 mul Ri+1 Ri+1 Ri+2 add Ri Ri Ri+1 165 / 184 Berechnung der R-Werte von Funktionen Ähnlich deklarierten Feldern, werden Funktions-Namen als konstante Zeiger auf Funktionen aufgefasst. Dabei ist der R-Wert dieses Zeigers gleich der Anfangs-Adresse der Funktion. Achtung! Für eine Variable int (*)() g sind die beiden Aufrufe (*g)() und g() äquivalent. Die Dereferenzierungen eines Funktions-Zeigers ist unnötig und wird ignoriert. Folglich: codeR f codeR (e) = loadc ( f ) = codeR e f ein Funktions-Name e ein Funktions-Zeiger 166 / 184 Übergeben von Zusammengesetzten Parametern Betrachte folgenden Deklarationen: typedef struct { int x, y; } point_t; int distToOrigin(point_t); ; Wie übergibt man einen nicht-Basistypen als Parameter? 167 / 184 Übergeben von Zusammengesetzten Parametern Betrachte folgenden Deklarationen: typedef struct { int x, y; } point_t; int distToOrigin(point_t); ; Wie übergibt man einen nicht-Basistypen als Parameter? Idee: caller übergibt Zeiger auf Struktur 167 / 184 Übergeben von Zusammengesetzten Parametern Betrachte folgenden Deklarationen: typedef struct { int x, y; } point_t; int distToOrigin(point_t); ; Wie übergibt man einen nicht-Basistypen als Parameter? Idee: caller übergibt Zeiger auf Struktur Problem: callee könnte übergebenen Parameter ändern 167 / 184 Übergeben von Zusammengesetzten Parametern Betrachte folgenden Deklarationen: typedef struct { int x, y; } point_t; int distToOrigin(point_t); ; Wie übergibt man einen nicht-Basistypen als Parameter? Idee: caller übergibt Zeiger auf Struktur Problem: callee könnte übergebenen Parameter ändern Lösung: caller legt eine Kopie der Struktur an 167 / 184 Übergeben von Zusammengesetzten Parametern Betrachte folgenden Deklarationen: typedef struct { int x, y; } point_t; int distToOrigin(point_t); ; Wie übergibt man einen nicht-Basistypen als Parameter? Idee: caller übergibt Zeiger auf Struktur Problem: callee könnte übergebenen Parameter ändern Lösung: caller legt eine Kopie der Struktur an codeiR e = codeLi+1 e move Ri k Ri+1 e eine Struktur der Größe k 167 / 184 Übergeben von Zusammengesetzten Parametern Betrachte folgenden Deklarationen: typedef struct { int x, y; } point_t; int distToOrigin(point_t); ; Wie übergibt man einen nicht-Basistypen als Parameter? Idee: caller übergibt Zeiger auf Struktur Problem: callee könnte übergebenen Parameter ändern Lösung: caller legt eine Kopie der Struktur an codeiR e = codeLi+1 e move Ri k Ri+1 e eine Struktur der Größe k Neuer Befehl: move 167 / 184 Kopien von Speicherbereichen Die move Instruktion kopiert k Elemente auf den Stack. k Rj Ri move Ri k Rj for (i = k-1; i0; i--) S[SP+i] = S[Rj +i]; Ri = SP; SP = SP+k; 168 / 184 Variablen im Speicher Kapitel 3: Die Halde 169 / 184 Der Heap Zeiger gestatten auch den Zugriff auf anonyme, dynamisch erzeugte Datenelemente, deren Lebenszeit nicht dem LIFO-Prinzip unterworfen ist. Wir benötigen einen potentiell beliebig großen Speicherbereich H: den Heap (Halde). Implementierung: ; S H 0 MAX SP NP EP EP NP = b New Pointer; zeigt auf unterste belegte Haldenzelle. = b Extreme Pointer; zeigt auf die Zelle, auf die der SP maximal zeigen kann (innerhalb der aktuellen Funktion). 170 / 184 Invarianten des Heaps und des Stacks Stack und Heap dürfen sich nicht überschneiden 171 / 184 Invarianten des Heaps und des Stacks Stack und Heap dürfen sich nicht überschneiden eine Überschneidung kann bei jeder Erhöhung von SP eintreten (Stack Overflow) oder bei einer Erniedrigung des NP eintreten (Out Of Memory) 171 / 184 Invarianten des Heaps und des Stacks Stack und Heap dürfen sich nicht überschneiden eine Überschneidung kann bei jeder Erhöhung von SP eintreten (Stack Overflow) oder bei einer Erniedrigung des NP eintreten (Out Of Memory) im Gegensatz zum Stack Overflow kann ein Out Of Memory Fehler vom Programmierer abgefangen werden malloc liefert in diesem Fall NULL zurück, dass als (void*) 0 definiert ist 171 / 184 Invarianten des Heaps und des Stacks Stack und Heap dürfen sich nicht überschneiden eine Überschneidung kann bei jeder Erhöhung von SP eintreten (Stack Overflow) oder bei einer Erniedrigung des NP eintreten (Out Of Memory) im Gegensatz zum Stack Overflow kann ein Out Of Memory Fehler vom Programmierer abgefangen werden malloc liefert in diesem Fall NULL zurück, dass als (void*) 0 definiert ist EP reduziert die nötigen Überprüfungen auf Überschneidung auf den Funktionseintritt die Überprüfungen bei Heap-Allokationen bleiben erhalten 171 / 184 Reservierung von Speicher auf dem Stack Der Befehl enter q setzt den EP auf den neuen Wert. Steht nicht mehr genügend Platz zur Verfügung, wird die Programm-Ausführung abgebrochen. EP q enter q EP = SP + q; if (EP NP) Error (“Stack Overflow”); 172 / 184 Dynamisch Allokierter Speicher Es gibt eine weitere Arte, Zeiger zu erzeugen: ein Aufruf von malloc liefert einen Zeiger auf eine Heap-Zelle: codeiR malloc (e) = codeiR e new Ri NP NP n n Ri new Ri Ri if (NP - R[i] <= EP) R[i] = NULL; else { NP = NP - R[i]; R[i] = NP; } 173 / 184 Freigabe von Speicherplatz Ein mit malloc allokierter Bereich muss irgendwann mit free wieder freigegeben werden. Probleme: Der freigegebene Speicherbereich könnte noch von anderen Zeigern referenziert (dangling references). Nach einiger Freigabe könnte der Speicher etwa so aussehen (fragmentation): frei 174 / 184 Mögliche Implementierungen: 1 Nimm an, der Programmierer weiß, was er tut. Verwalte dann die freien Abschnitte (etwa sortiert nach Größe) in einer speziellen Datenstruktur; malloc wird teuer ; 2 Tue nichts, d.h.: codei free(e) = codeiR e ; einfach und effizient, aber nicht für reaktive Progaramme 3 Benutze eine automatische, evtl. “konservative” Garbage-Collection, die gelegentlich sicher nicht mehr benötigten Heap-Platz einsammelt und dann malloc zur Verfügung stellt. 175 / 184 Variablen im Speicher Kapitel 4: Erweiterte Übersetzung von Funktionen 176 / 184 Übersetzung von Programmen Vor der Programmausführung gilt: SP = 1 FP = EP = 0 PC = 0 NP = MAX Sei p V_defs F_def1 : : : F_defn , ein Programm, wobei F_defi eine Funktion fi definiert, von denen eine main heißt. Der Code für das Programm p enthält: Code für die Funktions-Definitionen F_defi ; Code zum Anlegen der globalen Variablen; Code für den Aufruf von main() die Instruktion halt. 177 / 184 Instruktionen für den Programmstart Ein Programm P = F1 ; : : : Fn muss eine main Funktion enthalten. code1 P = enter (k + 3) alloc k loadc R1 _main saveloc R1 R0 mark call R1 restoreloc R1 R0 halt codei F1 f1 .. . _f1 : _fn : codei Fn fn 178 / 184 Instruktionen für den Programmstart Ein Programm P = F1 ; : : : Fn muss eine main Funktion enthalten. code1 P = enter (k + 3) alloc k loadc R1 _main saveloc R1 R0 mark call R1 restoreloc R1 R0 halt codei F1 f1 .. . _f1 : _fn : codei Fn fn Diskussion: k sind die Anzahl der Stackplätze für globale Variablen saveloc R1 R0 hat keinen Effekt (d.h. es rettet kein Register) enthält die Adressen der Funktionen und globalen Variablen 178 / 184 Übersetzung von Funktionen Die Übersetzung einer Funktion erweitern wir folgt: code1 tr f(args)fdecls ssg = enter q alloc k move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: 179 / 184 Übersetzung von Funktionen Die Übersetzung einer Funktion erweitern wir folgt: code1 tr f(args)fdecls ssg = enter q alloc k move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: enter stellt sicher, dass genügend Kellerspeicher zur Verfüngung steht (q: Anzahl der maximal benötigten Kellerzellen) 179 / 184 Übersetzung von Funktionen Die Übersetzung einer Funktion erweitern wir folgt: code1 tr f(args)fdecls ssg = enter q alloc k move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: enter stellt sicher, dass genügend Kellerspeicher zur Verfüngung steht (q: Anzahl der maximal benötigten Kellerzellen) alloc reserviert Platz für Variablen auf dem Keller (k < q) 179 / 184 Übersetzung von Funktionen Die Übersetzung einer Funktion erweitern wir folgt: code1 tr f(args)fdecls ssg = enter q alloc k move Rl+1 R .. . 1 move Rl+n R n codel+n+1 ss 0 return Randbedinungen: enter stellt sicher, dass genügend Kellerspeicher zur Verfüngung steht (q: Anzahl der maximal benötigten Kellerzellen) alloc reserviert Platz für Variablen auf dem Keller (k < q) Können lokale Felder in f deklariert werden? 179 / 184 Übersetzung von Funktionsaufrufen Der Funktionsaufruf g(e1 ; : : : en ) wird nun wie folgt übersetzt: codeiR g(e1 ; : : : en ) = codeiR g codeiR+1 e1 .. . codeiR+n en move R .. . move R 1 Ri+1 n Ri+n saveloc R1 Ri 1 mark call Ri restoreloc R1 Ri 1 pop k move Ri R0 180 / 184 Übersetzung von Funktionsaufrufen Der Funktionsaufruf g(e1 ; : : : en ) wird nun wie folgt übersetzt: codeiR g(e1 ; : : : en ) = codeiR g codeiR+1 e1 .. . codeiR+n en move R .. . move R 1 Ri+1 n Ri+n saveloc R1 Ri 1 mark call Ri restoreloc R1 Ri 1 pop k move Ri R0 Unterschied zu vorher: wir gehen davon aus, dass g nur n Argumente hat, d.h., dass es nicht variadic ist neu: pop entfernt mögliche Kellerzellen, die durch codeiR+j ej erzeugt wurden 180 / 184 Peephole Optimization Die Erzeugten Instruktionen enthalten viele redundante Sequenzen, z.B.: move R7 R7 pop 0 move R5 R7 mul R4 R4 R7 Peephole Optimierung sucht nach solchen Mustern und ersetzt sie durch einfachere Instruktionen (bzw. gar keine). 181 / 184 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. 182 / 184 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern 182 / 184 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden 182 / 184 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj 182 / 184 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj falls das Programm mehr als die verfügbaren Register braucht, allokiere einige Variablen auf dem Stack und nicht mehr in Registern 182 / 184 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj falls das Programm mehr als die verfügbaren Register braucht, allokiere einige Variablen auf dem Stack und nicht mehr in Registern Wir benötigen eine Lösung für die folgenden Probleme: 182 / 184 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj falls das Programm mehr als die verfügbaren Register braucht, allokiere einige Variablen auf dem Stack und nicht mehr in Registern Wir benötigen eine Lösung für die folgenden Probleme: bestimme, wann ein Register nicht benutzt ist 182 / 184 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj falls das Programm mehr als die verfügbaren Register braucht, allokiere einige Variablen auf dem Stack und nicht mehr in Registern Wir benötigen eine Lösung für die folgenden Probleme: bestimme, wann ein Register nicht benutzt ist weise mehreren virtuellen Registern das gleiche reale Register zu, wenn sie nicht gleichzeitig benutzt werden 182 / 184 Realistische Register Maschinen Die R-CMa ist geeignet, auf einfache Weise Code für eine Registermaschine zu erzeugen. reale Maschinen haben nur eine fixe Anzahl an Registern die unendliche Menge an virtuellen Registern muss auf eine endliche Menge realer Register abgebildet werden Idee: Benutze ein Register Ri , das zur Zeit nicht genutzt wird, für den Wert eines anderen Registers Rj falls das Programm mehr als die verfügbaren Register braucht, allokiere einige Variablen auf dem Stack und nicht mehr in Registern Wir benötigen eine Lösung für die folgenden Probleme: bestimme, wann ein Register nicht benutzt ist weise mehreren virtuellen Registern das gleiche reale Register zu, wenn sie nicht gleichzeitig benutzt werden Diese Probleme sind Thema in Programmoptimierung. 182 / 184 Registerfärbung in der Fakultätsfunktion Betrachte: def-use int fac(int x) { if (x<=0) then return 1; else return x*fac(x-1); } _A: 1 0 1 2 3 4 _fac: enter 5 move R1 R 1 move R2 R1 loadc R3 0 leq R2 R2 R3 jumpz R2 _A loadc R2 1 move R0 R2 return jump _B U D U D U D D U U D U D U _B: move R2 R1 move R3 R1 loadc R4 1 sub R3 R3 R4 move R 1 R3 loadc R3 _fac saveloc R1 R2 mark call R3 restoreloc R1 R2 move R3 R0 mul R2 R2 R3 move R0 R2 return return 1 0 1 2 3 4 U D U D D U U D D U D U U U D U D D U U D D U D U U 183 / 184 Registerfärbung in der Fakultätsfunktion Betrachte: def-use liveness int fac(int x) { if (x<=0) then return 1; else return x*fac(x-1); } 1 0 1 2 3 4 _A: 1 0 1 2 3 4 _fac: enter 5 move R1 R 1 move R2 R1 loadc R3 0 leq R2 R2 R3 jumpz R2 _A loadc R2 1 move R0 R2 return jump _B >? >| | | | | | | | >? >| ?>| >? ? >? _B: move R2 R1 move R3 R1 loadc R4 1 sub R3 R3 R4 move R 1 R3 loadc R3 _fac saveloc R1 R2 mark call R3 restoreloc R1 R2 move R3 R0 mul R2 R2 R3 move R0 R2 return return >| | | | | | ? > | ?| > ? | | | | | | | | | | | | >| >| ?>| >? ? ? ? >|| | | | | | | | | | | ?| >| ?>| ?> ? 183 / 184 Registerfärbung in der Fakultätsfunktion Betrachte: def-use liveness coloring int fac(int x) { if (x<=0) then return 1; else return x*fac(x-1); } 1 0 1 2 3 4 _A: 1 0 1 2 3 4 _fac: enter 5 move R1 R 1 move R0 R1 loadc R 1 0 leq R0 R0 R 1 jumpz R2 _A loadc R2 1 move R0 R2 return jump _B >? >| > ? ?>| ? ?>> ? >| | | | | | | | _B: move R2 R1 move R0 R1 loadc R 1 1 sub R0 R0 R 1 move R 1 R0 loadc R3 _fac saveloc R1 R2 mark call R3 restoreloc R1 R2 move R0 R0 mul R2 R2 R0 move R0 R2 return return >| | >| | | > ? ?> ||| | | >| ? | | > | | | ? ? | | | | | | | | | | | | | | | | ?| ?>| | ?>| ? > ? >| ?>| ? 183 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden 184 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden Variablen müssen auf den Stack ausgelagert werden ; evtl. benötigt dies wiederum Register 184 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden Variablen müssen auf den Stack ausgelagert werden ; evtl. benötigt dies wiederum Register übersetze Funktionen in eine single static assignment Form 184 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden Variablen müssen auf den Stack ausgelagert werden ; evtl. benötigt dies wiederum Register übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) 184 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden Variablen müssen auf den Stack ausgelagert werden ; evtl. benötigt dies wiederum Register übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung 184 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden Variablen müssen auf den Stack ausgelagert werden ; evtl. benötigt dies wiederum Register übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung Schematisch präsentierte liveness-Analyse verbesserungsfähig: nach x y + 1 ist x nur lebendig wenn y lebendig ist 184 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden Variablen müssen auf den Stack ausgelagert werden ; evtl. benötigt dies wiederum Register übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung Schematisch präsentierte liveness-Analyse verbesserungsfähig: nach x y + 1 ist x nur lebendig wenn y lebendig ist saveloc hält Register unnötig am Leben ; Zwischensprache 184 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden Variablen müssen auf den Stack ausgelagert werden ; evtl. benötigt dies wiederum Register übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung Schematisch präsentierte liveness-Analyse verbesserungsfähig: nach x y + 1 ist x nur lebendig wenn y lebendig ist saveloc hält Register unnötig am Leben ; Zwischensprache gibt es optimale Regeln für die liveness-Analyse? 184 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden Variablen müssen auf den Stack ausgelagert werden ; evtl. benötigt dies wiederum Register übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung Schematisch präsentierte liveness-Analyse verbesserungsfähig: nach x y + 1 ist x nur lebendig wenn y lebendig ist saveloc hält Register unnötig am Leben ; Zwischensprache gibt es optimale Regeln für die liveness-Analyse? ; Vorlesung Programmoptimierung 184 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden Variablen müssen auf den Stack ausgelagert werden ; evtl. benötigt dies wiederum Register übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung Schematisch präsentierte liveness-Analyse verbesserungsfähig: nach x y + 1 ist x nur lebendig wenn y lebendig ist saveloc hält Register unnötig am Leben ; Zwischensprache gibt es optimale Regeln für die liveness-Analyse? ; Vorlesung Programmoptimierung Wie berechnet man die liveness Mengen, Registerverteilung zügig? 184 / 184 Ausblick Registerverteilung hat weitere Aufgaben: unnötige move Instruktionen müssen vermieden werden Variablen müssen auf den Stack ausgelagert werden ; evtl. benötigt dies wiederum Register übersetze Funktionen in eine single static assignment Form optimale Färbung möglich (allerdings müssen evtl. Register getauscht werden) ; Vorlesung Programmoptimierung Schematisch präsentierte liveness-Analyse verbesserungsfähig: nach x y + 1 ist x nur lebendig wenn y lebendig ist saveloc hält Register unnötig am Leben ; Zwischensprache gibt es optimale Regeln für die liveness-Analyse? ; Vorlesung Programmoptimierung Wie berechnet man die liveness Mengen, Registerverteilung zügig? ; Vorlesung Programmoptimierung 184 / 184