Performance von Exceptions in Java

von Hubert Schmid vom 2012-10-28

Exceptions sind langsam. Diese Aussage habe ich kürzlich mal wieder im Zusammenhang mit Java gelesen. Alleinstehend und ohne Bezugssystem sagt sie nicht viel aus, aber ich nehme sie als Aufhänger, um die Situation ein wenig genauer zu betrachten.

Exceptions verbrauchen Ressourcen.

Diese Aussage ist banal. Und wie einfach sie auch immer ist: Sie ist wichtig. Der folgende Code verbraucht sowohl CPU- als auch Speicher-Ressourcen. Führt man die Anweisungen wiederholt in einer Schleife aus, so lässt sich die Ausführungszeit hinreichend genau bestimmen, mit der Ausführungszeit ohne diese Anweisungen vergleichen und dadurch den Unterschied aufzeigen.

try { throw new RuntimeException("some text"); } catch (RuntimeException e) { // nothing }

Verbrauchen keine Exceptions auch Ressourcen?

Im vorherigen Beispiel wurden Ausnahmen geworfen und im catch-Block gefangen. Wie sieht die Performance aus, wenn der catch-Block zwar existiert, aber nie durchlaufen wird? Für diese Frage brauche ich ein wenig mehr Code um zu verhindern, dass die Laufzeitoptimierung die Ergebnisse zu stark beeinträchtigt. Es lohnt sich aber genau hinzuschauen, da ich weiter unten darauf aufbauen werde.

interface Foo { Object call(int index); } final class Bar implements Foo { public Object call(int index) { return "instance " + index; } } // in main class static void perform(Object[] dummy, Foo foo, int numberOfIterations) { for (int i = 0; i < numberOfIterations; ++i) { int index = i % dummy.length; try { dummy[index] = foo.call(i); } catch (Exception e) { dummy[index] = e; } } }

In der Schleife wird die Methode Bar.call aufgerufen, die ein String-Objekt erzeugt und zurückgibt. Die Idee ist etwas zu haben, gegen das man vergleichen kann. Das Ergebnis ist einfach: Es macht keinen Unterschied, ob in der Methode perform ein try-catch-Block verwendet wird oder nicht. Die Laufzeit ist in beiden Fällen identisch, das heißt sie wird von der Methode Bar.call dominiert.

Wer ist schneller?

Interessanter ist die Situation, in der tatsächlich Ausnahmen geworfen werden. Dafür ergänze ich den Code um die Klasse Baz, die ebenfalls die Schnittstelle Foo implementiert, und deren call-Methode immer eine Ausnahme wirft. Die Laufzeit vergleiche ich mit der obigen Implementierung.

Das Ergebnis ist interessant: Auf meinem Rechner ist die Ausführung mit der Klasse Baz fast acht mal schneller. Richtig gelesen: Die Variante mit Ausnahmen ist deutlich schneller.

Das ist ganz schön schnell – und vermutlich ein wenig überraschend. Die Erklärung ist, dass einerseits die Stringverknüpfung relativ teuer ist, und dass ich andererseits beim Werfen der Ausnahme ein wenig getrickst habe. Denn die Instanz der Klasse RuntimeException wird einmalig erzeugt und bei jedem Methodenaufruf wiederverwendet und geworfen.

final class Baz implements Foo { private final RuntimeException e = new RuntimeException("some text"); public Object call(int index) { throw e; } }

Teure Instanzen!

Ganz anders sieht die Situation aus, wenn bei jedem Methodenaufruf eine neue Instanz der Klasse RuntimeException erzeugt wird. Mit der folgenden Änderung wird der Code über 170 mal langsamer – und damit auch deutlich langsamer als der Referenzcode mit der Stringverknüpfung.

final class Baz implements Foo { public Object call(int index) { throw new RuntimeException("some text"); } }

Das Problem liegt ganz offensichtlich nicht in der Ausnahmeverarbeitung sondern in der Implementierung der RuntimeException beziehungsweise einer ihrer Basisklassen. Nach dem Verursacher muss man nicht lange suchen: Ganz eindeutig ist die Bestimmung der Stacktrace für die schlechte Performance verantwortlich.

Ohne Stacktrace.

Ein kleiner Test sorgt für die letzte Gewissheit: Die Klasse RuntimeException besitzt auch einen Konstruktor, mit dem man die Bestimmung des Stacktrace unterdrücken kann. Die Verwendung ist ein wenig umständlich, da noch zwei andere Parameter gesetzt werden müssen. Aber das ließe sich in eigenen Klassen im Zweifelsfall verbergen.

final class Baz implements Foo { public Object call(int index) { throw new RuntimeException("some text", null, true, false) { }; } }

Mein Testprogramm wird mit dieser Änderung gleich um zwei Größenordnungen schneller. Diese Implementierung ist nur noch um einen Faktor 2,5 langsamer als die Implementierung mit der einmal erzeugten RuntimeException, und damit insbesondere auch schneller als die Stringverknüpfung.

Final.

Exceptions sind langsam. Deutlicher langsamer als die Stringverknüpfung.

Oder anders formuliert: Exceptions sind schnell. Sie wirken sich nur dann auf die Performance aus, wenn sie tatsächlich erzeugt werden. Sofern das nicht in unglaublichen Massen passiert, sind alle Bedenken bezüglich Ausnahmen hinfällig. Andernfalls bieten sich mehrere Optimierungsmöglichkeiten, so dass selbst in den innersten Schleifen die Wirkung kaum noch messbar ist. Das heißt: Performance ist im Allgemeinen kein Kriterium für oder wider Ausnahmebehandlung.