260 likes | 371 Views
Seminar aus Softwareentwicklung: Programmierstil Effizienz. Friedrich Priewasser. Übersicht. Überschlagsrechnungen Profiling Code Tuning Effiziente Speichernutzung. Überschlagsrechnungen. Ermöglichen das Abschätzen von Laufzeiten und Speicherbedarf
E N D
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++) 999 if (n % i == 0) 78022 return 0; 831 return 1; 168 } main(){ int i,n; n=1000; 1 for (i=2; i<=n; i++) 1 if (prime(i)) 999 printf("%d\n", i); 168 } 999 Zahlen werden überprüft 168 Zahlen sind prim
Reduzieren der Testsauf Teilbarkeit int root(int n){ return (int) sqrt((float) n); 5456 } 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 } main(){ int i,n; n=1000; 1 for (i=2; i<=n; i++) 1 if (prime(i)) 999 printf("%d\n", i); 168 } 5288 statt 78022 Tests Laufzeit steigt allerdings
Verwenden eines Profilersmit Zeitmessung %Zeit Name 82.7 sqrt 4.5 prime 4.3 root 2.6 frexp ... ... Wurzelberechnung benötigt über 4/5 der Gesamtzeit • Arbeit innerhalb Schleifen minimieren • Komplexe Funktion durch einfache ersetzen int prime(int n) { int i, bound; bound = root(n); for (i=2; i<=bound; i++) if (n%i == 0) return 0; return 1; } 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 0; if(x<0x4) return 1; if(x<0x8) return 2; if(x<0x10) return 3; ... if(x<0x20000000) return 28; if(x<0x40000000) return 29; if(x<0x80000000) return 30; return 31; } 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) return 0; else return 1; } else { if(x<0x8) return 2; ... else return 29; } else { if(x<0x80000000) return 30; else return 31; } } } } } 40ns Vergleich: Originalversion 700ns mit Konstante 450ns -36% ohne Math.Log 120ns -83% mit Binärsuche 40ns -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: Weitere Verbesserung durch Ändern des Algorithmus: val=0; powerOfX=1; for(int p=0;p<=power;p++){ val=val+coef[p]*powerOfX; powerOfX*=powerOfX; } 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 arrmax(int n){ if (n==1) return x[0]; else return max(x[n-1],arrmax(n-1)); } int[] x={5,2,1,3}; int max=arrmax(4); 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: bei erstem Aufruf: void InitTab(){ for(int x=0;x<=900;x++) sinTab[x]=Math.sin(x); } 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 • 2000 Nachbarn • geg.: x und y Position • ges.: Nr. des Nachbarn Einfachste Lösung: Array mit 200x200 Einträgen 40000 Elemente: bei 32-Bit Werten 160 000 Byte bei 16-Bit Werten 80 000 Byte
Verwenden verketteter Listen: Suchaufwand: Max: über 200 Punkte Mittel: über 10 Punkte Speicherbedarf: 200*4 Byte + 2000*12 Byte = 24800 Byte durch malloc steigt der Bedarf auf das mehrfache
pointnum 17 538 1053 98 15 1800 ... 437 832 2 5 126 1 138 11 ... 11 67 row 0 3 5 5 ... 1889 2000 firstincol 0 1 2 3 199 200 Ersetzen der Listen durch eine Liste mit fixer Länge: find(int i,int j){ for (k=firstincol[i];k<firsincol[i];k++){ if (row[k]==j) return pointnum[k]; } return -1; } Suchaufwand: Max: über 200 Punkte Mittel: ü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: 32 Bit 16 Bit 2-dim Array: 80 000 Byte 40 000 Byte Linked-Lists: >= 42 800 Byte 1-dim Arrays: 16 804 Byte 8 804 Byte ohne „Row“ 8 804 Byte 4 402 Byte Platzersparnis: 75.6 kB = 94.5%
ZusammenfassungSpeichereffizienz • 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