Prestatie-effecten van uitzonderingen in Java

1. Overzicht

In Java worden uitzonderingen over het algemeen als duur beschouwd en mogen ze niet worden gebruikt voor stroomregeling. Deze tutorial zal bewijzen dat deze perceptie juist is en aangeven wat het prestatieprobleem veroorzaakt.

2. Omgeving instellen

Voordat we code schrijven om de prestatiekosten te evalueren, moeten we een benchmarkomgeving opzetten.

2.1. Java Microbenchmark-harnas

Het meten van de overhead van uitzonderingen is niet zo eenvoudig als het uitvoeren van een methode in een eenvoudige lus en het noteren van de totale tijd.

De reden is dat een just-in-time-compiler in de weg kan zitten en de code kan optimaliseren. Door een dergelijke optimalisatie kan de code beter presteren dan in een productieomgeving. Met andere woorden, het kan vals positieve resultaten opleveren.

Om een ​​gecontroleerde omgeving te creëren die JVM-optimalisatie kan verminderen, gebruiken we Java Microbenchmark Harness, of kortweg JMH.

De volgende subsecties zullen het opzetten van een benchmarkomgeving doorlopen zonder in te gaan op de details van JMH. Voor meer informatie over deze tool, bekijk onze Microbenchmarking met Java-tutorial.

2.2. JMH-artefacten verkrijgen

Om JMH-artefacten te krijgen, voegt u deze twee afhankelijkheden toe aan de POM:

 org.openjdk.jmh jmh-core 1.21 org.openjdk.jmh jmh-generator-annprocess 1.21 

Raadpleeg Maven Central voor de nieuwste versies van JMH Core en JMH Annotation Processor.

2.3. Benchmarkklasse

We hebben een klas nodig om benchmarks vast te houden:

@Fork (1) @Warmup (iteraties = 2) @Measurement (iteraties = 10) @BenchmarkMode (Mode.AverageTime) @OutputTimeUnit (TimeUnit.MILLISECONDS) publieke klasse ExceptionBenchmark {private static final int LIMIT = 10_000; // benchmarks gaan hier}

Laten we de hierboven getoonde JMH-annotaties doornemen:

  • @Vork: Specificeren van het aantal keren dat JMH een nieuw proces moet voortbrengen om benchmarks uit te voeren. We stellen de waarde ervan in op 1 om slechts één proces te genereren, zodat u niet te lang hoeft te wachten om het resultaat te zien
  • @Opwarmen: Opwarmparameters uitvoeren. De iteraties element 2 betekent dat de eerste twee runs worden genegeerd bij het berekenen van het resultaat
  • @Meting: Dragen van meetparameters. Een iteraties waarde van 10 geeft aan dat JMH elke methode 10 keer zal uitvoeren
  • @BenchmarkMode: Dit is hoe JHM uitvoeringsresultaten moet verzamelen. De waarde Gemiddelde tijd vereist dat JMH de gemiddelde tijd telt die een methode nodig heeft om zijn bewerkingen te voltooien
  • @OutputTimeUnit: Geeft de uitvoertijdeenheid aan, in dit geval de milliseconde

Bovendien is er een statisch veld in de klas, namelijk LIMIET. Dit is het aantal iteraties in de hoofdtekst van elke methode.

2.4. Benchmarks uitvoeren

Om benchmarks uit te voeren, hebben we een hoofd methode:

openbare klasse MappingFrameworksPerformance {openbare statische leegte hoofd (String [] args) gooit Uitzondering {org.openjdk.jmh.Main.main (args); }}

We kunnen het project verpakken in een JAR-bestand en het vanaf de opdrachtregel uitvoeren. Als je dit nu doet, krijg je natuurlijk een lege uitvoer omdat we geen benchmarking-methode hebben toegevoegd.

Voor het gemak kunnen we de maven-jar-plugin naar de POM. Met deze plug-in kunnen we het hoofd methode binnen een IDE:

org.apache.maven.plugins maven-jar-plugin 3.2.0 com.baeldung.performancetests.MappingFrameworksPerformance 

De nieuwste versie van maven-jar-plugin vind je hier.

3. Prestatiemeting

Het is tijd om een ​​aantal benchmarkmethoden te hebben om prestaties te meten. Elk van deze methoden moet de extensie @Benchmark annotatie.

3.1. Methode die normaal terugkeert

Laten we beginnen met een methode die normaal terugkeert; dat wil zeggen, een methode die geen uitzondering genereert:

@Benchmark public void doNotThrowException (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {blackhole.consume (new Object ()); }}

De blackhole parameter verwijst naar een instantie van Blackhole. Dit is een JMH-klasse die het elimineren van dode code helpt voorkomen, een optimalisatie die een just-in-time-compiler kan uitvoeren.

De benchmark gooit in dit geval geen uitzondering. In feite, we zullen het gebruiken als een referentie om de prestaties te evalueren van degenen die uitzonderingen genereren.

Het uitvoeren van het hoofd methode geeft ons een rapport:

Benchmarkmodus Cnt-score Fouteenheden ExceptionBenchmark.doNotThrowException avgt 10 0,049 ± 0,006 ms / op

Er is niets speciaals aan dit resultaat. De gemiddelde uitvoeringstijd van de benchmark is 0,049 milliseconden, wat op zich vrij zinloos is.

3.2. Een uitzondering maken en gooien

Hier is nog een benchmark die uitzonderingen gooit en vangt:

@Benchmark public void throwAndCatchException (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {probeer {throw new Exception (); } catch (uitzondering e) {blackhole.consume (e); }}}

Laten we de output eens bekijken:

Benchmarkmodus Cnt Score Fouteenheden ExceptionBenchmark.doNotThrowException gemiddelde 10 0,048 ± 0,003 ms / op ExceptionBenchmark.throwAndCatchException gemiddelde 10 17,942 ± 0,846 ms / op

De kleine verandering in de uitvoeringstijd van de methode doNotThrowException is niet belangrijk. Het is gewoon de fluctuatie in de staat van het onderliggende besturingssysteem en de JVM. De belangrijkste afhaalmaaltijd is dat Als u een uitzondering genereert, wordt een methode honderden keren langzamer uitgevoerd.

De volgende paragrafen zullen ontdekken wat precies tot zo'n dramatisch verschil leidt.

3.3. Een uitzondering creëren zonder deze weg te gooien

In plaats van een uitzondering te maken, te gooien en te vangen, maken we deze gewoon:

@Benchmark public void createExceptionWithoutThrowingIt (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {blackhole.consume (new Exception ()); }}

Laten we nu de drie benchmarks uitvoeren die we hebben verklaard:

Benchmarkmodus Cnt Score Fouteenheden ExceptionBenchmark.createExceptionWithoutThrowingIt gemiddelde 10 17.601 ± 3.152 ms / op ExceptionBenchmark.doNotThrowException gemiddelde 10 0.054 ± 0.014 ms / op ExceptionBenchmark.throwAndCatchException gemiddelde 10 17.174 ± 0.474 ms / op

Het resultaat kan als een verrassing komen: de uitvoeringstijd van de eerste en de derde methode is nagenoeg gelijk, terwijl die van de tweede aanzienlijk kleiner is.

Op dit punt is het duidelijk dat de werpen en vangst uitspraken zelf zijn redelijk goedkoop. Het creëren van uitzonderingen daarentegen leidt tot hoge overheadkosten.

3.4. Een uitzondering werpen zonder de stapeltracering toe te voegen

Laten we eens kijken waarom het maken van een uitzondering veel duurder is dan het maken van een gewoon object:

@Benchmark @Fork (waarde = 1, jvmArgs = "-XX: -StackTraceInThrowable") public void throwExceptionWithoutAddingStackTrace (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {probeer {throw new Exception (); } catch (uitzondering e) {blackhole.consume (e); }}}

Het enige verschil tussen deze methode en die in paragraaf 3.2 is de jvmArgs element. Zijn waarde -XX: -StackTraceInThrowable is een JVM-optie, waardoor wordt voorkomen dat de stacktracering wordt toegevoegd aan de uitzondering.

Laten we de benchmarks opnieuw uitvoeren:

Benchmark-modus Cnt Score Fouteenheden ExceptionBenchmark.createExceptionWithoutThrowingIt gemiddelde 10 17.874 ± 3.199 ms / op ExceptionBenchmark.doNotThrowException gemiddelde 10 0.046 ± 0.003 ms / op ExceptionBenchmark.throwAndCatchException gemiddelde 10 16.268 ± 0.239 ms / op de gemiddelde waarde

Door de uitzondering niet te vullen met de stacktracering, hebben we de uitvoeringsduur met meer dan 100 keer verminderd. Blijkbaar, door de stapel lopen en de frames toevoegen aan de uitzondering, veroorzaken de traagheid die we hebben gezien.

3.5. Een uitzondering werpen en de stapeltracering ervan afwikkelen

Laten we tot slot kijken wat er gebeurt als we een uitzondering werpen en de stapeltracering afwikkelen bij het vangen ervan:

@Benchmark public void throwExceptionAndUnwindStackTrace (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {probeer {throw new Exception (); } catch (uitzondering e) {blackhole.consume (e.getStackTrace ()); }}}

Dit is het resultaat:

Benchmarkmodus Cnt Score Fouteenheden ExceptionBenchmark.createExceptionWithoutThrowingIt gemiddelde 10 16.605 ± 0.988 ms / op ExceptionBenchmark.doNotThrowException gemiddelde 10 0.047 ± 0.006 ms / op ExceptionBenchmark.throwAndCatchException gemiddelde 10 16.449 ± 0.304 ms / opTchmark.Exception ExceptionBenchmark.throwExceptionWithoutAddingStackTrace gemiddelde 10 1,185 ± 0,015 ms / op

Alleen al door de stacktrace af te wikkelen, zien we een enorme toename van zo'n 20 keer in de duur van de uitvoering. In andere woorden, de prestatie is veel slechter als we de stacktracering van een uitzondering extraheren en deze niet weggooien.

4. Conclusie

In deze zelfstudie hebben we de prestatie-effecten van uitzonderingen geanalyseerd. Specifiek, het ontdekte dat de prestatiekosten meestal zitten in de toevoeging van de stacktracering aan de uitzondering. Als deze stacktracering daarna wordt afgewikkeld, wordt de overhead veel groter.

Aangezien het gooien en hanteren van uitzonderingen duur is, we zouden het niet moeten gebruiken voor normale programmastromen. In plaats daarvan mogen, zoals de naam al aangeeft, uitzonderingen alleen in uitzonderlijke gevallen worden gebruikt.

De volledige broncode is te vinden op GitHub.