Compiler - Zwischencodegenerierung

Ein Quellcode kann direkt in seinen Zielmaschinencode übersetzt werden. Warum müssen wir dann den Quellcode überhaupt in einen Zwischencode übersetzen, der dann in seinen Zielcode übersetzt wird? Lassen Sie uns die Gründe sehen, warum wir einen Zwischencode benötigen.

  • Wenn ein Compiler die Quellsprache in seine Zielcomputersprache übersetzt, ohne die Option zum Generieren von Zwischencode zu haben, ist für jeden neuen Computer ein vollständiger nativer Compiler erforderlich.

  • Durch den Zwischencode ist kein neuer vollständiger Compiler für jede einzelne Maschine erforderlich, da der Analyseteil für alle Compiler gleich bleibt.

  • Der zweite Teil des Compilers, die Synthese, wird entsprechend der Zielmaschine geändert.

  • Es wird einfacher, die Quellcodemodifikationen anzuwenden, um die Codeleistung zu verbessern, indem Codeoptimierungstechniken auf den Zwischencode angewendet werden.

Zwischenvertretung

Zwischencodes können auf verschiedene Arten dargestellt werden und haben ihre eigenen Vorteile.

  • High Level IR- Die Darstellung von Zwischencode auf hoher Ebene kommt der Quellsprache selbst sehr nahe. Sie können einfach aus dem Quellcode generiert werden, und wir können problemlos Codemodifikationen anwenden, um die Leistung zu verbessern. Für die Zielmaschinenoptimierung ist dies jedoch weniger bevorzugt.

  • Low Level IR - Dieser befindet sich in der Nähe des Zielcomputers, wodurch er für die Register- und Speicherzuordnung, die Auswahl von Befehlssätzen usw. geeignet ist. Er eignet sich für maschinenabhängige Optimierungen.

Zwischencode kann entweder sprachspezifisch (z. B. Bytecode für Java) oder sprachunabhängig (Code mit drei Adressen) sein.

Code mit drei Adressen

Der Zwischencodegenerator empfängt Eingaben von seiner Vorgängerphase, dem semantischen Analysator, in Form eines mit Anmerkungen versehenen Syntaxbaums. Dieser Syntaxbaum kann dann in eine lineare Darstellung konvertiert werden, z. B. Postfix-Notation. Zwischencode ist in der Regel maschinenunabhängiger Code. Daher geht der Codegenerator von einer unbegrenzten Anzahl von Speichern (Registern) aus, um Code zu generieren.

Zum Beispiel:

a = b + c * d;

Der Zwischencodegenerator versucht, diesen Ausdruck in Unterausdrücke zu unterteilen und dann den entsprechenden Code zu generieren.

r1 = c * d;
r2 = b + r1;
a = r2

r wird als Register im Zielprogramm verwendet.

Ein Code mit drei Adressen hat höchstens drei Adresspositionen, um den Ausdruck zu berechnen. Ein Code mit drei Adressen kann in zwei Formen dargestellt werden: vierfach und dreifach.

Vierfache

Jede Anweisung in vierfacher Darstellung ist in vier Felder unterteilt: Operator, arg1, arg2 und Ergebnis. Das obige Beispiel ist unten im Vierfachformat dargestellt:

Op arg 1 arg 2 Ergebnis
* * c d r1
+ b r1 r2
+ r2 r1 r3
= r3 ein

Dreifache

Jede Anweisung in Dreifachdarstellung hat drei Felder: op, arg1 und arg2. Die Ergebnisse der jeweiligen Unterausdrücke werden durch die Position des Ausdrucks angegeben. Tripel stehen für Ähnlichkeit mit DAG und Syntaxbaum. Sie entsprechen DAG bei der Darstellung von Ausdrücken.

Op arg 1 arg 2
* * c d
+ b (0)
+ (1) (0)
= (2)

Tripel sehen sich dem Problem der Code-Unbeweglichkeit während der Optimierung gegenüber, da die Ergebnisse positionell sind und das Ändern der Reihenfolge oder Position eines Ausdrucks Probleme verursachen kann.

Indirekte Tripel

Diese Darstellung ist eine Verbesserung gegenüber der Dreifachdarstellung. Es werden Zeiger anstelle der Position verwendet, um die Ergebnisse zu speichern. Dadurch können die Optimierer den Unterausdruck frei positionieren, um einen optimierten Code zu erzeugen.

Erklärungen

Eine Variable oder Prozedur muss deklariert werden, bevor sie verwendet werden kann. Die Deklaration umfasst die Zuweisung von Speicherplatz im Speicher und die Eingabe von Typ und Name in die Symboltabelle. Ein Programm kann unter Berücksichtigung der Zielmaschinenstruktur codiert und entworfen werden, es ist jedoch möglicherweise nicht immer möglich, einen Quellcode genau in seine Zielsprache zu konvertieren.

Wenn das gesamte Programm als Sammlung von Prozeduren und Unterprozeduren betrachtet wird, können alle lokalen Namen der Prozedur deklariert werden. Die Speicherzuweisung erfolgt nacheinander, und die Namen werden dem Speicher in der Reihenfolge zugewiesen, in der sie im Programm deklariert sind. Wir verwenden die Offset-Variable und setzen sie auf Null {offset = 0}, was die Basisadresse bezeichnet.

Die Quellprogrammiersprache und die Zielmaschinenarchitektur können in der Art und Weise, wie Namen gespeichert werden, variieren, sodass die relative Adressierung verwendet wird. Während dem Vornamen ab dem Speicherort 0 {offset = 0} Speicher zugewiesen wird, sollte dem später deklarierten nächsten Namen neben dem ersten Speicher zugewiesen werden.

Example:

Wir nehmen das Beispiel der Programmiersprache C, in der einer ganzzahligen Variablen 2 Byte Speicher und einer Float-Variablen 4 Byte Speicher zugewiesen werden.

int a;
float b;

Allocation process:
{offset = 0}

   int a;
   id.type = int
   id.width = 2

offset = offset + id.width 
{offset = 2}

   float b;
   id.type = float
   id.width = 4
   
offset = offset + id.width 
{offset = 6}

Um dieses Detail in eine Symboltabelle einzugeben, kann eine Prozedureingabe verwendet werden. Diese Methode kann folgende Struktur haben:

enter(name, type, offset)

Dieses Verfahren sollte einen Eintrag in der Symboltabelle für variable erstellen Namen , ihren Typ Satz aufweist und die relative Adresse geben Offset in seinem Datenbereich.