Einführung in die Systemprogrammierung Prof. Dr. Christoph Reichenbach Fachbereich 12 / Institut für Informatik 9. Juli 2015
Sprachen vs. Übersetzer Sprache C11 Übersetzer GNU C Compiler Intel C Compiler LLVM/Clang Sprachen können verschiedene Implementierungen haben: Übersetzer Interpreter Laufzeit-Übersetzer
Sprachen vs. Übersetzer Sprache C11 Übersetzer GNU C Compiler Intel C Compiler LLVM/Clang Sprachen können verschiedene Implementierungen haben: Übersetzer Interpreter Laufzeit-Übersetzer Sprachdefinition vereinigt Sprachimplementierungen
Die Struktur von Programmiersprachen Sprachdefinition gibt an: Lexikalische Sprachstruktur (Lexeme/Tokens) Grammatische Sprachstruktur (Syntax) Sprachsemantik: Statische Semantik Semantik der Namen: Welcher Name bindet an welche Definition? Semantik der Typen: Welche Typen können wie miteinander verknüpft werden? Dynamische Semantik Laufzeitverhalten des Programmes Beinhaltet meist: Reihenfolge und Inhalt von Ein- und Ausgaben Beinhaltet meist nicht: Ausführungsgeschwindigkeit, benötigter Speicherplatz Optimierungen müssen Sprachsemantik erhalten!
Der Übersetzer C-Programm Frontend Middle-End Backend Optimierung Assembler-Code
Der Übersetzer C-Programm Frontend Middle-End Backend Optimierung Assembler-Code
Der Übersetzer C-Programm Frontend Lexikalische Analyse Syntaxanalyse Namensanalyse Typüberprüfung Zwischenform Middle-End Optimierung Backend Fehlermeldungen Assembler-Code
Der Übersetzer C-Programm Frontend Lexikalische Analyse Syntaxanalyse Namensanalyse Typüberprüfung Zwischenform Middle-End Optimierung Backend Fehlermeldungen Assembler-Code
Der Übersetzer C-Programm Frontend Lexikalische Analyse Syntaxanalyse Namensanalyse Typüberprüfung Zwischenform Middle-End Optimierung Backend Registerauswahl Befehlsauswahl Codeerzeugung Fehlermeldungen Assembler-Code
Der Übersetzer: Frontend: Lexikalische Analyse Eingabe: { int x = 17 + 3L; char *c = x;
Der Übersetzer: Frontend: Lexikalische Analyse Eingabe: { int x = 17 + 3L; char *c = x; Lexikalische Analyse zerlegt Eingabe in Tokens/Lexeme Erzeugt vom Lexer (Tokenizer, Scanner) Ignoriert Leerzeichen, Zeilenumbrüche etc. (in C und C-artigen Sprachen)
Der Übersetzer: Frontend: Lexikalische Analyse Eingabe: { int x = 17 + 3L; char *c = x; Lexikalische Analyse zerlegt Eingabe in Tokens/Lexeme Erzeugt vom Lexer (Tokenizer, Scanner) Ignoriert Leerzeichen, Zeilenumbrüche etc. (in C und C-artigen Sprachen) Tokens und Lexeme: punctuator { int int identifier x punctuator = constant 17 punctuator + constant 3 long-suffix L punctuator ; char char punctuator * identifier c punctuator = identifier x punctuator ; punctuator
Der Übersetzer: Frontend: Parser { int x = 17 + 3 L ; { int x = 17 + 3L; char *c = x; char * c = x ;
Der Übersetzer: Frontend: Parser declaration type-specifier declarator { int x = 17 + 3 L ; { int x = 17 + 3L; char *c = x; char * c = x ;
Der Übersetzer: Frontend: Parser declaration initializer type-specifier declarator int-constant int-constant { int x = 17 + 3 L ; { int x = 17 + 3L; char *c = x; char * c = x ;
Der Übersetzer: Frontend: Parser declaration initializer type-specifier declarator int-constant int-constant { int x = 17 + 3 L ; { int x = 17 + 3L; char *c = x; declaration type-specifier declarator initializer char * c = x ;
Der Übersetzer: Frontend: Parser declaration compound-statement initializer type-specifier declarator int-constant int-constant { int x = 17 + 3 L ; { int x = 17 + 3L; char *c = x; declaration type-specifier declarator initializer char * c = x ;
Der Übersetzer: Frontend: Parser declaration compound-statement initializer type-specifier declarator int-constant int-constant { int x = 17 + 3 L ; { int x = 17 + 3L; char *c = x; declaration type-specifier declarator initializer char * c = x ; Parser erzeugt AST (Abstract Syntax Tree)
Frontend: Namensanalyse declaration compound-statement initializer type-specifier declarator int-constant int-constant int x 17 + 3 L declaration type-specifier char declarator * c initializer x
Frontend: Namensanalyse declaration compound-statement initializer type-specifier declarator int-constant int-constant int x 17 + 3 L declaration type-specifier char declarator * c initializer x Verwendungen von Namen werden an ihre Definitionen gebunden
Frontend: Typanalyse (1/2) declaration compound-statement initializer type-specifier int declarator x int int-constant int 17 + int-constant long 3 L declaration type-specifier char declarator * c char* initializer x int
Frontend: Typanalyse (1/2) declaration compound-statement initializer type-specifier int declarator x int int-constant int 17 + int-constant long 3 L declaration type-specifier char declarator * c char* initializer x int Typen werden an Namen gebunden
Frontend: Typanalyse (2/2) declaration compound-statement initializer type-specifier int declarator x int int-constant int 17 + int-constant long 3 L Γ τ(declarator) declaration = τ(initializer) type-specifier char declarator * c char* initializer x int
Frontend: Typanalyse (2/2) declaration compound-statement initializer type-specifier int declarator x int int-constant int-constant int long 17 + 3 L Γ τ(declarator) declaration = τ(initializer) type-specifier Typfehler: char* int declarator initializer char * c char* x int Typregeln erzwingen Typkorrektheit
Der Übersetzer: Middle-End Frontend erzeugt Zwischenform des Programmes
Der Übersetzer: Middle-End Frontend erzeugt Zwischenform des Programmes Zwischenform hilft bei Programmanalyse: Welche Programmteile hängen voneinander ab? (Kontrollfluß/Daten) Welche Programmteile benötigen welche Ressourcen?
Der Übersetzer: Middle-End Frontend erzeugt Zwischenform des Programmes Zwischenform hilft bei Programmanalyse: Welche Programmteile hängen voneinander ab? (Kontrollfluß/Daten) Welche Programmteile benötigen welche Ressourcen? Middle-End optimiert Programmrepräsentierung Optimierte Zwischenform geht an Backend
Der Übersetzer: Optimierungen Optimierung = Programmanalyse a + Transformation t Gegeben Programm p. Falls a(p): Ergebnis von p = Ergebnis von t(p)
Der Übersetzer: Optimierungen Optimierung = Programmanalyse a + Transformation t Gegeben Programm p. Falls a(p): Ergebnis von p = Ergebnis von t(p) Beispiel: a[0] = 0; // A0 a[1] = a[0] ; // A1 f(); a[2] = a[0] ; // A2
Der Übersetzer: Optimierungen Optimierung = Programmanalyse a + Transformation t Gegeben Programm p. Falls a(p): Ergebnis von p = Ergebnis von t(p) Beispiel: a[0] = 0; // A0 a[1] = a[0] ; // A1 f(); a[2] = a[0] ; // A2 A1 auf a[1] = 0 änderbar Korrekte Programmanalysen sind konservativ: Wenn ich nicht weiß, ob ich darf, darf ich nicht
Der Übersetzer: Optimierungen Optimierung = Programmanalyse a + Transformation t Gegeben Programm p. Falls a(p): Ergebnis von p = Ergebnis von t(p) Beispiel: a[0] = 0; // A0 a[1] = a[0] ; // A1 f(); a[2] = a[0] ; // A2 A1 auf a[1] = 0 änderbar A2 nur auf a[2] = 0 änderbar, wenn f() nicht nach a[0] schreibt Korrekte Programmanalysen sind konservativ: Wenn ich nicht weiß, ob ich darf, darf ich nicht
Der Übersetzer: Backend Backend erzeugt Assembler- oder Maschinencode Unterstützt oft verschiedene Prozessoren und Aufrufkonventionen Wesentliche Aufgaben:
Der Übersetzer: Backend Backend erzeugt Assembler- oder Maschinencode Unterstützt oft verschiedene Prozessoren und Aufrufkonventionen Wesentliche Aufgaben: Registerauswahl: Welche Variablen in welche Register? (gesichert, temporär, Spezialregister) Andere Variablen in den Stapelspeicher
Der Übersetzer: Backend Backend erzeugt Assembler- oder Maschinencode Unterstützt oft verschiedene Prozessoren und Aufrufkonventionen Wesentliche Aufgaben: Registerauswahl: Welche Variablen in welche Register? (gesichert, temporär, Spezialregister) Andere Variablen in den Stapelspeicher Befehlsauswahl: Effiziente Befehle suchen, um Ausdrücke abzubilden (Direkt-Operation, Register-Operation usw.)
Der Übersetzer: Backend Backend erzeugt Assembler- oder Maschinencode Unterstützt oft verschiedene Prozessoren und Aufrufkonventionen Wesentliche Aufgaben: Registerauswahl: Welche Variablen in welche Register? (gesichert, temporär, Spezialregister) Andere Variablen in den Stapelspeicher Befehlsauswahl: Effiziente Befehle suchen, um Ausdrücke abzubilden (Direkt-Operation, Register-Operation usw.) Architekturspezifische Optimierungen
Zusammenfassung: Der Übersetzer Drei Übersetzerphasen: Frontend liest Programm ein Typanalyse und Fehlersuche Erzeugt Zwischenrepräsentation Middle-End optimiert Zwischenrepräsentation Kann übersprungen werden (schnelle Übersetzung, langsamer Code) Backend bildet Zwischenrepräsentation auf Assembler/Maschinencode ab Evtl. kleinere maschinenspezifische Optimierungen
Qualifizierer in C extern int sprintf(char * restrict s, const char * restrict format,...);
Qualifizierer in C extern int sprintf(char * restrict s, const char * restrict format,...); Typen in C können qualifiziert werden Qualifizierer wie const sind Schlüsselwörter, die als Teil von Typspezifikationen angegeben werden Geben Hinweise auf geplante Verwendung von Variablen und Funktionen Wirkung: Einfluß auf Programmoptimierung: Helfen oder blockieren Programmanalysen für Optimierungen Einfluß auf Semantik: Typanalyse kann Fehler markieren, wenn Hinweise auf Verwendung der tatsächlichen Verwendung widersprechen
Übersicht: Qualifizierer const Stellt sicher, daß Wert nicht verändert werden kann Erlaubt zusätzliche Optimierungen inline Erbittet Einbetten ( inlining ) von Funktionskörpern in Aufrufer Kann u.u. Programm beschleunigen restrict (auf Zeiger) Verspricht, daß kein aliasing stattfinded Erleichtert Zwischenspeichern von Werten Kann u.u. Programm beschleunigen volatile Zwingt Übersetzer, Variable im Speicher zu halten Einsatzgebiete: Gerätetreiber, nicht-lokaler Kontrollfluß
Qualifizierer: const Variablen, die nach Initialisierung nicht beschrieben werden, sind Konstanten Qualifizierer const const int i = v + 1; Eigenschaft wird von Typüberprüfung erzwungen: Versuchte Zuweisung ist Programmfehler const int x = 5; int f(int y) { return x * y;
Qualifizierer: const Variablen, die nach Initialisierung nicht beschrieben werden, sind Konstanten Qualifizierer const const int i = v + 1; Eigenschaft wird von Typüberprüfung erzwungen: Versuchte Zuweisung ist Programmfehler const int x = 5; int f(int y) { return x * y; // Mit Konstante int f(int y) { return 5 * y;
Qualifizierer: const C-Quellcode const int x = 5; int f(int y) { return x * y; const ermöglicht Optimierungen im Übersetzer
Qualifizierer: const C-Quellcode const int x = 5; int f(int y) { return x * y; mit const: f:sll $v0, $a0, 2 jr $ra addu $v0, $v0, $a0 const ermöglicht Optimierungen im Übersetzer
Qualifizierer: const C-Quellcode const int x = 5; int f(int y) { return x * y; mit const: f:sll $v0, $a0, 2 jr $ra addu $v0, $v0, $a0 ohne const: f:lw $v0, x($gp) nop mult $a0, $v0 mflo $v0 jr $ra nop const ermöglicht Optimierungen im Übersetzer
Qualifizierer: const C-Quellcode const int x = 5; int f(int y) { return x * y; mit const: Mit const: effizienter Ohne const: generischer f:sll $v0, $a0, 2 jr $ra addu $v0, $v0, $a0 ohne const: f:lw $v0, x($gp) nop mult $a0, $v0 mflo $v0 jr $ra nop const ermöglicht Optimierungen im Übersetzer
Qualifizierer: const und Zeiger Zwei relevante Zugriffsrechte für int *p: p += 1: Zeigerarithmetik *p += 1: Inhalt der referenzierten Speicherstelle erhöhen Optionen: int *p Wir dürfen sowohl p als auch *p modifizieren
Qualifizierer: const und Zeiger Zwei relevante Zugriffsrechte für int *p: p += 1: Zeigerarithmetik *p += 1: Inhalt der referenzierten Speicherstelle erhöhen Optionen: int *p Wir dürfen sowohl p als auch *p modifizieren const int *p int const *p Wir dürfen *p modifizieren, aber nicht p
Qualifizierer: const und Zeiger Zwei relevante Zugriffsrechte für int *p: p += 1: Zeigerarithmetik *p += 1: Inhalt der referenzierten Speicherstelle erhöhen Optionen: int *p Wir dürfen sowohl p als auch *p modifizieren const int *p int const *p Wir dürfen *p modifizieren, aber nicht p int * const p Wir dürfen p modifizieren, aber nicht *p
Qualifizierer: const und Zeiger Zwei relevante Zugriffsrechte für int *p: p += 1: Zeigerarithmetik *p += 1: Inhalt der referenzierten Speicherstelle erhöhen Optionen: int *p Wir dürfen sowohl p als auch *p modifizieren const int *p int const *p Wir dürfen *p modifizieren, aber nicht p int * const p Wir dürfen p modifizieren, aber nicht *p const int * const p int const * const p Wir dürfen weder p noch *p modifizieren
Qualifizierer: inline Funktionsaufrufe können hohe Kosten haben: Stapelspeicher beschreiben Registerwahl einschränken Stapelspeicher lesen Inlining ist Optimierung, die Funktionskörper einbettet: int f(int x) { return 2 + x; void g(int argc, int *args) { for (int i = 0; i < argc; i++) args[i] = f(args[i]);
Qualifizierer: inline Funktionsaufrufe können hohe Kosten haben: Stapelspeicher beschreiben Registerwahl einschränken Stapelspeicher lesen Inlining ist Optimierung, die Funktionskörper einbettet: int f(int x) { return 2 + x; void g(int argc, int *args) { for (int i = 0; i < argc; i++) args[i] = f(args[i]); // Mit Inlining void g(int argc, int *args) { for (int i = 0; i < argc; i++) args[i] = 2 + (args[i]);
Qualifizierer: ohne inline int f(int x) { return 2 + x; void g(int argc, int *args) { for (int i = 0; i < argc; i++) { args[i] = f(args[i]);
Qualifizierer: ohne inline int f(int x) { return 2 + x; void g(int argc, int *args) { for (int i = 0; i < argc; i++) { args[i] = f(args[i]); g: addiu $sp, $sp, -40 sw $ra, $s0-$s3...($sp) move $s2, $a0 blez $a0, L6 move $s3, $a1 move $s1, $zero L5:sll $s0, $s1, 2 addu $s0, $s3, $s0 lw $a0, 0($s0) jal f addiu $s1, $s1, 1 slt $v1, $s1, $s2 bne $v1, $zero, L5 sw $v0, 0($s0) L6:lw $ra, $s0-$s3...($sp) jr $ra addiu $sp, $sp, 40
Qualifizierer: mit inline inline int f(int x) { return 2 + x; void g(int argc, int *args) { for (int i = 0; i < argc; i++) { args[i] = f(args[i]);
Qualifizierer: mit inline inline int f(int x) { return 2 + x; void g(int argc, int *args) { for (int i = 0; i < argc; i++) { args[i] = f(args[i]); g: blez $a0, L9 move $a3, $a0 move $a2, $zero L5:sll $v0, $a2, 2 addu $v0, $a1, $v0 lw $v1, 0($v0) addiu $a2, $a2, 1 addiu $v1, $v1, 2 slt $a0, $a2, $a3 bne $a0, $zero, L5 sw $v1, 0($v0) L9:jr $ra nop
Qualifizierer: mit inline inline int f(int x) { return 2 + x; void g(int argc, int *args) { for (int i = 0; i < argc; i++) { args[i] = f(args[i]); g: blez $a0, L9 move $a3, $a0 move $a2, $zero L5:sll $v0, $a2, 2 addu $v0, $a1, $v0 lw $v1, 0($v0) addiu $a2, $a2, 1 addiu $v1, $v1, 2 slt $a0, $a2, $a3 bne $a0, $zero, L5 sw $v1, 0($v0) L9:jr $ra nop Kein expliziter f-aufruf Stapelspeicherverwaltung unnötig
Inlining in der Praxis Inlining ist eine der wichtigsten Optimierungen Übersetzer inlininen meist heuristisch: Funktionen mit kleinem Funktionskörper (kleiner als Inlining-Schwelle) (C, C++) Funktionen, die als Funktionszeiger übergeben werden (OCaml) Funktionen, die häufig von der gleichen Stelle aufgerufen werden (Java)
Inlining in der Praxis Inlining ist eine der wichtigsten Optimierungen Übersetzer inlininen meist heuristisch: Funktionen mit kleinem Funktionskörper (kleiner als Inlining-Schwelle) (C, C++) Funktionen, die als Funktionszeiger übergeben werden (OCaml) Funktionen, die häufig von der gleichen Stelle aufgerufen werden (Java) C11 6.7.4.6: Exakte Bedeutung von inline ist implementierungsabhängig gcc: Der inline-qualifizierer erhöht die Inlining-Schwelle für diese Funktion
Inlining in der Praxis Inlining ist eine der wichtigsten Optimierungen Übersetzer inlininen meist heuristisch: Funktionen mit kleinem Funktionskörper (kleiner als Inlining-Schwelle) (C, C++) Funktionen, die als Funktionszeiger übergeben werden (OCaml) Funktionen, die häufig von der gleichen Stelle aufgerufen werden (Java) C11 6.7.4.6: Exakte Bedeutung von inline ist implementierungsabhängig gcc: Der inline-qualifizierer erhöht die Inlining-Schwelle für diese Funktion Meist ist explizites inline nicht nötig
Qualifizierer: restrict int maxsum(int *summe, int *a, int len) { *summe = 0; int max = 0; while (len--) { *summe += a[len]; if (a[len] > max) max = a[len]; return max; Eingabe: Array a mit len Elementen Berechnet: *summe = a[0] +... + a[len-1] return max(a[0]... a[len-1])
Qualifizierer: ohne restrict int maxsum(int *summe, int *a, int len) { *summe = 0; int max = 0; while (len--) { *summe += a[len]; //A1 if (a[len] > max) //A2 max = a[len]; //A3 return max; sw $zero, 0($a0) beq $a2, $zero, L5 addiu $v1, $a2, -1 move $a2, $zero move $v0, $zero L4:sll $a3, $v1, 2 addu $a3, $a1, $a3 lw $t0, 0($a3) # lade a[len] nop addu $a2, $a2, $t0 sw $a2, 0($a0) # schreibe *summe lw $a3, 0($a3) # lade a[len] nop slt $t0, $v0, $a3 beq $t0, $zero, L3 addiu $t1, $v1, -1 move $v0, $a3 L3:bne $v1, $zero, L4 move $v1, $t1 jr $ra nop L5:jr $ra move $v0, $zero
Qualifizierer: restrict int maxsum(int *summe, int *a, int len) { *summe = 0; int max = 0; while (len--) { *summe += a[len]; //A1 if (a[len] > max) //A2 max = a[len]; //A3 return max; Was, wenn summe in *a zeigt? aliasing: Mehrere Zeiger zeigen auf gleiches Objekt int a[3]; int x = maxsum(&a[1], a, 3); a = summe summe ist Alias von (a+1) Bestimmte Optimierungen werden durch konservative Annahmen des Übersetzers verhindert
Qualifizierer: ohne restrict int maxsum(int *summe, int *a, int len) { *summe = 0; int max = 0; while (len--) { *summe += a[len]; //A1 if (a[len] > max) //A2 max = a[len]; //A3 return max; sw $zero, 0($a0) beq $a2, $zero, L5 addiu $v1, $a2, -1 move $a2, $zero move $v0, $zero L4:sll $a3, $v1, 2 addu $a3, $a1, $a3 lw $t0, 0($a3) # lade a[len] nop addu $a2, $a2, $t0 sw $a2, 0($a0) # schreibe *summe lw $a3, 0($a3) # lade a[len] nop slt $t0, $v0, $a3 beq $t0, $zero, L3 addiu $t1, $v1, -1 move $v0, $a3 L3:bne $v1, $zero, L4 move $v1, $t1 jr $ra nop L5:jr $ra move $v0, $zero
Qualifizierer: mit restrict int maxsum(int * restrict summe, int * restrict a, int len) { *summe = 0; int max = 0; while (len--) { *summe += a[len]; if (a[len] > max) max = a[len]; return max;
Qualifizierer: mit restrict int maxsum(int * restrict summe, int * restrict a, int len) { *summe = 0; int max = 0; while (len--) { *summe += a[len]; if (a[len] > max) max = a[len]; return max; sw $zero, 0($a0) beq $a2, $zero, L5 addiu $v1, $a2, -1 move $a3, $zero move $v0, $zero L4:sll $a2, $v1, 2 addu $a2, $a1, $a2 lw $a2, 0($a2) # lade a[len] addiu $t1, $v1, -1 addu $a3, $a3, $a2 # 1 lw, 2 nop entfernt slt $t0, $v0, $a2 beq $t0, $zero, L3 sw $a3, 0($a0) # schreibe *summe move $v0, $a2 L3:bne $v1, $zero, L4 move $v1, $t1 jr $ra nop L5:jr $ra move $v0, $zero
Qualifizierer: restrict restrict verspricht, daß Aliasing unmöglich ist
Qualifizierer: restrict restrict verspricht, daß Aliasing unmöglich ist Compilerannahme: Aliasing zwischen Zeigern τ *a und τ *b möglich: a restrict a b ja ja restrict b ja nein
Qualifizierer: restrict restrict verspricht, daß Aliasing unmöglich ist Compilerannahme: Aliasing zwischen Zeigern τ *a und τ *b möglich: a restrict a b ja ja restrict b ja nein Kein Aliasing: Von Zeigern geladene Werte können länger in Registern zwischengespeichert werden... = *a; // *a lesen *b = z; // *b schreiben... = *a; // *a zwischengespeichert gdw restrict a und b
Qualifizierer: volatile Optimierer darf normalerweise: Variablen in Register legen Speicherzugriffreihenfolge umordnen Beide Änderungen haben normalerweise keine Auswirkung auf das sichtbare Programmverhalten
Qualifizierer: volatile Optimierer darf normalerweise: Variablen in Register legen Speicherzugriffreihenfolge umordnen Beide Änderungen haben normalerweise keine Auswirkung auf das sichtbare Programmverhalten Problematisch für: Gerätetreiber: Speicheradressen auf Geräteregister abgebildet Zugriffreihenfolge gemäß Hardwareprotokoll Plötzliche Sprünge (Ausnahmebehandlung): Registerinhalte gehen verloren
Qualifizierer: volatile volatile: Variable wird nicht in Registern gesichert Reihenfolge der volatile-zugriffe wird immer beibehalten int c; volatile int va, vb; va = 0; c = 1; vb = 2; // nach va
Qualifizierer: volatile volatile: Variable wird nicht in Registern gesichert Reihenfolge der volatile-zugriffe wird immer beibehalten int c; volatile int va, vb; va = 0; c = 1; vb = 2; // nach va Mögliche Schreibreihenfolgen va = 0; c = 1; va = 0; c = 1; va = 0; vb = 2; vb = 2; vb = 2; c = 1;
Qualifizierer: volatile volatile: Variable wird nicht in Registern gesichert Reihenfolge der volatile-zugriffe wird immer beibehalten int c; volatile int va, vb; va = 0; c = 1; vb = 2; // nach va Mögliche Schreibreihenfolgen va = 0; c = 1; va = 0; c = 1; va = 0; vb = 2; vb = 2; vb = 2; c = 1; Vorsicht: volatile alleine reicht nicht, um nebenläufige Prozesse zu synchronisieren
Automatische Optimierungen Übersetzer können viele Arten von Optimierungen durchführen: Inlining Auswertung konstanter Ausdrücke Rückhaltung von Speicherinhalten...
Automatische Optimierungen Übersetzer können viele Arten von Optimierungen durchführen: Inlining Auswertung konstanter Ausdrücke Rückhaltung von Speicherinhalten... Programmierer müssen sich nicht um Optimierungen kümmern, die der Übersetzer durchführen kann
Grundlegende Optimierungen Konstantenfaltung (constant folding) Konstantenpropagierung (constant propagation) Eliminierung gemeinsamer Teilausdrücke (common subexpression elimination) Eliminierung von totem Code (dead code elimination) Schleifeninvariante Codeverschiebung (loop-invariant code motion)
Konstantenfaltung Vor Optimierung: int x = 3 + 7;
Konstantenfaltung Vor Optimierung: int x = 3 + 7; Nach Optimierung: int x = 10; Berechnungen über Konstanten werden ausgewertet
Konstantenpropagierung Vor Optimierung: int x = 7;... // x wird nicht verändert for (int i = 0; i < x; i++) {... // x wird nicht verändert
Konstantenpropagierung Vor Optimierung: int x = 7;... // x wird nicht verändert for (int i = 0; i < x; i++) {... // x wird nicht verändert Nach Optimierung: int x = 7;... for (int i = 0; i < 7; i++) {... Variablen mit konstantem Inhalt werden substituiert
Eliminierung gemeinsamer Teilausdrücke Vor Optimierung: int x = v * v + 17; int y = v * v + 23;
Eliminierung gemeinsamer Teilausdrücke Vor Optimierung: int x = v * v + 17; int y = v * v + 23; Nach Optimierung: const int v2 = v * v; int x = v2 + 17; int y = v2 + 23; Mehrfach hintereinander verwendete Teilausdrücke werden extrahiert und vorberechnet
Eliminierung von totem Code Vor Optimierung: char *s = "C99 oder neuer"; if ( STDC_VERSION < 199901L) { s = "vor C99";
Eliminierung von totem Code Vor Optimierung: char *s = "C99 oder neuer"; if ( STDC_VERSION < 199901L) { s = "vor C99"; Nach Optimierung: char *s = "C99 oder neuer"; // Eliminiert Blöcke mit nicht erfüllbaren Vorbedingungen werden entfernt
Abhängigkeiten zwischen Optimierungen Optimierungen beeinflussen sich gegenseitig: Eliminierung von totem Code muß Bedingungen auswerten: Abhängigkeit: Konstantenfaltung / Konstantenpropagierung Konstantenfaltung muß Konstanten sehen: Abhängigkeit: Konstantenpropagierung Konstantenpropagierung muß wissen, daß Ausdrücke konstant sind: Abhängigkeit: Eliminierung von totem Code (De)aktivierung einer Optimierung kann andere Optimierungen indirekt beeinflussen
Schleifeninvariante Codeverschiebung Vor Optimierung: for (int i = 0; i <= 1000; i++) { int v = p * p; a[i] += v;
Schleifeninvariante Codeverschiebung Vor Optimierung: for (int i = 0; i <= 1000; i++) { int v = p * p; a[i] += v; Nach Optimierung: int v = p * p; for (int i = 0; i <= 1000; i++) { a[i] += v; Berechnungen, die in jedem Schleifenschritt das gleiche Ergebnis produzieren, werden vor die Schleife geschoben
Komplexe Optimierungen Schleifenteilung (loop splitting) Schleifenabwicklung (loop unrolling) Schleifenfusion (loop fusion) Stärkereduktion (strength reduction) Stapel-inlining (stack inlining) Inlining
Schleifenteilung Vor Optimierung: int v = 1; for (int i = 0; i <= 1000; i++) { a[i] += v; v = p * p; //A v ist nicht schleifen-invariant: A kann nicht verschoben werden
Schleifenteilung Vor Optimierung: int v = 1; for (int i = 0; i <= 1000; i++) { a[i] += v; v = p * p; //A v ist nicht schleifen-invariant: A kann nicht verschoben werden Nach Optimierung: int v = 1; for (int i = 0; i == 0; i++) { a[i] += v; v = p * p; //A for (int i = 1; i <= 1000; i++) { a[i] += v; v = p * p; //A Schleifen werden in Teilschleifen aufgespalten, um weitere Optimierungen zu erlauben (hier: Verschieben von A)
Schleifenabwicklung (1/2) Vor Optimierung: for (int i = 0; i < 4; i++) { a[i] += i; Nach Optimierung:
Schleifenabwicklung (1/2) Vor Optimierung: for (int i = 0; i < 4; i++) { a[i] += i; Nach Optimierung: a[0] += 0; a[1] += 1; a[2] += 2; a[3] += 3; Schleifenkörper wird kopiert, um Vergleiche/Sprünge zu sparen und weitere Optimierungen zu ermöglichen
Schleifenabwicklung (2/2) Vor Optimierung: for (int i = 0; i < 4000; i++) { a[i] += i;
Schleifenabwicklung (2/2) Vor Optimierung: for (int i = 0; i < 4000; i++) { a[i] += i; Nach Optimierung: for (int i = 0; i < 4000; i+=4) { a[i] += i; a[i+1] += i+1; a[i+2] += i+2; a[i+3] += i+3; Partielle Schleifenabwicklung für lange Schleifen, insbesondere zusammen mit Schleifenteilung
Schleifenfusion Vor Optimierung: for (int i = 0; i < 4000; i++) { a[i] += i; for (int i = 0; i < 4000; i++) { b[i] += a[i];
Schleifenfusion Vor Optimierung: for (int i = 0; i < 4000; i++) { a[i] += i; for (int i = 0; i < 4000; i++) { b[i] += a[i]; Nach Optimierung: for (int i = 0; i < 4000; i++) { a[i] += i; // Zusammengefuegt b[i] += a[i]; Schleifenkörper, die über die gleichen Daten iterieren, werden zusammengefügt
Stärkereduktion Vor Optimierung: for (int i = 0; i < 1000; i++) { a[i * 13] = 0;
Stärkereduktion Vor Optimierung: for (int i = 0; i < 1000; i++) { a[i * 13] = 0; Nach Optimierung: int i13 = 0; for (int i = 0; i < 1000; i++) { a[ i13] = 0; i13 += 13; Ersetze teure Operationen in Schleife durch billigere inkrementelle Operationen
Stapel-Inlining Vor Optimierung: int *a = (int *) malloc(sizeof(int) * 4); lies_daten(a); int i = a[0] + a[3]; free(a);
Stapel-Inlining Vor Optimierung: int *a = (int *) malloc(sizeof(int) * 4); lies_daten(a); int i = a[0] + a[3]; free(a); Nach Optimierung: int a[4]; lies_daten(a); int i = a[0] + a[3];
Stapel-Inlining Vor Optimierung: int *a = (int *) malloc(sizeof(int) * 4); lies_daten(a); int i = a[0] + a[3]; free(a); Nach Optimierung: int a[4]; lies_daten(a); int i = a[0] + a[3]; Einschränkung: Allozierung muß klein sein, wenn Stapelspeicher beschränkt ist
Stapel-Inlining Vor Optimierung: int *a = (int *) malloc(sizeof(int) * 4); lies_daten(a); int i = a[0] + a[3]; free(a); Nach Optimierung: int a[4]; lies_daten(a); int i = a[0] + a[3]; Einschränkung: Allozierung muß klein sein, wenn Stapelspeicher beschränkt ist Allozierung auf Ablagespeicher wird in Allozierung auf Stapelspeicher umgewandelt
Zusammenfassung: Optimierungen (1/2) Konstantenfaltung: Berechnungen auf Konstanten werden fertiggerechnet Konstantenpropagierung: Konstante Variablen werden substituiert Eliminierung gemeinsamer Teilausdrücke: Mehrfach verwendete Teilausdrücke werden extrahiert, nur ein Mal berechnet Eliminierung von totem Code: Nicht ausführbare Programmteile werden gelöscht Schleifeninvariante Codeverschiebung: Unveränderliche Berechnungen in Schleifen werden aus Schleife gehoben
Zusammenfassung: Optimierungen (2/2) Schleifenteilung: Schleife wird in mehrere Schleifen gespalten Schleifenabwicklung: Mehrere Kopien des Schleifenkörpers werden ausgeführt Schleifenfusion: Schleifen über gleiche Daten werden zusammengeklebt Stärkereduktion: Berechnungen mit teuren Operationen in Schleifen werden mit billigeren Operationen inkrementalisiert Stapel-inlining: malloc wird auf den Stapelspeicher verlegt Inlining: Funktionskörper werden in Aufrufstelle kopiert
Die Grenzen von Optimierungen Einschränkungen der Implementierung Fehlende Unterstützung Berechenbarkeit Programmgleicheit ist unentscheidbar Sprachsemantik Programmverhalten muß Sprachspezifikation entsprechen Überspezifizierung Programmierer-Spezifikation schränkt Optimierer unnötig ein Beschränktes Wissen Übersetzer sieht nicht genug Informationen
Optimierungsgrenzen durch Berechenbarkeitsgrenzen Gegeben: zwei Sortierfunktionen quicksort bubble_sort Beide sind semantisch äquivalent: gleiche Wirkung, soweit durch Sprachsemantik definiert Kann der Übersetzer automatisch eine durch die andere Funktion ersetzen? Zur automatischen Optimierung muß Übersetzer berechnen können, daß die Funktionen äquivalent sind
Optimierungsgrenzen durch Berechenbarkeitsgrenzen Gegeben: zwei Sortierfunktionen quicksort bubble_sort Beide sind semantisch äquivalent: gleiche Wirkung, soweit durch Sprachsemantik definiert Kann der Übersetzer automatisch eine durch die andere Funktion ersetzen? Zur automatischen Optimierung muß Übersetzer berechnen können, daß die Funktionen äquivalent sind Programmgleichheit ist nicht berechenbar: Diese Optimierung ist i.a. nicht möglich
Optimierungsgrenzen durch Sprachsemantik (1/2) a struct { short a; int w1; short b; int w2; s; // 2 Bytes // 4 Bytes // 2 Bytes // 4 Bytes w1 b w2
Optimierungsgrenzen durch Sprachsemantik (1/2) a a b struct { short a; int w1; short b; int w2; s; // 2 Bytes // 4 Bytes // 2 Bytes // 4 Bytes w1 b w1 w2 w2? Wir könnten Speicher sparen, wenn wir die Felder umsortieren würden
Optimierungsgrenzen durch Sprachsemantik (2/2) Darf der Übersetzer diese Umsortierung selbst durchführen? C11 Sprachspezifikation, 6.7.2.1.15: Within a structure object, the [...] members [...] have addresses that increase in the order in which they are declared. [...]
Optimierungsgrenzen durch Sprachsemantik (2/2) Darf der Übersetzer diese Umsortierung selbst durchführen? C11 Sprachspezifikation, 6.7.2.1.15: Within a structure object, the [...] members [...] have addresses that increase in the order in which they are declared. [...] Sprachspezifikation verbietet Optimierung
Optimierungsgrenzen durch Überspezifikation Überspezifikation: Programm beinhaltet Details, die 1. Das Berechnungsergebnis nicht beeinflussen, aber 2. Zur Berechnung nicht nötig sind Überspezifikation verhindert Optimierungen for (volatile int x = 1; x < 10; x++) { a[x] += 2; // kein Grund für volatile
Optimierungsgrenzen durch Überspezifikation Überspezifikation kann indirekt stattfinden: int helfer(int i) { return... int f(int i) {...helfer... Inlining von helfer möglich
Optimierungsgrenzen durch Überspezifikation Überspezifikation kann indirekt stattfinden: int helfer(int i) { return... int f(int i) {...helfer... Inlining von helfer möglich Aber: helfer kann nach inlining nicht eliminiert werden: Ein externes Modul könnte auf helfer zugreifen helfer ist nicht static, damit global im Sinne des Binders
Optimierungsgrenzen durch Überspezifikation Überspezifikation kann indirekt stattfinden: int helfer(int i) { return... int f(int i) {...helfer... Inlining von helfer möglich Aber: helfer kann nach inlining nicht eliminiert werden: Ein externes Modul könnte auf helfer zugreifen helfer ist nicht static, damit global im Sinne des Binders Überspezifikation durch fehlende Angaben möglich
Zusammenfassung: Einschränkungen der Optimierungen Implementierungsbeschränkungen Berechenbarkeit Sprachsemantik und Überspezifizierung Beschränktes Wissen
Wissen des Optimierers Optimierung: Programmanalyse a + Transformation t Wissen von a: t ist korrekt Optimierung t ist inkorrekt keine Optimierung nicht genug Informationen keine Optimierung
Wissen des Optimierers Optimierung: Programmanalyse a + Transformation t Wissen von a: t ist korrekt Optimierung t ist inkorrekt keine Optimierung nicht genug Informationen keine Optimierung Desto mehr der Optimierer weiß, desto besser kann er optimieren
Modul-Optimierung Modul-Optimierung: Normalfall Jedes Modul (Programmdatei) wird einzeln optimiert Wissen des Optimierers: Inhalt eines Moduls Modul-0 Modul-1 Modul-2 Übersetzer Übersetzer Übersetzer Binder Ausführung
Gesamtprogrammoptimierung whole-program optimisation Alle Module werden gleichzeitig optimiert Wissen des Optimierers: Inhalt aller Module Das Programm ist vollständig Modul-0 Modul-1 Modul-2 Übersetzer Binder Ausführung Wissen, daß das Programm vollständig ist, erlaubt viele neue Optimierungen, die sonst nur mit const oder static möglich wären
Bindezeit-Optimierung Modul-0 Modul-1 Modul-2 link-time optimisation Binder optimiert Module mit Informationen aus anderen Modulen Wissen des Optimierers: Teile des Modulwissens Übersetzer Übersetzer Übersetzer Binder Ausführung
Optimierung mit Laufzeitprofil profile-directed optimisation feedback-driven optimisation Übersetzer verwenden Laufzeitmessungen, um Programme zu beschleunigen Zusätzlich zu Modul- /Ganzprogrammoptimierung Wissen des Optimierers: Schleifendurchlaufstatistiken Sprungwahrscheinlichkeiten Oft ausgeführter Code und alles, was er vorher wußte Modul-0 Modul-1 Modul-2 Übersetzer Übersetzer Übersetzer Binder Ausführung Programmprofil
Optimierung: Zusammenfassung Optimierungen können zu verschiedenen Zeitpunkten stattfinden: Optimierung bei Modulübersetzung: Vorteile: Relativ schnelle Übersetzung Nachteile: Nur beschränktes Wissen Ganzprogramm-Optimierung: Vorteile: Sehr effektiv Nachteile: Sehr langsam, manchmal unmöglich: Unterstützt keine dynamisch geladenen Module Optimierung braucht viel Arbeitsspeicher Bindezeit-Optimierung: Vorteile: Optimierung jenseits von Modulgrenzen Nachteile: Große Objektdateien, langsames Binden Optimierung mit Laufzeitprofil: Vorteile: Muß Programmverhalten nicht raten Nachteile: Typischer Programmlauf nötig, hoher Aufwand