n - SSW

Werbung
Seminar aus Softwareentwicklung:
Programmierstil
Effizienz
Friedrich Priewasser
Übersicht
•
•
•
•
Überschlagsrechnungen
Profiling
Code Tuning
Effiziente Speichernutzung
Überschlagsrechnungen
• Ermöglichen das Abschätzen von Laufzeiten und
Speicherbedarf
• Schon vor Implementierung kann Machbarkeit
überprüft werden
• Für jede Operation wird die durchschnittliche
Ausführungsdauer gemessen:
n=...
for (int i=0; i<n; i++);
n=...
for (int i=0; i<n; i++)
i1=i2+i3;
Kosten für eine Operation
• Probleme:



Compileroptimierungen
verwendeter Speicher (Cache oder Hauptspeicher)
Ergebnis nur für ähnliche Prozessoren einsetzbar
• an „sinnvollen“ Bsp. überprüfen ob Schätzung
stimmt (Größenordnung)
• Sicherheitsfaktoren verwenden
Profiling
• Profiler: Werkzeug zum Auflisten der Häufigkeit
mit der ein Programmteil ausgeführt wurde
• Bsp: Primzahlen bis 1000 ermitteln:
int prime(int n){
int i;
for (i=2; i<n; i++)
if (n % i == 0)
return 0;
return 1;
}

999
78022
831
168
main(){
int i,n;
n=1000;
for (i=2; i<=n; i++)
if (prime(i))
printf("%d\n", i);
}
999 Zahlen werden überprüft
168 Zahlen sind prim
1
1
999
168
Reduzieren der Tests
auf Teilbarkeit
int root(int n){
return (int) sqrt((float) n);
}
int prime(int n) {
int i;
for(i=2;i<=root(n);i++) 999
if (n%i == 0)
5288
return 0;
831
return 1;
168
}

5288 statt 78022 Tests
Laufzeit steigt allerdings
5456
main(){
int i,n;
n=1000;
for (i=2; i<=n; i++)
if (prime(i))
printf("%d\n", i);
}
1
1
999
168
Verwenden eines Profilers
mit Zeitmessung
%Zeit
82.7
4.5
4.3
2.6
...
Name
sqrt
prime
root
frexp
...
 Wurzelberechnung benötigt über 4/5 der Gesamtzeit
• Arbeit innerhalb Schleifen
minimieren
int prime(int n)
{ int i, bound;
bound = root(n);
for (i=2; i<=bound; i++)
if (n%i == 0)
return 0;
return 1;
}
• Komplexe Funktion durch
einfache ersetzen
int prime(int n)
{ int i;
for (i=2; i*i<=n; i++)
if (n%i == 0)
return 0;
return 1;
}
Code Tuning
• Gründe die gegen Code Tuning sprechen:
optimierter Code ist
 schwierig zu programmieren, zu lesen und zu
überarbeiten
 fehleranfällig
 mit viel Zeitaufwand beim Erstellen verbunden
im schlimmsten Fall
 Langsamer
• Falsche Vermutungen können die Laufzeit erhöhen

das Aus für das Projekt
• Zu frühes Optimieren führt zu nicht korrekten, schlecht
modularisierten Code
• Programmcode (noch) nicht optimieren
Methoden zur
Geschwindigkeitssteigerung
• Programm Design überdenken
(Modularisierung, Grobentwurf, ...)
• Modul- und Methodendesign überarbeiten
(Wahl geeigneter Datenstrukturen und Algorithmen)
• Zugriffe auf Betriebssystem reduzieren
(Ausgabe auf Bildschirm, Festplatte, ... Einlesen von
Festplatte, ...)
• Geeigneten Compiler wählen
(Compileroptimierungen)
• Andere Hardware verwenden
(Hardware ist billiger als Software)
• Code Tuning
Vorgehensweise beim
Code-Tuning
• Geschwindigkeit des Programms messen

ca. 5% des Codes benötigen über 50% der
Laufzeit
• „Hot Spot“ im Programm überarbeiten, „tunen“
• Erfolg der Optimierung überprüfen


Ist Programm wirklich schneller geworden?
Läuft es weiterhin fehlerfrei?
• Sinnhaftigkeit weiterer Optimierung
überdenken
Ein Beispiel:
Zweier-Logarithmus-Berechnung für Integer
static uint Log2(uint n){
return (uint) (System.Math.Log(n)/System.Math.Log(2));
}
700ns
• Ersetzen von Funktionsaufrufen durch Ergebnis
static uint Log2(uint n){
return (uint) (System.Math.Log(n)/0.6931471805599453094);
}
450ns
• Geeignete Datentypen verwenden / Algorithmus ändern
static uint Log2(uint x){
if(x<0x2)
return
if(x<0x8)
return
...
if(x<0x20000000) return
if(x<0x80000000) return
return 31;
}
0;
2;
if(x<0x4)
if(x<0x10)
return 1;
return 3;
28;
30;
if(x<0x40000000) return 29;
120ns
Ein Beispiel:
Zweier-Logarithmus-Berechnung für Integer
• Algorithmus verbessern
static uint Log2(uint x){
if(x<0x10000){
if(x<0x100){
if(x<0x10){
if(x<0x4){
if(x<0x2)
else
} else {
if(x<0x8)
...
else
} else {
if(x<0x80000000)
else
} } } } }
return 0;
return 1;
return 2;
return 29;
return 30;
return 31;
Vergleich: Originalversion
mit Konstante
ohne Math.Log
mit Binärsuche
700ns
450ns
120ns
40ns
40ns
-36%
-83%
-94%
Komplizierte Operationen durch
einfache ersetzen
• Positionsbestimmung bei Zyklischer Puffer:
pos=(pos+1) % n;
ersetzen durch:
pos++;
if(pos>=n) pos=0;
• Polynom-Auswertung
val=0;
for(int p=0;p<=power;p++)
val=val+coef[p]*Math.pow(x,p);
ersetzen durch:
val=0;
powerOfX=1;
for(int p=0;p<=power;p++){
val=val+coef[p]*powerOfX;
powerOfX*=powerOfX;
}
Weitere Verbesserung
durch Ändern des
Algorithmus:
val=0;
for(int p=power;p>=0;p--)
val=val*x+coef[p];
Inline-Codierung
• Vermeidet Aufwand des Funktionsaufrufs
• Beliebtes Mittel in C: Makros

Bsp.: max Funktion
int max(int a, int b){
return a>b ? a : b;
}
wird ersetzt durch
#define max(a,b) ((a)>(b) ? (a) : (b))
Je nach Compiler bis zu 50% schneller
Beispiel zur Anwendung:
int[] x={5,2,1,3};
int max=arrmax(4);
3 > arrmax(3) ? 3 : arrmax(3)
1 > arrmax(2) ? 1 : arrmax(2)
2 > arrmax(1) ? 2 : arrmax(1)
5
2<5 => arrmax(1) berechnen
5
1<5 => arrmax(2) berechnen
2 > arrmax(1) ? 2 : arrmax(1)
5
2<5 => arrmax(1) berechnen
5
3<5 => arrmax(3) berechnen

int arrmax(int n){
if (n==1) return x[0];
else return max(x[n-1],arrmax(n-1));
}
3<5 => arrmax(3) berechnen
1 > arrmax(2) ? 1 : arrmax(2)
2 > arrmax(1) ? 2 : arrmax(1)
5
2<5 => arrmax(1) berechnen
5
1<5 => arrmax(2) berechnen
2 > arrmax(1) ? 2 : arrmax(1)
5
2<5 => arrmax(1) berechnen
5
Komplexität steigt durch Verwenden des Makros von O(n) auf O(2n)
Loop-Unrolling
for (int i=0;i<5;i++) a[i]=i;

a[0]=0; a[1]=1; a[2]=2; a[3]=3; a[4]=4;

6.5 mal schneller = 85% Zeit Ersparnis
Allgemein:
i=1;
while (i<=Num){
a[i]=i;
i++;
}

i=1;
upper=Num-N+1;
while(i<=upper){
a[i]=i;
a[i+1]=i+1;
...
a[i+N-1]=i+N-1;
i+=N;
}
while(i<=N){
a[i]=i;
i++;
}
Speichern für Wiederverwendung
• Einmalige Berechnung von Funktionsergebnissen



zur Implementierungszeit (Ergebnisse in Datei
speichern)
zur Initialisierungszeit
bei erstem Aufruf
• z.B.: Tabelle mit vorberechneten Sinuswerten


0 bis 90 Grad (Rest kann berechnet werden)
0.1 Grad Schritte
bei Initialisierung:
void InitTab(){
for(int x=0;x<=900;x++)
sinTab[x]=Math.sin(x);
}
bei erstem Aufruf:
double SinTab(int n){
if(sinTab[n]<-1)
sinTab[n]=Math.sin(n/10);
return sinTab[n];
}
Schreiben von Programmteilen in
Assembler
• Vorgehensweise





Programm vollständig in Hochsprache schreiben
Testen und feststellen ob das Programm den
Anforderungen entspricht
Feststellen welche Teile des Codes nicht schnell genug
arbeiten (Profiler)
Vom Compiler erzeugten Assembler-Code optimieren
Korrektheit und Geschwindigkeitsgewinn überprüfen
bzw. messen
• Nachteil: Portabilität geht verloren
Compiler Optimierungen
• Kosten nichts
• Leistungsgewinn hängt ab von
Programmcode
 Sprache
 Compiler
Bereich: 0 bis 50 Prozent

Weitere Techniken
• Gleich oft durchlaufene Schleifen zusammenfassen
• Arbeit innerhalb Schleifen minimieren
• Tests beenden wenn Ergebnis bekannt ist

Mit break Schleife beenden
• „Sentinels“ verwenden beim Suchen in Arrays


Letztes Element durch gesuchten Wert ersetzen
Ersetzt dir Abfrage ob der Index noch gültig ist
• if else und switch Statements der Häufigkeit nach
ordnen
Speichereffizienz
• Bsp.: Geographische Datenbank:




200x200 Felder
5 538
2000 Nachbarn
geg.: x und y Position
ges.: Nr. des Nachbarn
965
1171
17
98
162
0
0
Einfachste Lösung: Array mit 200x200 Einträgen
 40000 Elemente: bei 32-Bit Werten 160 000 Byte
bei 16-Bit Werten 80 000 Byte
7
Verwenden verketteter Listen:
colhead
pointnum
row
next
0
2
17
1
1
98
5
538
126 1053
138 15
2
Suchaufwand:
Max:
Mittel:
über 200 Punkte
über 10 Punkte
Speicherbedarf:
200*4 Byte + 2000*12 Byte = 24800 Byte
durch malloc steigt der Bedarf auf das mehrfache
Ersetzen der Listen durch eine Liste mit
fixer Länge:
pointnum
row
17
2
538
5
1053
126
98
1
firstincol
0
0
3
1
5
2
5
3
15
138
1800
11
...
...
...
1889 2000
199 200
437
11
832
67
find(int i,int j){
for (k=firstincol[i];k<firsincol[i];k++){
if (row[k]==j) return pointnum[k];
}
return -1;
}
Suchaufwand:
Max:
Mittel:
über 200 Punkte
über 10 Punkte
Speicherbedarf:
bei 32-Bit Werten: 201*4 Byte + 2000*4 Byte + 2000*4 Byte = 16804 Byte
bei 16-Bit Werten: 201*2 Byte + 2000*2 Byte + 2000*2 Byte = 8402 Byte
Entfernen des Row-Arrays:
find(int i,int j){
for (k=firstincol[i];k<firsincol[i];k++){
if (point[poinnum[k]].row==j) return pointnum[k];
}
return -1;
}
Speicherbedarf bei 16 Bit-Werten:
201*2 Byte + 2000*2 Byte = 4402 Byte
Vergleich:
2-dim Array:
Linked-Lists:
1-dim Arrays:
ohne „Row“
32 Bit
80 000 Byte
>= 42 800 Byte
16 804 Byte
8 804 Byte
Platzersparnis: 75.6 kB = 94.5%
16 Bit
40 000 Byte
8 804 Byte
4 402 Byte
Zusammenfassung
Speichereffizienz
• Kleinst mögliche Wert-Typen verwenden
• Werte neu berechnen statt speichern
• Geeignete Datenstrukturen verwenden


Keinen Platz für Null-Werte verschwenden
Menge an Hilfsdaten (Zeiger, ...) reduzieren
• Nicht übertreiben (Jahr 2000 Problem)
Schlüsselpunkte beim
Optimieren
• Performance alleine führt nicht zu guter
Softwarequalität
• Wenige Prozent des Codes (ca. 5%) benötigen
über 50% der Laufzeit
• Messen der Geschwindigkeit (vor und nach der
"Optimierung") ist das A und O des Code Tuning
• Nur von Anfang an sauberer Code führt zu guten
Ergebnissen
Herunterladen