240 likes | 318 Views
Optimierungstechniken in modernen Compilern. Einführung. Klassifizierung von Computersystemen. Klassifizierung nach Flynn:. Single Instruction Single Data (SISD): Ein einzelner Prozessor führt einen Befehlsstrom auf Daten in einem Datenspeicher aus.
E N D
Optimierungstechniken in modernen Compilern Einführung
Klassifizierung von Computersystemen • Klassifizierung nach Flynn: Single Instruction Single Data (SISD): Ein einzelner Prozessor führt einen Befehlsstrom auf Daten in einem Datenspeicher aus. Single Instruction Multiple Data (SIMD): Ein einzelner Befehl steuert mehrere Ausführungseinheiten, von denen jede Zugriff auf einen lokalen Speicher hat. Es existiert ein Steuerfluss im Programm. Multiple Instruction Single Data (MISD): Eine Sequenz von Daten aus demselben Speicher wird von mehreren Ausführungseinheiten bearbeitet, von denen jede einen eigenen Steuerfluss besitzt. Multiple Instruction Multiple Data (MIMD): Mehrere Steuerflüsse steuern mehrere Ausführungseinheiten, von denen jede Zugriff auf einen lokalen Speicher hat. Es existieren mehrere Steuerflüsse im Programm. Alle Ausführungseinheiten greifen auf denselben Datenspeicher zu. Jede Ausführungseinheiten besitzt einen lokalen Datenspeicher (z.B. einen Cache).
Einordnung von Prozessorarchitekturen Prozessororganisation SISD SIMD MISD MIMD Uniprocessor Vector Processor Array Processor Geclusterte VLIWs Shared Memory Distributed Memory TMS320C62x Scalar Superscalar Symmetric Multiprocessor (SMP) Non-Uniform Memory Access (NUMA) Cluster i386, i486 Dynamic Scheduled Static Scheduled Dual-Core Pentium Pentium Dynamic Allocation Static Allocation Itanium VLIW: Transmeta Crusoe, Philips TriMedia
Skalarer Prozessor ohne Pipeline • Typische Optimierungen des Compilers: • Registerdruck minimieren • Geeigneten Zielcode auswählen Schematischer Aufbau: Hoher Registerdruck: Geringer Registerdruck: Steuerwerk Registerbank ldm (r8) r0 ldm (r9) r1 ldm (r10) r2 add r0,r1 r0 add r0,r2 r0 … use r8,r9,r10 ldm (r8) r0 ldm (r9) r1 add r0,r1 r0 ldm (r10) r1 add r0,r1 r0 … use r8,r9,r10 Speicher Speicher ALU Speicher Schlechte Codeauswahl: Bessere Codeauswahl: ldc #8 r0 add r0,r8 r0 ldm (r0) r1 ldm (r8+8) r1
Skalarer Prozessor mit Befehlspipeline • Typische Optimierungen des Compilers: • Registerdruck minimieren • Geeigneten Zielcode auswählen • Pipeline-Hazards vermeiden Schematischer Aufbau: Schlechte Befehlsanordnung: Bessere Befehlsanordnung: Speicher Steuerwerk FE/DE ldm (r8) r0 ldm (r9) r1 stall stall add r0,r1 r0 ldm (r10) r2 stall stall add r0,r2 r0 ldm (r8) r0 ldm (r9) r1 ldm (r10) r2 stall add r0,r1 r0 stall stall add r0,r1 r0 Registerbank DE/EX ALU EX/MEM MMU Speicher MEM/WB
Superskalarer Prozessor • Typische Optimierungen des Compilers: • Registerdruck minimieren • Geeigneten Zielcode auswählen • Umordnung der Operationen, um Abhängigkeiten zwischen Operationen im Befehlspuffer zu minimieren. Schematischer Aufbau: Speicher Schlechte Befehls-anordnung für Puffer mit Kapazität 2: Bessere Befehls-anordnung für Puffer mit Kapazität 2: Steuerwerk FE/DE1 Befehlspuffer add r0,r1 r2 add r0,r2 r2 add r0,r3 r4 add r0,r4 r4 add r0,r1 r2 add r0,r3 r4 add r0,r2 r2 add r0,r4 r4 DE1/DE2 Registerbank DE2/EX ALU ALU MMU Speicher EX/MEM
VLIW • Typische Optimierungen des Compilers: • Registerdruck minimieren • Geeigneten Zielcode auswählen • Pipeline-Hazards vermeiden • Feingranulare Parallelität erkennen und Operationen statisch parallelisieren. Sequentieller Programmcode: Schematischer Aufbau: add r0,r1 r2 add r0,r2 r2 add r0,r3 r4 add r0,r4 r4 Speicher Steuerwerk FE/DE Registerbank Parallelisierter Programmcode: DE/EX add r0,r1 r2 | add r0,r3 r4 add r0,r2 r2 | add r0,r4 r4 ALU ALU MMU Speicher EX/MEM
SMP • Typische Optimierungen: • Grobgranulare Parallelität erkennen und das Programm in Threads aufteilen, so dass wenig Synchronisation zwischen den Threads erforderlich ist. • Registerdruck minimieren • Geeigneten Zielcode auswählen • Pipeline-Hazards vermeiden Schematischer Aufbau: Sequentieller Code: Code Proc1: Code Proc2: call f call g call f call g Speicher Speicher Steuerwerk FE/DE Steuerwerk FE/DE Registerbank Registerbank DE/EX DE/EX ALU ALU EX/MEM EX/MEM Sequentieller Code: Code Proc1: Code Proc2: MMU MMU MEM/WB MEM/WB ldc 100 r0 loopHead: … dec r0 cmp r0,#50 jg loopHead ldc 50 r0 loopHead: … dec r0 cmp r0,#0 jnz r0 loopHead ldc #100 r0 loopHead: … dec r0 cmp r0,#0 jnz loopHead Cache Cache Bus gemeinsamer Speicher
Warum soll der Compiler optimieren? • Optimierungen auf Quelltextebene (z.B. durch den Programmierer) sind möglich, machen es aber erforderlich, dass der Quelltext für jede Zielarchitektur optimiert wird. • Verschiedene Optimierungen, die auf Zielcodeebene erforderlich sind, lassen sich in einer Hochsprache nicht formulieren (z.B. Registerplanung): • Statisch geplante Architekturen: • Compiler führt Scheduling und Allokation durch – auf Quelltextebene in der Regel nicht ausdrückbar • Dynamisch geplante bzw. skalare Architekturen • Compiler kann Hazards vermeiden helfen und Pipeline besser füllen • Nur ein kleines Fenster für die Optimierung in Hardware; Compiler kann optimierbaren Code in dieses Fenster schieben • SMP • Compiler verteilt Programmcode und macht Parallelisierung möglich • Programmiersprachen besitzen sequentielle Semantik; Geeignete Form der Parallelität muss durch den Compiler erkannt werden: • feingranular, • grobgranular.
Demonstration dieser Problematik an einem Beispiel: Matrixmultiplikation • Optimal für eine skalare Architektur. • Ergebnis der Multiplikation wird im Register für t akkumuliert. for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t = 0.0; for k = 1 to 100 step 1 do t = t + a[j][k] * b[k][i] od c[j][i] = t; od od i = 1 iLoop: j = 1 jLoop: t = 0.0; k = 1 kLoop: t = t + a[j][k] * b[k][i] k = k + 1 if k <= 100 then goto kLoop c[j][i] = t; j = j + 1 if j <= 100 then goto jLoop i = i + 1; if i <= 100 then goto iLoop
Was kann parallel ausgeführt werden? i b for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t = 0.0; for k = 1 to 100 step 1 do t = t + a[j][k] * b[k][i] od c[j][i] = t; od od k k a c t j Basisblock enthält keine nennenswerte Parallelität Iterationen der k-Schleife können nicht parallel ausgeführt werden: Iteration k+1 benötigt Wert von t aus Iteration k. Iterationen der j-Schleife können nicht parallel ausgeführt werden: Benutzung derselben Variablen t. Iterationen der i-Schleife können nicht parallel ausgeführt werden: Benutzung derselben Variablen t.
Scalar Expansion for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t = 0.0; for k = 1 to 100 step 1 do t = t + a[j][k] * b[k][i] od c[j][i] = t; od od i b k for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = 0.0; for k = 1 to 100 step 1 do t[j] = t[j]+a[j][k]*b[k][i] od c[j][i] = t[j]; od od k a c t t[j] j Basisblock enthält keine nennenswerte Parallelität Iterationen der k-Schleife können nicht parallel ausgeführt werden: Iteration k+1 benötigt Wert t[j] aus Iteration k. Iterationen der j-Schleife können parallel ausgeführt werden, weil a und b nur gelesen werden und Akkumulation in unterschiedliche Elemente von t erfolgt. Iterationen der i-Schleife können nicht parallel ausgeführt werden: Benutzung derselben Variablen t.
Loop-Distribution for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = 0.0; for k = 1 to 100 step 1 do t[j] = t[j]+a[j][k]*b[k][i] od c[j][i] = t[j]; od od Initialisieren, Berechnen und Zurückschreiben geschieht Elementweise in t. i b k for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = 0.0; od for j = 1 to 100 step 1 do for k = 1 to 100 step 1 do t[j] = t[j]+a[j][k]*b[k][i] od od for j = 1 to 100 step 1 do c[j][i] = t[j]; od od Zuerst Vektor t initialisieren. k a c t Werte in t berechnen. j Werte aus t nach c zurück schreiben. Initialisieren, Berechnen und Zurückschreiben wurde separiert; kann aber nicht parallel ausgeführt werden.
Loop-Interchange for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = 0.0; od for j = 1 to 100 step 1 do for k = 1 to 100 step 1 do t[j] = t[j]+a[j][k]*b[k][i] od od for j = 1 to 100 step 1 do c[j][i] = t[j]; od od i b Keine nennenswerte Parallelität in der inneren Schleife, da jede Iteration den Wert t[j] aus der vorigen Iteration benötigt. k k a c t for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = 0.0; od for k = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = t[j]+a[j][k]*b[k][i] od od for j = 1 to 100 step 1 do c[j][i] = t[j]; od od j Iterationen der inneren Schleife können parallel ausgeführt werden.
Möglichkeit der Vektorisierung (idealisiert) for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = 0.0; od for k = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = t[j]+a[j][k]*b[k][i] od od for j = 1 to 100 step 1 do c[j][i] = t[j]; od od i b k k a c t for i = 1 to 100 step 1 do t[1..100] = 0.0; for k = 1 to 100 step 1 do t[1..100] = t[1..100]+a[1..100][k]*b[k][i] od c[1..100][i] = t[1..100]; od j Vektoroperation erfordert die parallele Ausführbarkeit der Operationen auf den Elemente des Vektors. Damit sind diese Operationen parallel auf einem superskalaren nicht-Vektor Prozessor ausführbar.
Möglichkeit der Vektorisierung (praktisch) for i = 1 to 100 step 1 do t[1..100] = 0.0; for k = 1 to 100 step 1 do t[1..100] = t[1..100]+a[1..100][k]*b[k][i] od c[1..100][i] = t[1..100]; od i b k for i = 1 to 100 step 1 do for j = 1 to 100 step 32 do t[j..j+31] = 0.0; od for k = 1 to 100 step 1 do for j = 1 to 100 step 32 do t[j..j+31] = t[j..j+31]+a[j..j+31][k]*b[k][i] od od for j = 1 to 100 step 32 do c[j..j+31][i] = t[j..j+31]; od od k a c t j
Ausführung auf VLIW-Prozessor mit N Ausführungseinheiten for i = 1 to 100 step 1 do for j = 1 to 100 step 32 do t[j..j+31] = 0.0; od for k = 1 to 100 step 1 do for j = 1 to 100 step 32 do t[j..j+31] = t[j..j+31]+a[j..j+31][k]*b[k][i] od od for j = 1 to 100 step 32 do c[j..j+31][i] = t[j..j+31]; od od for i = 1 to 100 step 1 do for j = 1 to 100 step N do t[j] = 0.0; ... ; t[j+N-1] = 0.0; od for k = 1 to 100 step 1 do for j = 1 to 100 step N do t[j] = t[j]+a[j][k]*b[k][i]; ... t[j+N-1] = t[j+N-1]+a[j+N-1][k]*b[k][i]; od od for j = 1 to 100 step N do c[j][i] = t[j]; ... c[j+N-1][i] = t[j+N-1]; od od N Multiplikationen und Additionen können parallel ausgeführt werden.
Matrixmultiplikation für SMP mit zwei Prozessoren i b for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t = 0.0; for k = 1 to 100 step 1 do t = t + a[j][k] * b[k][i] od c[j][i] = t; od od k k a t0 t1 j for i = 1 to 50 step 1 do for j = 1 to 100 step 1 do t0 = 0.0; for k = 1 to 100 step 1 do t0 = t0 + a[j][k] * b[k][i] od c[j][i] = t0; od od for i = 51 to 100 step 1 do for j = 1 to 100 step 1 do t1 = 0.0; for k = 1 to 100 step 1 do t1 = t1 + a[j][k] * b[k][i] od c[j][i] = t1; od od
Grob- vs. feingranulare Parallelität i Matrixmultiplikation vor Loop-Interchange (keine feingranulare Parallelität in der inneren Schleife): b for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = 0.0; od for j = 1 to 100 step 1 do for k = 1 to 100 step 1 do t[j] = t[j]+a[j][k]*b[k][i] od od for j = 1 to 100 step 1 do c[j][i] = t[j]; od od k k a c t t[j] j for j = 1 to 50 step 1 do for k = 1 to 100 step 1 do t[j] = t[j]+a[j][k]*b[k][i] od od for j = 51 to 100 step 1 do for k = 1 to 100 step 1 do t[j] = t[j]+a[j][k]*b[k][i] od od Aber äußere Schleife kann auf zwei verschiedenen Prozessoren verteilt werden.
Grob- vs. feingranulare Parallelität i Matrixmultiplikation nach Loop-Interchange (viel feingranulare Parallelität in der inneren Schleife): b for i = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = 0.0; od for k = 1 to 100 step 1 do for j = 1 to 100 step 1 do t[j] = t[j]+a[j][k]*b[k][i] od od for j = 1 to 100 step 1 do c[j][i] = t[j]; od od k k a c t j Aufteilen der äußeren Schleife auf verschiedene Prozessoren führt zu gleichzeitigem Schreiben derselben Elemente in t. Ist also nicht möglich.
Aufgeworfene Fragen • Unter welchen Umständen ist eine bestimmte Transformation zulässig? • Welche Transformationen erzeugen • fein granulare Parallelität, • grob granulare Parallelität? • Wie kann die erzeugte fein granulare Parallelität in superskalaren Prozessorarchitekturen genutzt werden? • Wie kann die erzeugte grob granulare Parallelität in SMP Architekturen genutzt werden?
Aufbau der Vorlesung • Einleitung • Grundlagen • Aufbau eines Compilers • Überblick über die Analysephase • Vorgehen bei einfacher Synthesephase • Zwischencodeformate • Datenflussanalyseschema • Modellierung von Datenabhängigkeiten • Abhängigkeitsanalyse
Aufbau der Vorlesung • Optimierungstechniken für DSPs und Mikrocontroller • Globale Registerallokation • Scheduling-Techniken • Code-Selektion • Optimierunbgstechniken für superskalare Prozessoren • Erzeugung fein granularer Parallelität • Trade-Off Registerallokation/ILP • Statische/Dynamische Parallelisierung • HW-Support für bessere statische Parallelisierung • Region-Based-Scheduling • Global Code Motion • Traces, Superblöcke, Hyperblöcke • Modulo Scheduling • IF-Conversion
Aufbau der Vorlesung • Optimierungstechniken für SMPs • Erzeugung grob granularer Parallelität • Parallelisierung ohne Synchronisierung • Parallelisierung mit Synchronisierung • (OpenMP)