Ein Compiler (auch Kompilierer oder Übersetzer) ist ein Computerprogramm, das ein in einer Quellsprache geschriebenes Programm in ein semantisch äquivalentes Programm einer Zielsprache umwandelt. Üblicherweise handelt es sich dabei um die Übersetzung eines von einem Programmierer in einer Programmiersprache geschriebenen Quelltextes in Assemblersprache, Bytecode oder Maschinensprache. Die Anwendung eines Compilers wird als Kompilierung bezeichnet.
Der Compilerbau, also die Programmierung eines Compilers, ist eine eigenständige Disziplin innerhalb der Informatik.
Die Bezeichnungen Compiler oder Kompilierer sind eigentlich irreführend, weil sie von der Zusammenstellung von Tabellen herrühren, die der Compiler für seine interne Datenverwaltung benötigt, was aber an der Kernaufgabe eines Compilers vorbeigeht.
Verwandt mit einem Compiler ist ein Interpreter, der ein Programm nicht in die Zielsprache übersetzt, sondern Schritt für Schritt direkt ausführt.
compilerphasen.gif Es lassen sich im Wesentlichen zwei Phasen unterscheiden: eine Analysephase, die den Quelltext analysiert und daraus einen attributierten Syntaxbaum erzeugt, sowie die Synthesephase, die daraus das Zielprogramm erzeugt.
Ein Scanner benutzt gelegentlich einen separaten Screener, um Whitespace (Leerraum, also Leerzeichen, Zeilenenden, usw.) und Kommentare zu überspringen.
Die Optimierung erfolgt teilweise in Abhängigkeit von den Eigenschaften der Hardware, z. B. wie viele und welche Register der Prozessor des Computers zur Verfügung stellt.
Einige Optimierungen führen dazu, dass der Compiler Programmkonstrukte in semantisch äquivalente, aber günstigere Konstrukte umwandelt, die keine Entsprechung im Quellcode haben. Ein Nachteil ist allerdings, dass es bei Aktivierung entsprechender Optimierungen kaum noch möglich ist, den Programmablauf mit einem interaktiven Debugger zu verfolgen.
„Optimierung“ bedeutet nicht, dass das Programm danach in irgendeiner Weise optimal wäre, lediglich besser. Es ist auch möglich, dass das Programm nachher „totoptimiert“ ist, also die Optimierung über das Ziel so weit hinausgeschossen ist, dass das Programm effektiv langsamer ausgeführt wird. Das ist z. B. dadurch möglich, dass längerer Code erzeugt wird, der zwar an sich schneller ausgeführt wird, aber mehr Zeit benötigt, um erst einmal in den Cache geladen zu werden und damit erst bei häufigerer Benutzung vorteilhaft ist.
Viele Optimierungen moderner Compiler sind solche Abwägungen zwischen dem, was möglich ist, und dem, was sinnvoll ist. Die Grenze zwischen beiden ist meist nicht klar ersichtlich und muss durch Tests (s. Profiler) herausgefunden werden.
Im folgenden betrachten wir einige Optimierungsmöglichkeiten eines Compilers. Es sollte aber nicht vergessen werden, dass das größte Optimierungspotenzial oft darin besteht, den Algorithmus selbst zu verändern bzw. durch einen besseren zu ersetzen. Dieser Vorgang kann meistens nicht automatisiert werden, sondern muss durch den Programmierer erfolgen. Einfachere Optimierungen kann er dagegen an den Compiler delegieren und so den Quelltext lesbarer halten.
In vielen höheren Programmiersprachen benötigt man beispielsweise eine Hilfsvariable, um den Inhalt zweier Variablen zu vertauschen:
| Höhere | ProgrammierspracheMaschinenbefehle | ohne OptimierungMaschinenbefehle | mit Optimierung
|---|---|---|
| t = a | a → Register 1 | Register 1 → ta → Register 1 |
| a = b | b → Register 1 | Register 1 → ab → Register 2 |
| b = t | t → Register 1 | Register 1 → bRegister 1 → b | Register 2 → a
Mit der Optimierung werden statt 6 nur noch 4 Assemblerbefehle benötigt, außerdem wird der Speicherplatz für die Hilfsvariable t nicht gebraucht. D. h. diese Vertauschung wird schneller ausgeführt und benötigt weniger Hauptspeicher. Dies gilt jedoch nur, wenn ausreichend Register im Prozessor zur Verfügung stehen. Die Speicherung von Daten in Registern statt im Hauptspeicher ist eine häufig angewendete Möglichkeit der Optimierung.
Die Berechnung des Kreisumfangs mittels
pi = 3.1415
u = 2 * pi * r
kann ein Compiler bereits zum Übersetzungszeitpunkt zu u = 6.2830 * r auswerten. Dies spart die Multiplikation 2 * pi zur Laufzeit des erzeugten Programms. Diese Vorgehensweise wird als Konstantenfaltung (engl. „constant folding“) bezeichnet. (Compiler für die Sprache Ada müssen diese Compile-Zeit-Berechnungen sogar in beliebiger Genauigkeit durchführen.)
Wenn der Compiler erkennen kann, dass ein Teil des Programmes niemals durchlaufen wird, dann kann er diesen Teil bei der Übersetzung weglassen.
Beispiel: ... ...
100 goto 900
200 k=3
900 i=7
... ...
Wenn in diesem Programm niemals ein GOTO auf das Label 200 erfolgt, dann kann auf die Anweisung 200 k=3 verzichtet werden.
Wird eine Variable nicht benötigt, so muss dafür kein Speicherplatz zugewiesen und kein Programmcode erzeugt werden.
Beispiel: subroutine test (a,b)
b = 2 * a
c = 3.14 * b
return b
Hier wird die Variable c nicht benötigt: Sie steht nicht in der Parameterliste, wird in späteren Berechnungen nicht verwendet und wird auch nicht ausgegeben. Deshalb kann die Anweisung c = 3.14 * b entfallen.
Insbesondere Schleifen versucht man zu optimieren, indem man z. B.
Bei kleinen Unterprogrammen fällt der Aufwand zum Aufruf des Unterprogrammes verglichen mit der vom Unterprogramm geleisteten Arbeit stärker ins Gewicht. Daher versuchen Compiler, den Maschinencode kleinerer Unterprogramme direkt einzufügen. Diese Technik wird auch als Inlining bezeichnet. In manchen Programmiersprachen ist es möglich, durch inline-Schlüsselwörter den Compiler darauf hinzuweisen, dass das Einfügen von bestimmten Unterprogrammen gewünscht ist. Das Einfügen von Unterprogrammen eröffnet oft, abhängig von den Parametern, weitere Möglichkeiten für Optimierungen.
Anstatt mehrfach auf dieselbe Variable im Speicher, beispielsweise in einer Datenstruktur, zuzugreifen, kann der Wert nur einmal gelesen und für weitere Verarbeitungen in Registern oder im Stack zwischengespeichert werden.
In C, C++ und Java muss dieses Verhalten ggf. mit dem Schlüsselwort volatile abgeschaltet werden: Eine als volatile bezeichnete Variable wird bei jeder Benutzung wiederholt vom originalem Speicherplatz gelesen, da ihr Wert sich unterdessen geändern haben könnte. Das kann beispielsweise der Fall sein, wenn es sich um einen Hardware-Port handelt oder ein parallel laufender Thread den Wert geändert haben könnte.
Beispiel: int a = array*->element.x; int b = 3 * array*->element.x;
Im Maschinenprogramm wird nur einmal auf array*->element.x zugegriffen, der Wert wird zwischengespeichert und zweimal verwendet. Ist x volatile, dann wird zweimal zugegriffen.
Es gibt außer volatile noch einen anderen Grund, der eine Zwischenspeicherung in Registern unmöglich macht: Wenn der Wert der Variablen v durch Verwendung des Zeigers z im Speicher verändert werden könnte, kann eine Zwischenspeicherung von v in einem Register zu fehlerhaftem Programmverhalten führen. Da die in der Programmiersprache C oft verwendeten Zeiger nicht auf ein Array beschränkt sind (sie könnten irgendwohin im Hauptspeicher zeigen), hat der Optimizer oft nicht genügend Informationen, um eine Veränderung einer Variable durch einen Zeiger auszuschließen.
Statt einer Multiplikation oder Division von Ganzzahlen mit einer Zweierpotenz kann eine Shift-Anweisung verwendet werden. Es gibt Fälle, in denen nicht nur Zweierpotenzen, sondern auch andere Zahlen (einfache Summen von Zweierpotenzen) für diese Optimierung herangezogen werden. So kann z. B. n << 1 + n << 2 schneller sein als n * 6. Statt einer Division durch eine Konstante kann eine Multiplikation mit dem Reziprokwert der Konstante erfolgen. Selbstverständlich sollte man solch spezielle Optimierungen auf jeden Fall dem Compiler überlassen.
Programmiersprachen wie Pascal und Java fordern Laufzeitüberprüfungen beim Zugriff auf Felder oder Variablen. Wenn der Compiler ermittelt, dass ein bestimmter Zugriff immer im erlaubten Bereich sein wird, kann der Code für diese Laufzeitüberprüfungen weggelassen werden.
Zusammenhängender Code, z. B. eine Schleife, sollte zur Laufzeit möglichst auf der gleichen „Seite“ (zusammenhängend vom Betriebssystem verwalteter Speicherblock) im Hauptspeicher liegen. Dies kann man z. B. dadurch erreichen, dass man dem Programmcode geeignete Leeranweisungen („NOPs“ – No OPeration) hinzufügt. Dadurch wird der Programmcode zwar größer, aber wegen des reduzierten Pagings wird das Programm schneller ausgeführt.
Durch das Vorziehen von Speicherlesezugriffen und das Verzögern von Schreibzugriffen lässt sich die Fähigkeit moderner Prozessoren zur Parallelarbeit verschiedener Funktionseinheiten ausnutzen. So kann beispielsweise bei den Befehlen: a = b * c; d = e * f; der Operand e bereits geladen werden, während ein anderer Teil des Prozessors noch mit der ersten Multiplikation beschäftigt ist.
Folgendes in der Programmiersprache C definierte Programm stellt einen einfachen Einpass-Compiler dar. Dieser Compiler übersetzt einfache Ausdrücke in Infix-Notation in Ausdrücke der Postfix-Notation sowie in eine maschinennahe Assemblersprache.
#include
- include
- include
- define MODE_POSTFIX 0
- define MODE_ASSEMBLY 1
char lookahead; int pos; int compile_mode; char expression*;
void error() { printf("Syntaxfehler!\n"); }
void match( char t ) { if( lookahead == t ) { pos++; lookahead = expression*; } else error(); }
void digit() { switch( lookahead ) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': printf( compile_mode == MODE_POSTFIX ? "%c" : "\tPUSH %c\n", lookahead ); match( lookahead ); break; default: error(); break; } }
void term() { digit(); while( 1 ) { switch( lookahead ) { case '*': match('*'); digit(); printf( "%s", compile_mode == MODE_POSTFIX ? "*" : "\tPOP B\n\tPOP A\n\tMUL A, B\n\tPUSH A\n" ); break; case '/': match('/'); digit(); printf( "%s", compile_mode == MODE_POSTFIX ? "/" : "\tPOP B\n\tPOP A\n\tDIV A, B\n\tPUSH A\n" ); break; default: return; } } }
void expr() { term(); while( 1 ) { switch( lookahead ) { case '+': match('+'); term(); printf( "%s", compile_mode == MODE_POSTFIX ? "+" : "\tPOP B\n\tPOP A\n\tADD A, B\n\tPUSH A\n" ); break; case '-': match('-'); term(); printf( "%s", compile_mode == MODE_POSTFIX ? "-" : "\tPOP B\n\tPOP A\n\tSUB A, B\n\tPUSH A\n"); break; default: return; } } }
int main ( int argc, char** argv ) { printf("Bitte geben Sie einen Ausdruck in Infix-Notation ein:\n\n\t"); gets( expression ); printf("\nCompilierter Ausdruck in Postfix-Notation:\n\n\t"); compile_mode = MODE_POSTFIX; pos = 0; lookahead = *expression; expr(); printf("\n\nCompilierter Ausdruck in Assemblersprache:\n\n"); compile_mode = MODE_ASSEMBLY; pos = 0; lookahead = *expression; expr();
return 0; }
Ein Lauf dieses Compilers führt beispielsweise zu folgender Ausgabe:
Bitte geben Sie einen Ausdruck in Infix-Notation ein:5+3*2-9
Compilierter Ausdruck in Postfix-Notation:
532*+9-
Compilierter Ausdruck in Assemblersprache:
PUSH 5 PUSH 3 PUSH 2 POP B POP A MUL A, B PUSH A POP B POP A ADD A, B PUSH A PUSH 9 POP B POP A SUB A, B PUSH A
Compilerbau | Programmierwerkzeug
Vertalerkonstruksie | Compilador | Компилатор | Compilador | Překladač | Compiler | Compiler | Compilador | Kompilaator | Ohjelmointikielen kääntäjä | Compilateur | Compilador | מהדר | Program-prevodilac | Fordítóprogram | Kompilator | Compilatore | コンパイラ | 컴파일러 | Kompiliatorius | Compiler | Kompilator | Kompilator | Compilador | Транслятор | Compiler | Kompilator | ตัวแปลโปรแกรม | Derleyici | Trình biên dịch | 编译器
This article is licensed under the GNU Free Documentation License.
It uses material from the
"Compiler".
Home Page • arts • business • computers • games • health • hospitals • home • kids & teens • news • physicians • recreation• reference • regional • science • shopping • society • sports • world