Projekt Systementwicklung Effiziente Codierung: Laufzeitoptimierung Prof. Dr. Nikolaus Wulff
Effiziente Codierung Der Wunsch effizienten Code zu schreiben entstammt mehreren Quellen: Zielplattformen mit begrenztem Hauptspeicher verlangen eine Optimierung des Speicherbedarfs. Häufig läuft die Anwendung zu langsam, so dass eine Laufzeitoptimierung kritischer Algorithmen notwendig ist. Selten ist es möglich Laufzeitverhalten und Speicherbedarf gleichzeitig zu optimieren. Guter, effizienter und wartbarer Code ist für Informatiker sowohl eine interlektuelle Herausforderung als auch eine persönliche Befriedigung und trennt Spreu vom Weizen. Prof. Dr. Nikolaus Wulff Projekt Systementwicklung 2
Code Analyse Die Optimierung beginnt haüfig mit einem existierenden Programm, das es zu verbessern gilt. Nur relevante Programmbestandteile sollten optimiert werden. Was nützt die 10-fache Beschleunigung einer Routine, die nur einmal aufgerufen wird, wenn eine wichtige Funktion, die 100000 Mal verwendet wird um einen Faktor 2 zu langsam läuft? Vor die Optimierung kommt daher die Code Analyse, um den kritischen Pfad zu erkennen. Hierbei helfen entsprechende Profiler und ein geübtes Auge. Prof. Dr. Nikolaus Wulff Projekt Systementwicklung 3
Bit- und Byte-Optimierer Während früher noch einzelne Bits in Assembler optimiert wurden, wird dies zunehmend seltener. Z.B. C beinhaltet das Schlüsselwort register dessen Verwendung aber nur bei hardwarenaher Programmierung Sinn macht. Der Code wird schwieriger zu portieren und ist CPU- und plattformabhängig. Optimierende Compiler erkennen meistens selbst welche Variablen in schnelleren Registern passend zur Zielplattform abzulegen sind. Nur noch bei spezieller μ-prozessor Programmierung wird sich noch über Alignment, Register und Speicherauslegung Gedanken gemacht. Prof. Dr. Nikolaus Wulff Projekt Systementwicklung 4
Optimierung von Algorithmen Optimierung im Großen bedeutet die Verfeinerung von Algorithmen und Datenstrukturen. Neben der naiven Programmierung gibt es optimierte Varianten, die dasselbe Ergebnis in besserer Laufzeit oder mit weniger Speicherbedarf liefern. Die Optimierung beruht häufig auf einer grundlegenden meist mathematischen Analyse des Problems. Ein Beispiel soll die Idee erläutern, es gibt leider kein Patentrezept... Prof. Dr. Nikolaus Wulff Projekt Systementwicklung 5
Potenzierung per Rekursion Häufig wird die Potenz x n für natürliche Exponenten n benötigt. Mathematisch wird x n rekursiv definiert x 0 =1 x n =x x n 1 0<n und entsprechend einfach implementiert: double pow(double x, unsigned int n) { if(n==0) return 1; } return x*pow(x,n-1); Prof. Dr. Nikolaus Wulff Projekt Systementwicklung 6
Schnelleres Potenzieren Rekursionen sind meistens nicht sehr effizient und belasten den Stack unnötig. Eine optimierte iterative Implementierung der pow-funktion könnte so aussehen: double pow(double x, unsigned int n) { unsigned int j; double y = (n == 0)? 1 : x; for (j = 1; j < n; y *= x, j++); } return y; x n = j=1 Hier wurden die y-initialisierung und die for- Schleife scheinbar optimiert, aber das wichtigste Optimierungspotenzial wurde übersehen... Prof. Dr. Nikolaus Wulff Projekt Systementwicklung 7 n x
Teile und Herrsche Beide bis lang vorgestellten Varianten haben eine lineare Laufzeit O(n) mit n Multiplikationen. Eine Fallunterscheidung nach geradem und ungeradem Exponenten n, d. h. n=2k oder n=2k+1, liefert eine überraschende Optimierung: (x n ) pow(x,n) x 2k =x k x k =(x k ) 2 x 2k+1 =x x k x k =x (x k ) 2 Eine derartig implementierte pow-funktion berechnet die geometrisch fallende Folge n, n/2, n/4, n/8... und ein solcher Algorithmus hat eine wesentlich effizientere Laufzeit O(log 2 n). Prof. Dr. Nikolaus Wulff Projekt Systementwicklung 8
Optimierte Rekursion Die optimierte rekursive Implementierung double pow(double x, unsigned int n) { double y = 1; if(n) { /* test n>0 */ y = pow(x,n/2); y *= y; if(n%2==1) /* test n odd? */ y *= x; } return y; } ist nur unwesentlich länger als die ursprüngliche Version aber wesentlich effizienter. Spezielle C Operatoren wie n>>1 statt n/2 würden alleine den Kern der Optimierung verfehlen. Prof. Dr. Nikolaus Wulff Projekt Systementwicklung 9
Optimierte pow-funktion Auflösen der Rekursion in Form einer do-while Schleife liefert den optimierten Algorithmus: double pow(double x, unsigned int n) { double y = 1, z = x; do { if(n&1) /* test n odd? */ y *= z; } Frei nach: z *= z; } while(n>>=1); /* test n/2 >0 */ return y; Donald E. Knuth, The Art of Computer Programming Arithmetik, 4.6.3 Algorithmus A Prof. Dr. Nikolaus Wulff Projekt Systementwicklung 10
Zusammenfassung Teile und Herrsche ist ein Ansatz der bei vielen Verfahren zum Einsatz kommt, z.b. Sortieralgorithmen haben Laufzeit O(log n) statt naiv O(n 2 ). Häufig sind optimierte Algorithmen nicht mehr so leicht zu verstehen und erfordern mehr geistige Kapazitäten beim Programmierer... Wie D. E Knuth so schön titelt: Algorithmen zu optimieren ist eine Kunst. Auch beim Game of Life gibt es Optimierungspotential bei der Berechnung der Nachbarn und bei der Auswertung der Regeln... Prof. Dr. Nikolaus Wulff Projekt Systementwicklung 11