Java Exceptions für den Kontrollfluss

Verlust an Performance durch unbedachten Einsatz von Exceptions

Exceptions zur Steuerung des Kontrollfluss - Software Performance Anti-Pattern von Nikolai Moesus | QMETHODS | August 2017
 

Anti-Pattern Beschreibung

Exceptions zur Steuerung des Kontrollfluss - Software Performance Anti-Pattern Manchmal erscheint es praktisch Exceptions zur Steuerung des Kontrollflusses in Java zu verwenden, um bspw. aus tief verschachtelten Schleifen oder Funktionen herauszuspringen. Diese Verwendung von Exceptions verringert nicht nur die Verständlichkeit des Softwarecodes (es besteht eine große Ähnlichkeit mit der GoTo Anweisung - Basic lässt grüßen!), sondern hat auch negativen Einfluss auf die Performance der Softwareanwendung.
Die Hierarchie der Methodenaufrufe (Stack Trace) einer Exception beschreibt, durch welche Methodenaufrufe das Programm zu dem Ort gelangt ist, an dem die Exception aufgetreten ist. Zum Behandeln von Ausnahmesituationen und Analysieren von Programmfehlern ist das sehr praktisch, jedoch zur Steuerung des Kontrollflusses nur bedingt zu empfehlen.
Das Erzeugen einer Java Exception kann zeit- und ressourcenintensiv sein, da durch die Java Laufzeitumgebung die vollständige Aufrufhierarchie der Methoden dynamisch ermittelt wird. Die Auswirkung auf die Performance hängt hierbei von der Tiefe der Aufrufhierarchie ab. Befindet sich das Programm beispielsweise in der 100sten Stufe einer rekursiven Methode, ist das Ermitteln der Hierarchie an Methodenaufrufen wesentlich aufwändiger.
Im Rahmen einer Performanceanalyse einer Softwareanwendung sind wir auf das Framework 'JPA Security' gestoßen, welches die interne Abarbeitung auf Basis von Exceptions umgesetzt hat und somit die Antwortzeit der Software-Anwendung signifikant beeinflusst hat.
 

Software-Code Beispiele

Das erste Code-Beispiel zeigt, wie eine Exception geworfen wird, um schnell einen längeren Code-Block zu überspringen - ein klassischer GoTo.
void throwingException () {
	for (/*condition*/)
		try {
			...
			if (/*condition*/) {
				throw new FlowException();
			}
			// a lot of code lines
		} catch (FlowException e) {
			// goto here
		}
	}
}
Negativ Code-Beispiel: Verwendung einer Exception zur Steuerung des Kontrollflusses
 
Das zweite Code-Beispiel zeigt, wie immer wieder dieselbe im Vorhinein generierte Exception geworfen wird. Dadurch wird das Neuerstellen des Stack Trace eingespart.
void throwingFinalException () {
	final FlowException ex = new FlowException();
	for (/*condition*/)
		try {
			...
			if (/*condition*/) {
				throw ex;
			}
			// a lot of code lines
		} catch (FlowException e) {
			// goto here
		}
	}
}
Kritikwürdiges Code-Beispiel: Verwendung einer statischen Exception zur Steuerung des Kontrollflusses
 
Das dritte Code-Beispiel zeigt die 'normale' Verwendung mittels IF-Operator.
void throwingFinalException () {
	for (/*condition*/)
		...
		if (/*is not condition*/) {
			// a lot of code lines
		}
	}
}
Positiv Code-Beispiel: Regulärer Kontrollfluss
 

Analyse der Laufzeit

Die Laufzeitanalyse soll uns die Auswirkung der unterschiedlichen Implementierungsvarianten deutlich machen. Die folgende Abbildung zeigt neun verschiedene, mittels des Java Performance Servlets durchgeführte Lastszenarien. Folgende Einstellungen wurden verwendet:
  • 10^6 Durchläufe 
  • kein Warm-Up 
  • Ausführung einer Garbage Collection vor dem Start 
Dargestellt ist jeweils die Ausführungszeit von 10^6 Durchläufen der verschiedenen Varianten. Der Parameter "extra Stack-Tiefe" gibt an, um wie viele Ebenen der Stack künstlich vergrößert wurde, zusätzlich zu den ohnehin durch den Aufruf des Servlets vorhandenen Ebenen.
An den ersten drei Balken ist deutlich erkennbar, dass es wesentlich teurer ist, Exceptions zu verwenden, wenn viele Funktionen auf dem Stack liegen. Die nächsten drei Balken zeigen, dass es nicht das Werfen der Exception ist, was die hohen Kosten verursacht; es muss also das Erzeugen sein! Die letzten drei Balken zeigen, dass das Werfen einer statischen Exception nur noch minimal teurer ist als der reguläre Kontrollfluss.
 
Exceptions zur Steuerung des Kontrollfluss - Software Performance Anti-Pattern
Abbildung: Performancevergleich für Kontrollfluss-Implementierung mit Exceptions und IF-Operator
 
Der Softwarecode für das Java Performance Servlet zur Vermessung der Implementierungsvarianten steht im QMETHODS GitHub zur Verfügung. Nutzen sie es, um Experimente in ihrer eigenen Umgebung auszuführen. Zur Vermessung haben wir Dynatrace AppMon eingesetzt.
QMETHODS Github
 

Refactoring Empfehlung

Exceptions sollten idealerweise nur für echte Ausnahmefälle verwendet werden, dann fallen auch die Performanceeinbussen für die Erstellung der Exception nicht ins Gewicht. Werden Exceptions dennoch für den Kontrollfluss verwendet, sollte eine wiederverwendbare Exception-Instanz verwendet werden, um die Aufrufhierarchie nur einmal ermitteln zu müssen. Besonders beachtet werden sollte dies in ...
  • hochfrequentierten Methoden, 
  • umfangreichen Programmschleifen (for, while) oder 
  • rekursiven Aufrufen. 
Für diese Fälle würden wir ein entsprechendes Refactoring dringend empfehlen!
 
Zurück zur Anti-Pattern Übersichtseite



Ihr Kommentar zum Blog-Beitrag

Nennen Sie uns bitte Ihren Namen und E-Mail-Adresse zur Verifikation. Die E-Mail Adresse wird nicht veröffentlicht!