Tit   Inh   Ind   1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   16   17   18   19   20   21   22   23   24   25   26   27   28   29   30   31   32   <<   <   >   >> 

29.3 Einsatz eines Profilers



29.3.1 Erzeugen der Profiling-Informationen

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
Listing 29.9: Test-Programm für den Profiler

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.

 JDK1.1/1.2 

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.

 Warnung 

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.

 Hinweis 

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.

29.3.2 Auswerten der Profiling-Informationen

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
Listing 29.10: Programm zum Sortieren der Profiling-Informationen

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
Listing 29.11: Sortieren mit QuickSort

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 }
Listing 29.12: Eine optimierte Ausgabemethode

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.

29.3.3 Ausblick

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:


 Tit   Inh   Ind   1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   16   17   18   19   20   21   22   23   24   25   26   27   28   29   30   31   32   <<   <   >   >> 
Go To Java 2, Addison Wesley, Version 1.0.2, © 1999 Guido Krüger, http://www.gkrueger.com