Wo es klemmt

Wer die Leistung einer Anwendung verbessern will, muss zunächst die Engpässe in ihr lokalisieren. Hierzu gibt es mehrere Strategien und etliche Werkzeuge - freie wie kommerzielle.

In Pocket speichern vorlesen Druckansicht 54 Kommentare lesen
Lesezeit: 4 Min.
Von
  • Michael Tamm

Seit Jahren nehmen Softwareprojekte an Umfang und Komplexität zu, immer mehr Menschen arbeiten an einem Projekt. Deshalb setzt sich die Erkenntnis durch, dass Programmierer in erster Linie funktionierenden, verständlichen und wartbaren Code schreiben sollten - nicht besonders schnellen.

Doch irgendwann kommt meistens der Punkt, an dem die gut funktionierende Software erschreckend lange läuft. Es ist nicht schlimm, dass dies meist erst in der späteren Projektlaufzeit auffällt, sondern sogar gewollt. Denn nun liegen gut lesbare Quellen vor und eine Testsuite, die bestätigt, dass das Programm das Gewünschte tut: optimale Voraussetzungen für ein Refactoring zwecks Leistungssteigerung.

Zwar ist jedem Entwickler klar, dass er nun nicht wild drauflos optimieren sollte. Trotzdem hat er meistens eine oder mehrere Vermutungen, wo die Engpässe zu suchen sind. Selbst wer sich noch so sicher ist, sollte seine Vermutung allerdings durch eine Messung bestätigen. Nur so lässt sich zeigen, welchen Gewinn Veränderungen gebracht haben.

Performance-Anforderungen sind ein Punkt, der in vielen Verträgen für Software schlicht fehlt, zum Leidwesen der Kunden. Hier besteht noch Aufklärungsbedarf, dass Leistungskriterien ebenso wichtig sind wie die funktionalen Anforderungen an eine Software.

Es empfiehlt sich, eventuell vorhandene Performance-Anforderungen in die Testsuite einzubinden. Hierfür gibt es mit JUnitPerf (s. „Ressourcen im Web“) eine Bibliothek, die dies erleichtert. Existiert bereits ein JUnit-Test, der die funktionalen Anforderungen prüft, kann man ihn mit der JUnitPerf-Klasse TimedTest kapseln und mit einer Performance-Anforderung ausstatten. Läuft der Originaltest nicht in der spezifizierten Zeit durch, schlägt der TimedTest fehl. Auf diese Weise lassen sich Performance-Mängel früh entdecken und beheben.

Einfache Zeitmessungen lassen sich mit Java-Bordmitteln vornehmen. Die Methode System.currentTimeMillis() liefert die aktuelle Zeit in Millisekunden seit dem 1. Januar 1970 als long zurück. Jeweils ein Aufruf der Funktion am Anfang und Ende der Verarbeitung mit Speicherung der Werte in geeigneten Variablen - mehr ist für eine erste Abschätzung nicht nötig.

Wie genau der so erhaltene Wert ist, hängt zwar neben der Timer-Auflösung der jeweiligen Plattform von der allgemeinen Systemlast (Hintergrundprozesse, Garbage Collector, andere Threads) ab. Als Anhaltswert für langsam arbeitende Software ist der erhaltene Wert aber allemal gut genug.

Hat man zuerst die gesamte Verarbeitungszeit gemessen, sollte das Einfügen der beiden klammernden Aufrufe von System.currentTimeMillis() um die vermuteten Performance-Engpässe herum folgen. Nach einem Testlauf ist dadurch klar, wie viel Prozent der gesamten Verarbeitungszeit die Applikation in den vermuteten Schwachstellen verbringt. Nur an den Stellen, die einen signifikanten Anteil der gesamten Verarbeitungszeit beanspruchen, sind Optimierungen sinnvoll.

Im Umkehrschluss gilt übrigens: Funktionen, von denen man weiß, dass sie wohl nie schnell laufen, sollten nicht optimiert werden, solange dafür kein Bedarf besteht. Optimierter Code ist meist länger und schwerer verständlich, was die Wartbarkeit verschlechtert.

Im einfachsten Fall kann man die gemessenen Zeiten sicherlich mit System.out.println() ausgeben, aber in komplexeren Anwendungen ist dies nicht unbedingt das Mittel der Wahl. Besser eignet sich ein Logging-Framework wie Log4J oder die Klassen aus java.util.logging, die es seit dem JDK 1.4 gibt. Für Profiling-Daten empfiehlt sich die Verwendung eines eigenen Loggers, etwa mit der Kategorie „Profiling“, der so konfiguriert ist, dass er alle Meldungen in eine eigene Log-Datei schreibt.

Listing 1 zeigt das Grundgerüst für eine mit dieser einfachen Profiling-Maßnahme ausgestattete Klasse. Die Abfrage PROFILING_LOGGER.isLoggable am Anfang minimiert den durch die Zeitmessung verursachten Overhead, falls kein Profiling stattfindet, sodass der Code in der Klasse bleiben und in die Versionsverwaltung aufgenommen werden kann. Dies ermöglicht einfaches An- und Ausschalten des Profiling durch die Konfiguration des Logger.

Mehr Infos

Listing 1

Java-Logging mit System.currentTimeMillis()

import java.util.logging.Level;
import java.util.logging.Logger;

public class Foo {
/** Der normale Logger */
private static final Logger LOGGER =
Logger.getLogger(Foo.class.getName());

/** Der Logger für Profiling-Mitteilungen */
private static final Logger PROFILING_LOGGER =
Logger.getLogger("Profiling");

/** Mit Profiling-Code ausgestattete Methode */
public void f() {
boolean _profiling = PROFILING_LOGGER.isLoggable(Level.INFO);
long _startTime = 0;
if (_profiling) {
_startTime = System.currentTimeMillis();
}
try {
// ... der eigentliche Code ...
} finally {
if (_profiling) {
long dt = System.currentTimeMillis() - _startTime;
PROFILING_LOGGER.info("f() dauerte " + dt + " ms.");
}
}
}
}

Profiling-Code ist ein klassisches Beispiel für einen Aspekt: immer dasselbe, aber über viele Klassen eines Projekts verstreut. Deshalb kann es sinnvoll sein, den Code tatsächlich in einen Aspekt auszulagern, etwa mit aspectj, wie in Listing 2 gezeigt. Dieses Verfahren hat den Vorteil, dass die Anwendung selbst frei von Profiling-Code und ihre Lesbarkeit unbeeinträchtigt bleibt.

Mehr Infos

Listing 2

Eine selbst gebaute aspektorientierte Profiling-Lösung kapselt alle void- und int-Methoden.

public aspect Profiling {
pointcut anyPublicVoidMethod(): execution(public void *.*(..));
pointcut anyPublicIntMethod(): execution(public int *.*(..));

void around(): anyPublicVoidMethod() {
long startTime = System.currentTimeMillis();
try {
proceed();
} finally {
long dt = System.currentTimeMillis() - startTime;
System.out.println(thisJoinPointStaticPart.getSignature() +
" dauerte " + dt + " ms.");
}
}

int around(): anyPublicIntMethod() {
long startTime = System.currentTimeMillis();
try {
return proceed();
} finally {
long dt = System.currentTimeMillis() - startTime;
System.out.println(thisJoinPointStaticPart.getSignature() +
" dauerte " + dt + " ms.");
}
}
}

Der vollständige Text in der Printausgabe stellt einige freie und kommerzielle Werkzeuge zum Profiling von Java-Anwendungen vor. Siehe auch die Leseprobe zum Tuning von Java-Programmen. (ck)