1 / 26

Seminar aus Softwareentwicklung: Programmierstil Effizienz

Seminar aus Softwareentwicklung: Programmierstil Effizienz. Friedrich Priewasser. Übersicht. Überschlagsrechnungen Profiling Code Tuning Effiziente Speichernutzung. Überschlagsrechnungen. Ermöglichen das Abschätzen von Laufzeiten und Speicherbedarf

Download Presentation

Seminar aus Softwareentwicklung: Programmierstil Effizienz

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Seminar aus Softwareentwicklung: Programmierstil Effizienz Friedrich Priewasser

  2. Übersicht • Überschlagsrechnungen • Profiling • Code Tuning • Effiziente Speichernutzung

  3. Ü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;

  4. 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

  5. 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

  6. 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

  7. 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; }

  8. 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

  9. 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

  10. 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

  11. 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

  12. 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%

  13. 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];

  14. 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

  15. 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)

  16. 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++; } 

  17. 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]; }

  18. 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

  19. Compiler Optimierungen • Kosten nichts • Leistungsgewinn hängt ab von • Programmcode • Sprache • Compiler Bereich: 0 bis 50 Prozent

  20. 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

  21. 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

  22. 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

  23. 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

  24. 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%

  25. 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)

  26. 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

More Related