|
Die bisher vorgestellten Tips und Tricks sind sicherlich eine Hilfe, um bereits während der Entwicklung eines Programms grundsätzliche Performance-Probleme zu vermeiden. Läuft das fertige Programm dann trotzdem nicht mit der gewünschten Geschwindigkeit (was in der Praxis häufig vorkommt), helfen pauschale Hinweise leider nicht weiter. Statt dessen gilt es herausfinden, welche Teile des Programms für dessen schlechte Performance verantwortlich sind. Bei größeren Programmen, die aus vielen tausend Zeilen Quellcode bestehen, ist das eine komplizierte Aufgabe, die nur mit Hilfe eines guten Profilers bewältigt werden kann. Der Profiler ist ein Werkzeug, mit dessen Hilfe im laufenden Programm Performanceparameter, wie beispielsweise die verbrauchte CPU-Zeit, die Anzahl der allozierten Objekte oder die Anzahl der Aufrufe bestimmter Funktionen, überwacht und gemessen werden können. Mit Hilfe eines Auswertungswerkzeugs kann dann festgestellt werden, welche Teile des Programms die größte Last erzeugt haben und daher verbesserungsbedürftig sind.
Das Standard-JDK enthält bereits seit der Version 1.0 einen eingebauten Profiler, der Informationen über Laufzeit und Aufrufhäufigkeit von Funktionen geben kann. Im JDK 1.2 wurde er erweitert und ist nun zusätzlich in der Lage, den Speicherverbrauch zu messen und Profiling-Informationen threadweise auszugeben. Im Vergleich zu spezialisierten Produkten sind seine Fähigkeiten etwas rudimentär. So ist es beispielsweise nicht möglich, unterhalb von Funktionen (etwa auf der Ebene einzelner Quelltextzeilen) Laufzeiten zu messen, und eine dynamische Analyse während des laufenden Programms wird nicht unterstützt. Zudem erfordert die vom Profiler erzeugte Ausgabedatei einigen Nachbearbeitungsaufwand, um zu aussagekräftigen Ergebnissen zu kommen. Dennoch ist der JDK-Profiler ein wichtiges und hilfreiches Instrument, und wir wollen uns in diesem Abschnitt mit seiner Bedienung vertraut machen.
Als Beispiel soll das folgende Programm verwendet werden. Seine Aufgabe besteht darin, eine vorgegebene Menge von Fließkommazufallszahlen zu erzeugen und sortiert auf dem Bildschirm auszugeben. Wir unterteilen die Aufgabe in die drei Funktionen Erzeugen der Daten, Sortieren der Daten und Ausgeben der Daten. Das Programm verwendet zum Anlegen der Daten einen Vector mit einem Ladefaktor von 1 und zum Sortieren den Bubblesort-Algorithmus. Dessen Laufzeit ist quadratisch und er ist so langsam, daß das Programm bereits ab einigen hundert Elementen praktisch unbrauchbar wird:
001 /* ProfTest.java */ 002 003 import java.util.*; 004 005 public class ProfTest 006 { 007 static final int DATASIZE = 500; 008 static Vector data; 009 010 public static void createData() 011 { 012 System.out.println("Erzeugen der Daten"); 013 Random rand = new Random(); 014 data = new Vector(0, 1); 015 for (int i = 0; i < DATASIZE; ++i) { 016 data.addElement(new Double(rand.nextDouble())); 017 } 018 } 019 020 public static void sortData() 021 { 022 System.out.println("Sortieren der Daten"); 023 boolean sorted; 024 do { 025 sorted = true; 026 for (int i = 0; i < DATASIZE - 1; ++i) { 027 Double x1 = (Double)data.elementAt(i); 028 Double x2 = (Double)data.elementAt(i + 1); 029 if (x1.doubleValue() > x2.doubleValue()) { 030 data.setElementAt(x2, i); 031 data.setElementAt(x1, i + 1); 032 sorted = false; 033 } 034 } 035 } while (!sorted); 036 } 037 038 public static void printData() 039 { 040 Enumeration e = data.elements(); 041 while (e.hasMoreElements()) { 042 Double x = (Double)e.nextElement(); 043 System.out.println(x.doubleValue()); 044 } 045 } 046 047 public static void main(String args[]) 048 { 049 createData(); 050 sortData(); 051 printData(); 052 } 053 } |
ProfTest.java |
Um Profiling-Informationen zu diesem Programm zu erzeugen, ist zunächst
der Just-In-Time-Compiler zu deaktivieren,
denn sonst würden die Ergebnisse stark verfälscht. Ab dem
JDK1.2 Beta 4 setzt man dazu die Umgebungsvariable JAVA_COMPILER
auf den Wert NONE, denn standardmäßig
ist der JIT aktiviert:
set JAVA_COMPILER=NONE
Nun wird das Programm im Java-Interpreter mit der Option -Xprof
gestartet (das X muß groß
geschrieben werden):
java -Xprof ProfTest
In den Prä-1.2-JDKs wurde der Profiler nicht mit der Option -Xprof, sondern mit -prof aufgerufen. Zudem durfte nicht der normale Interpreter verwendet werden, sondern mit java_g mußte dessen Debug-Version aufgerufen werden. Soll der Profiler bei einem Applet verwendet werden, kann die Aufrufoption mit dem Schalter -J an den Appletviewer übergeben werden. |
![]() |
|
![]() |
Leider hat sich der Profiler nach dem Beta 4 des JDK 1.2 noch einmal verändert! Er wird nun nicht mehr mit den zuvor beschriebenen Optionen aufgerufen, sondern mit dem Schalter -Xrunhprof, der eine Vielzahl von Unteroptionen kennt (-Xrunhprof:help listet sie zusammen mit jeweils einem erläuternden Text auf). Auch das Ausgabeformat hat sich stark verändert und enthält zudem den Warnhinweis »WARNING! This file format is under development, and is subject to change without notice«. Zu allem Überfluß konnten einige der wichtigeren Unteroptionen bis zum Redaktionsschluß nicht zum Laufen gebracht werden, sondern führten regelmäßig zum Absturz der Testrechner. Der Leser sollte die nachfolgenden Ausführungen daher lediglich als methodische Erläuterung der Java-Profiling-Konzepte ansehen, je nach JDK-Version aber unter Umständen Detailunterschiede in Kauf nehmen. Wir werden versuchen, die Änderungen in der aktualisierten Online-Version nachzureichen, sobald sich der Profiler stabilisiert hat. |
![]() |
|
![]() |
Das Programm erzeugt nun den Vektor mit den Fließkommazahlen,
sortiert ihn und gibt das Ergebnis auf dem Bildschirm aus. Zusätzlich
erzeugt es die Datei java.prof,
in der nach Programmende die Profiling-Informationen stehen. Die Ausgabe
erfolgt im ASCII-Textformat, und die ersten Zeilen der Datei könnten
beispielsweise so aussehen:
count callee caller time
475048 java/util/Vector.elementAt(I)Ljava/lang/Object; \
ProfTest.sortData()V 3945
132408 java/util/Vector.setElementAt(Ljava/lang/Object;I)V \
ProfTest.sortData()V 1098
2008 sun/io/CharToByteISO8859_1.flush([BII)I \
java/io/OutputStreamWriter.flushBuffer()V 34
1506 java/io/BufferedOutputStream.flushBuffer()V \
java/io/BufferedOutputStream.flush()V 550
1506 java/io/OutputStream.flush()V \
java/io/BufferedOutputStream.flush()V 19
1500 java/lang/Math.max(II)I \
java/lang/FloatingDecimal.dtoa(IJI)V 13
...
Da die Ausgabezeilen teilweise sehr lang sind, wurden sie hier manuell umbrochen, was durch einen Backslash am Ende der Zeile und eine Einrückung der folgenden Zeile angezeigt wird. In Wirklichkeit gibt der Profiler die Zeilen zusammenhängend aus. |
![]() |
|
![]() |
Die erste Zeile gibt die Spaltenbelegung an. In diesem Beispiel hat jede Zeile vier Spalten, die nacheinander die Aufrufhäufigkeit, die aufgerufene Methode, den Aufrufer und den Rechenzeitverbrauch angeben. Die Sortierung der Ausgabe erfolgt nach der Aufrufhäufigkeit. So wurde beispielsweise die Methode elementAt der Klasse java.util.Vector insgesamt 475048mal von unserer eigenen Methode sortData aufgerufen und hat dabei 3945 Einheiten (Millisekunden) Rechenzeit verbraucht. Dagegen wurde die parameterlose Methode flushBuffer aus java.io.BufferedOutputStream 1506mal von der ebenfalls parameterlosen Methode flush der Klasse BufferedOutputStream aufgerufen und hat dabei 550 Einheiten Rechenzeit verbraucht.
Der Schalter -Xprof kennt die
Parameter file, type
und format. Mit file
kann eine alternative Ausgabedatei bestimmt werden. type
kann entweder inst oder cpu
sein und entscheidet, ob in der vierten Spalte die Laufzeit oder der
Speicherverbrauch angegeben wird. Der format-Parameter
legt fest, ob die Ausgabe im alten JDK-1.1-Format erfolgt oder ob
die Ergebnisse in einer fünften Spalte mit zusätzlichen
Thread-Informationen versehen werden sollen. Um den Profiler also
beispielsweise den Speicherverbrauch messen zu lassen und seine Ausgabe
threadweise zu erzeugen, hätten wir folgendes Kommando verwendet:
java -Xprof:type=inst,format=perthread ProfTest
Die Ausgabedatei java.prof
würde dann so aussehen (wieder mit eingefügten Umbrüchen
zur besseren Lesbarkeit):
thread name
0 "Thread-0"
1 "Finalizer"
2 "Reference Handler"
3 "main"
count callee caller weight thread
1 java/lang/String.length()I \
java/lang/StringBuffer.<init>(Ljava/lang/String;)V 3 0
1 java/security/AccessController.getInheritedAccessControlContext()\
Ljava/security/AccessControlContext; \
java/security/AccessControlContext.optimize()\
Ljava/security/AccessControlContext; -1 0
1 java/lang/StringBuffer.<init>(Ljava/lang/String;)V \
java/lang/Thread.<init>(Ljava/lang/ThreadGroup;\
Ljava/lang/Runnable;)V 11 0
...
Hier erfolgt die Ausgabe nach Threads sortiert. Zunächst werden die Aufrufe in Thread 0 angezeigt, gefolgt von denen in Thread 1 usw.
Wenn man sich die Datei java.prof genauer ansieht, wird man feststellen, daß sie etwa 1000 Zeilen mit Methodenaufrufen enthält und darüber hinaus am Ende noch weitere Informationen anzeigt. Tatsächlich taucht scheinbar jede Methode darin auf, die mindestens einmal im Programm aufgerufen wurde. Das Hauptproblem bei der Auswertung der Datei besteht also darin, aus dieser Datenmenge die für die Optimierung des Programms relevanten Informationen herauszufinden.
Für noch nicht optimierte Programme gilt oft die 80/20-Regel, die besagt, daß 80 Prozent der Rechenzeit in lediglich 20 Prozent des Programmcodes verbraucht werden. Um zu aussagekräftigen Ergebnissen zu kommen, müßte der Profiler-Output also nach der vierten Spalte sortiert werden, denn sie gibt den Rechenzeitverbrauch bzw. die Anzahl der erzeugten Objektinstanzen an. Da dieser Wert nicht notwendigerweise mit der Anzahl der Methodenaufrufe korrelieren muß, ist die ursprüngliche Sortierung der Datei in aller Regel wenig hilfreich.
Das folgende Programm ProfSort, kann dazu verwendet werden, die Datei java.prof nach der vierten Spalte absteigend zu sortieren. Zusätzlich kann es die Ausgabe auf eine vorgegebene Anzahl von Methodenaufrufen beschränken. Zum Sortieren wird die Klasse SortableVector aus dem Paket gk.util verwendet (auf der CD-ROM im Verzeichnis \misc zu finden). Alternativ kann auch eine der sortierbaren Collections verwendet werden, wie sie in Abschnitt 27.7 erläutert wurden.
001 /* ProfSort.java */ 002 003 import java.io.*; 004 import java.util.*; 005 import gk.util.*; 006 007 /** 008 * Klasse zum Sortieren des JDK-Profiler-Outputs nach 009 * verbrauchter Rechenzeit. Kann sowohl mit dem alten als 010 * auch mit dem neuen Format umgehen (Optionen -Xprof und 011 * -Xprof:format=perthread). 012 */ 013 public class ProfSort 014 { 015 InputStream in; 016 PrintStream out; 017 String header[]; 018 SortableVector data; 019 int colcnt; 020 021 /** 022 * Instanziert ein neues ProfSort-Objekt. 023 */ 024 public ProfSort(InputStream in, PrintStream out) 025 { 026 this.in = in; 027 this.out = out; 028 } 029 030 /** 031 * Einlesen des Headers und der Datenzeilen. 032 */ 033 public void readData() 034 throws IOException 035 { 036 data = new SortableVector(1500); 037 BufferedReader reader = new BufferedReader( 038 new InputStreamReader(in)); 039 String line; 040 //Suchen und Lesen der Kopfzeile 041 while ((line = reader.readLine()) != null) { 042 if (line.startsWith("count")) { 043 StringTokenizer st = new StringTokenizer(line); 044 colcnt = st.countTokens(); 045 if (colcnt == 4 || colcnt == 5) { 046 header = new String[colcnt]; 047 for (int i = 0; i < colcnt; ++i) { 048 header[i] = st.nextToken(); 049 } 050 } 051 break; 052 } 053 } 054 if (header.length <= 0) { 055 System.err.println("Unbekanntes Profiler-Format"); 056 System.exit(1); 057 } 058 //Einlesen der Datenzeilen 059 while ((line = reader.readLine()) != null) { 060 //"<unknown caller>" verwirrt den Tokenizer 061 int pos = line.indexOf("<unknown caller>"); 062 if (pos != -1) { 063 line = line.substring(0, pos) + "<UnknownCaller>"+ 064 line.substring(pos + 16); 065 } 066 StringTokenizer st = new StringTokenizer(line); 067 if (st.countTokens() != colcnt) { 068 break; 069 } 070 ProfilerEntry entry = new ProfilerEntry(); 071 entry.count = Integer.parseInt(st.nextToken()); 072 entry.callee = st.nextToken(); 073 entry.caller = st.nextToken(); 074 entry.time = Integer.parseInt(st.nextToken()); 075 if (colcnt >= 5) { 076 entry.thread = Integer.parseInt(st.nextToken()); 077 } 078 data.addElement(entry); 079 } 080 } 081 082 /** 083 * Ausgeben der count rechenzeitintensivsten Methoden 084 * innerhalb des angegebenen Threads. Falls die 085 * Eingabedatei keine Threadnummern enthält oder -1 086 * in thread übergeben wurde, werden alle Threads 087 * berücksichtigt. 088 */ 089 public void printData(int count, int thread) 090 { 091 //Sort data vector 092 data.qsort(new ElementComparator() { 093 public boolean preceeds(Object element1, Object element2) 094 { 095 ProfilerEntry entry1 = (ProfilerEntry)element1; 096 ProfilerEntry entry2 = (ProfilerEntry)element2; 097 return entry1.time > entry2.time; 098 } 099 }); 100 //Kopfzeile ausgeben 101 out.print(Str.getFormatted("%6s ", header[3])); 102 out.print(Str.getFormatted("%6s ", header[0])); 103 out.print(header[1] + " " + header[2]); 104 out.println(colcnt >= 5 ? " " + header[4] : ""); 105 //Datenzeilen ausgeben 106 int cnt = 0; 107 Enumeration e = data.elements(); 108 while (e.hasMoreElements()) { 109 ProfilerEntry entry = (ProfilerEntry)e.nextElement(); 110 if (colcnt < 5 || thread == -1 || entry.thread == thread) { 111 if (++cnt > count) { 112 break; 113 } 114 out.println(entry.toString()); 115 } 116 } 117 } 118 119 /** 120 * Aufrufbeispiel: 121 * 122 * type java.prof | java ProfSort 10 3 123 * 124 * Zeigt die 10 rechenzeitintensivsten Methoden des 125 * Threads 3 an. Die Datei java.prof muß vorher mit 126 * java -Xprof:format=perthread erstellt worden sein. 127 */ 128 public static void main(String args[]) 129 { 130 if (args.length < 1 || args.length > 2) { 131 System.err.println( 132 "Usage: java ProfSort <count> [<thread>]" 133 ); 134 System.exit(0); 135 } 136 int count = Integer.parseInt(args[0]); 137 int thread = -1; 138 if (args.length == 2) { 139 thread = Integer.parseInt(args[1]); 140 } 141 try { 142 ProfSort ps = new ProfSort(System.in, System.out); 143 ps.readData(); 144 ps.printData(count, thread); 145 } catch (IOException e) { 146 System.err.println(e.toString()); 147 System.exit(1); 148 } 149 } 150 } 151 152 /** 153 * Hilfsklasse zur Speicherung eines Profiler-Eintrags. 154 */ 155 class ProfilerEntry 156 { 157 public int count; 158 public int time; 159 public String caller; 160 public String callee; 161 public int thread; 162 163 public ProfilerEntry() 164 { 165 thread = -1; 166 } 167 168 public String toString() 169 { 170 return Str.getFormatted("%6d ", time) + 171 Str.getFormatted("%6d ", count) + 172 callee + " " + caller + 173 (thread != -1 ? " (" + thread + ")" : ""); 174 } 175 } |
ProfSort.java |
ProfSort kann sowohl Dateien
bearbeiten, die im alten Format erzeugt wurden, als auch solche, bei
denen die Option perthread aktiviert
war. Das Programm wird mit einem oder zwei Parametern aufgerufen:
java ProfSort <count> [<thread>]
count gibt die Anzahl der auszugebenden Methodenaufrufe an,
und mit thread kann die Ausgabe auf einen bestimmten Thread
beschränkt werden, wenn der Profiler mit der Option format=perthread
aufgerufen wurde. Fehlt das thread-Argument oder wurde die
Ausgabe ohne Thread-Informationen erzeugt, berücksichtig das
Programm alle Threads. Das Programm liest die Profilerdatei von der
Standardeingabe, so daß ein beispielhafter Aufruf so aussehen
könnte:
type java.prof | java ProfSort 10
Angewandt auf das erste Beispiel wäre die Ausgabe von ProfSort
nun:
time count callee caller
15681 1 ProfTest.main([Ljava/lang/String;)V \
<UnknownCaller>
12090 1 ProfTest.sortData()V \
ProfTest.main([Ljava/lang/String;)V
4332 485028 java/util/Vector.elementAt(I)Ljava/lang/Object; \
ProfTest.sortData()V
3514 1 ProfTest.printData()V \
ProfTest.main([Ljava/lang/String;)V
3463 500 java/io/PrintStream.println(D)V \
ProfTest.printData()V
2969 500 java/io/PrintStream.print(D)V \
java/io/PrintStream.println(D)V
2615 500 java/io/PrintStream.write(Ljava/lang/String;)V \
java/io/PrintStream.print(D)V
2496 1004 java/io/PrintStream.write([BII)V \
java/io/OutputStreamWriter.flushBuffer()V
2375 1004 java/io/BufferedOutputStream.flush()V \
java/io/PrintStream.write([BII)V
2372 502 java/io/OutputStreamWriter.flushBuffer()V \
java/io/PrintStream.write(Ljava/lang/String;)V
...
Die main-Methode des Programms hat also insgesamt 15681 Einheiten Rechenzeit verbraucht. Davon entfielen 12090 auf sortData und 3514 auf printData. Die dritte Methode createData ist nicht unter den Top-10, sie hat deutlich weniger Rechenzeit verbraucht.
Primäres Ziel unserer weiteren Optimierungsversuche wird also
der Aufruf von sortData. Wir
erkennen, daß innerhalb von sortData
4332 Einheiten auf den zum Vergleich der Elemente erforderlichen Aufruf
von Vector.elementAt entfielen.
Könnten wir diese Anzahl substantiell verringern, wäre eine
deutliche Beschleunigung des Programms zu erwarten. Um weitere Informationen
über das Laufzeitverhalten der Methode sortData
zu erhalten, müssen wir die Datei java.prof
nun nach den Zeilen durchsuchen, bei denen als Aufrufer (caller) unsere
Methode sortData eingetragen
ist. In unserem Beispiel wäre das:
4332 485028 java/util/Vector.elementAt(I)Ljava/lang/Object; \
ProfTest.sortData()V
1091 126224 java/util/Vector.setElementAt(Ljava/lang/Object;I)V \
ProfTest.sortData()V
1 1 java/io/PrintStream.println(Ljava/lang/String;)V \
ProfTest.sortData()V 1
0 1 java/lang/Double.doubleValue()D \
ProfTest.sortData()V 0
Dabei geht lediglich noch der Aufruf von setElementAt mit 1091 Einheiten nennenswert in das Ergebnis ein. Weder der einzelne Aufruf von println noch die Aufrufe von doubleValue (die möglicherweise inline ausgeführt werden und daher hier nicht explizit in Erscheinung treten) spielen für das Ergebnis eine wesentliche Rolle. Summiert man den Rechenzeitverbrauch, so kommt man auf 5424 Einheiten, immerhin noch 6666 Einheiten weniger als die gesamte Rechenzeit von sortData. Die verbleibende Zeit wird also nicht in untergeordneten Methodenaufrufen verbraucht, sondern in sortData selbst, beispielsweise im Code zur Ausführung der for-Schleifen oder zum Vergleichen der Fließkommazahlen.
Schaut man sich die doppelt geschachtelten Schleifen in sortData an, so erkennt man, daß der Schlüssel zur Optimierung der Methode im Einsatz eines besseren Sortierverfahrens liegt. Bubblesort hat bei gut gemischten Daten quadratische Laufzeit und benötigt daher fast 500 * 500 = 250000 Elementvergleiche. Das sind knapp 500000 Zugriffe auf den Vektor, 500000 Zugriffe auf den Wert eines Double-Objekts, 250000 Aufrufe des Größer-Tests auf Fließkommazahlen und 250000 Additionen und Tests des Schleifenzählers.
Wenn man statt dessen eines der Sortierverfahren der Klasse O(n log2 n) verwenden würde (beispielsweise QuickSort, ShellSort oder HeapSort), könnte man die Laufzeit von sortData auf einen Bruchteil seiner jetzigen Laufzeit verringern. Bei 1000 Elementen wäre die Geschwindigkeit dann bereits hundertmal höher, bei 10000 Elementen etwa 750mal. Das folgende Listing zeigt eine alternative Realisierung von sortData auf der Basis einer rekursiven Quicksort-Implementierung. Im Test hat dieses Verfahren tatsächlich die beschriebenen Verbesserungen gebracht und dazu geführt, daß sortData auch noch 50000 Elemente in etwa einer Sekunde sortiert hat.
001 /* qsort.inc */ 002 003 public static void qsortData(int left, int right) 004 { 005 if (left < right) { 006 if (right - left == 1) { 007 //Zwei Elemente werden direkt vertauscht 008 Double x1 = (Double)data.elementAt(left); 009 Double x2 = (Double)data.elementAt(right); 010 if (x1.doubleValue() > x2.doubleValue()) { 011 data.setElementAt(x1, right); 012 data.setElementAt(x2, left); 013 } 014 } else { 015 //Pivot per Zufallszahl auswählen 016 int pos = left + (int)((right - left + 1) * Math.random()); 017 double pivot = ((Double)data.elementAt(pos)).doubleValue(); 018 int i = left - 1; 019 int j = right + 1; 020 Double x, y; 021 //Partitionieren 022 while (true) { 023 //Linken Rand suchen 024 do { 025 ++i; 026 x = (Double)data.elementAt(i); 027 } while (x.doubleValue() < pivot); 028 //Rechten Rand suchen 029 do { 030 --j; 031 y = (Double)data.elementAt(j); 032 } while (y.doubleValue() > pivot); 033 if (i >= j) { 034 break; 035 } 036 //Randelemente vertauschen 037 data.setElementAt(x, j); 038 data.setElementAt(y, i); 039 } 040 //Rekursiver qsort der Partition[en] 041 if (j == right) { 042 qsortData(left, j - 1); 043 } else { 044 qsortData(left, j); 045 qsortData(j + 1, right); 046 } 047 } 048 } 049 } 050 051 ... 052 053 //Der Aufruf erfolgt nun parametrisiert: 054 createData(); 055 qsortData(0, DATASIZE - 1); 056 printData(); 057 058 ... |
qsort.inc |
Die Laufzeitstruktur der Methode printData stellt sich vollkommen anders dar. Sie verbraucht 3463 von 3514 Einheiten (also 98,5 %) in den 500 Aufrufen von java.io.PrintStream.println. Verfolgt man java.prof über die Aufrufer-Kette weiter, erkennt man, daß die Aufrufe von hasMoreElements und nextElement mit 9 bzw. 11 Einheiten keine nennenswerte Rolle spielen. Auch die Konvertierung von double nach String spielt mit etwa 200 Einheiten keine große Rolle. Der Löwenanteil der Rechenzeit wird tatsächlich in Ausgabemethoden verbraucht.
Meist besteht die einzige Möglichkeit, den Rechenzeitverbrauch von Java-eigenen Methoden zu verringern, darin, sie weniger oft aufzurufen oder den Aufruf durch eine schnellere Alternative zu ersetzen. Beides ist oft problematisch, denn der Aufwand ist hoch und das Programm kann schnell unleserlich werden. Wir könnten in unserem Beispiel etwa auf die Idee kommen, die Ausgabe selbst zu puffern und zum Schluß alle Fließkommazahlen auf einen Schlag auszugeben:
001 public static void printData() 002 { 003 String NL = System.getProperty("line.separator"); 004 StringBuffer sb = new StringBuffer(10000); 005 Enumeration e = data.elements(); 006 while (e.hasMoreElements()) { 007 Double x = (Double)e.nextElement(); 008 sb.append(String.valueOf(x.doubleValue())); 009 sb.append(NL); 010 } 011 System.out.print(sb.toString()); 012 } |
Tatsächlich würde das die Laufzeit von 3514 auf 812 Einheiten vermindern, was sicherlich ein guter Erfolg wäre. Zusammen mit dem verbessertem Sortierverfahren und Verbesserungen beim Erzeugen der Fließkommazahlen (z.B. durch Erhöhen des Ladefaktors) würde unser Programm in völlig neue Dimensionen vorstoßen. Problematisch ist hier natürlich der erhöhte Speicherverbrauch. Je formatierter Fließkommazahl werden inkl. Zeilenschaltung etwa 20 Bytes benötigt, in unserem Beispiel also bereits 10 kByte zusätzlicher Hauptspeicher. Bei größeren Datenmengen könnte hier schnell ein neuer Engpaß entstehen, der einen möglichen Nettogewinn durch die erhöhte Speicherbelastung wieder zunichte macht. Dieser Konflikt zwischen Laufzeitverhalten und Speicherverbrauch tritt in der Praxis recht häufig auf. Er sollte bei allen Optimierungen beachtet werden.
Egal, ob mit dem eingebauten Profiler das Laufzeitverhalten oder der Speicherverbrauch der Anwendung untersucht werden soll, die prinzipielle Vorgehensweise ist stets gleich:
Ist der Anteil von lokalem Code am Rechenzeitverbrauch hoch, kann versucht werden, diesen zu verringern. Typische Ansatzpunkte dafür sind das Vermindern der Anzahl von Schleifendurchläufen (durch bessere Algorithmen), die Verwendung von Ganz- statt Fließkommazahlen, das Herausziehen von schleifeninvariantem Code, das Vermeiden der Doppelauswertung von gemeinsamen Teilausdrücken, das Wiederverwenden bekannter Teilergebnisse, die Verwendung alternativer Datenstrukturen, das Eliminieren von unbenutztem Code oder das Reduzieren der Stärke von Ausdrücken, indem sie durch algebraisch gleichwertige, aber schnellere Ausdrücke ersetzt werden. Wird ein großer Anteil der Rechenzeit dagegen in Aufrufen von Untermethoden verbraucht, kann versucht werden, deren Aufrufhäufigkeit zu vermindern, sie durch performantere Aufrufe zu ersetzen, oder - im Falle eigener Methoden - ihre Ablaufgeschwindigkeit zu erhöhen.
Der zielgerichtete Einsatz dieser Techniken erfordert gute Werkzeuge, namentlich einen guten Profiler. Bei kleineren Problemen mag es ausreichend sein, die Ablaufgeschwindigkeit mit Ausgabeanweisungen und System.currentTimeMillis zu ermitteln, und auch der JDK-Profiler kann mit Erfolg eingesetzt werden. Daneben gibt es mittlerweile einige kommerzielle und experimentelle Produkte mit wesentlich erweiterten Fähigkeiten. Beispiele für solche Profiler sind JProbe (http://www.klg.com), der auch für Teile der Software zu diesem Buch verwendet wurde, OptimizeIt (http://www.optimizeit.com) oder JInsight (http://www.alphaWorks.ibm.com). Zu ihren Fähigkeiten zählen beispielsweise:
|
Go To Java 2, Addison Wesley, Version 1.0.2, © 1999 Guido Krüger, http://www.gkrueger.com |