Effektivitetseffekter af undtagelser i Java

1. Oversigt

I Java betragtes undtagelser generelt som dyre og bør ikke bruges til flowkontrol. Denne vejledning vil bevise, at denne opfattelse er korrekt og finde ud af, hvad der forårsager performance-problemet.

2. Opsætning af miljø

Før vi skriver kode for at evaluere præstationsomkostningerne, er vi nødt til at oprette et benchmarking-miljø.

2.1. Java Microbenchmark sele

Det er ikke så let at måle undtagelsesomkostninger som at udføre en metode i en simpel sløjfe og tage den samlede tid i betragtning.

Årsagen er, at en just-in-time kompilator kan komme i vejen og optimere koden. En sådan optimering kan få koden til at fungere bedre, end den rent faktisk ville gøre i et produktionsmiljø. Med andre ord, det kan give falske positive resultater.

For at skabe et kontrolleret miljø, der kan afbøde JVM-optimering, bruger vi Java Microbenchmark Harness eller kort sagt JMH.

De følgende underafsnit gennemgår opsætning af et benchmarking-miljø uden at gå i detaljer med JMH. For mere information om dette værktøj, se vores Microbenchmarking med Java tutorial.

2.2. Opnåelse af JMH-artefakter

For at få JMH-artefakter skal du tilføje disse to afhængigheder til POM:

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

Se Maven Central for de nyeste versioner af JMH Core og JMH Annotation Processor.

2.3. Benchmark-klasse

Vi har brug for en klasse for at holde benchmarks:

@Fork (1) @Warmup (iterationer = 2) @Måling (iterationer = 10) @BenchmarkMode (Mode.AverageTime) @OutputTimeUnit (TimeUnit.MILLISECONDS) offentlig klasse ExceptionBenchmark {privat statisk endelig int LIMIT = 10_000; // benchmarks gå her}

Lad os gennemgå JMH-kommentarerne vist ovenfor:

  • @Gaffel: Angivelse af antallet af gange, JMH skal gyde en ny proces for at køre benchmarks. Vi satte dens værdi til 1 for kun at generere en proces, undgå at vente for længe på at se resultatet
  • @Opvarmning: Bærer opvarmningsparametre. Det gentagelser element er 2 betyder, at de to første kørsler ignoreres, når resultatet beregnes
  • @Måling: Udførelse af måleparametre. En gentagelser værdi på 10 angiver, at JMH vil udføre hver metode 10 gange
  • @BenchmarkMode: Sådan skal JHM samle eksekveringsresultater. Værdien Gennemsnitstid kræver, at JMH tæller den gennemsnitlige tid, en metode har brug for for at fuldføre sine operationer
  • @OutputTimeUnit: Angiver output-tidsenheden, som er millisekund i dette tilfælde

Derudover er der et statisk felt inde i klassekroppen, nemlig BEGRÆNSE. Dette er antallet af gentagelser i hver metode.

2.4. Udførelse af benchmarks

For at udføre benchmarks har vi brug for en vigtigste metode:

public class MappingFrameworksPerformance {public static void main (String [] args) throw Exception {org.openjdk.jmh.Main.main (args); }}

Vi kan pakke projektet i en JAR-fil og køre det på kommandolinjen. Hvis du gør det nu, vil det naturligvis producere en tom output, da vi ikke har tilføjet nogen benchmarking-metode.

For nemheds skyld kan vi tilføje maven-jar-plugin til POM. Dette plugin giver os mulighed for at udføre vigtigste metode inde i en IDE:

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

Den seneste version af maven-jar-plugin kan findes her.

3. Måling af ydeevne

Det er tid til at have nogle benchmarkingmetoder til måling af ydeevne. Hver af disse metoder skal bære @Benchmark kommentar.

3.1. Metode returnerer normalt

Lad os starte med en metode, der vender tilbage normalt; en metode, der ikke kaster en undtagelse:

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

Det sort hul parameter henviser til en forekomst af Sort hul. Dette er en JMH-klasse, der hjælper med at forhindre dead code eliminering, en optimering, som en just-in-time compiler kan udføre.

Benchmarket kaster i dette tilfælde ingen undtagelse. Faktisk, Vi bruger det som en reference til at evaluere ydeevnen for dem, der kaster undtagelser.

Udfører vigtigste metode giver os en rapport:

Benchmark Mode Cnt Score Fejlenheder ExceptionBenchmark.doNotThrowException gennemsnit 10 0,049 ± 0,006 ms / op

Der er ikke noget særligt i dette resultat. Den gennemsnitlige udførelsestid for benchmarket er 0,049 millisekunder, hvilket i sig selv er ret meningsløst.

3.2. Oprettelse og kaste en undtagelse

Her er et andet benchmark, der kaster og fanger undtagelser:

@Benchmark public void throwAndCatchException (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {prøv {throw new Exception (); } fange (Undtagelse e) {blackhole.consume (e); }}}

Lad os se på output:

Benchmark-tilstand Cnt Score Fejlenheder ExceptionBenchmark.doNotThrowException avgt 10 0,048 ± 0,003 ms / op ExceptionBenchmark.throwAndCatchException avgt 10 17.942 ± 0.846 ms / op

Den lille ændring i metodeens udførelsestid doNotThrowException er ikke vigtigt. Det er bare udsving i tilstanden til det underliggende operativsystem og JVM. Den vigtigste afhentning er, at at kaste en undtagelse får en metode til at køre hundreder af gange langsommere.

De næste par underafsnit vil finde ud af, hvad der nøjagtigt fører til en så dramatisk forskel.

3.3. Oprettelse af en undtagelse uden at smide den

I stedet for at oprette, smide og fange en undtagelse opretter vi bare den:

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

Lad os nu udføre de tre benchmarks, vi har erklæret:

Benchmark Mode Cnt Score Fejlenheder ExceptionBenchmark.createExceptionWithoutTrowowingIt avgt 10 17.601 ± 3.152 ms / op ExceptionBenchmark.doNotThrowException avgt 10 0.054 ± 0.014 ms / op ExceptionBenchmark.throwAndCatchException avgt 10 17.174 ± 0.474 ms / op

Resultatet kan komme som en overraskelse: udførelsestiden for den første og den tredje metode er næsten den samme, mens den for den anden er væsentligt mindre.

På dette tidspunkt er det klart, at det kaste og fangst udsagn i sig selv er ret billige. Oprettelsen af ​​undtagelser producerer på den anden side høje omkostninger.

3.4. At kaste en undtagelse uden at tilføje stakspor

Lad os finde ud af, hvorfor det er meget dyrere at konstruere en undtagelse end at lave et almindeligt objekt:

@Benchmark @Fork (værdi = 1, jvmArgs = "-XX: -StackTraceInThrowable") public void throwExceptionWithoutAddingStackTrace (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {prøv {kast ny undtagelse (); } fange (Undtagelse e) {blackhole.consume (e); }}}

Den eneste forskel mellem denne metode og den i underafsnit 3.2 er jvmArgs element. Dens værdi -XX: -StackTraceInThrowable er en JVM-indstilling, der forhindrer, at staksporet føjes til undtagelsen.

Lad os køre benchmarks igen:

Benchmark Mode Cnt Score Fejlenheder ExceptionBenchmark.createExceptionWithoutTrowowingIt avgt 10 17.874 ± 3.199 ms / op ExceptionBenchmark.doNotThrowException avgt 10 0.046 ± 0.003 ms / op ExceptionBenchmark.throwAndCatchException avgt 10 16.268 ± 0.239 ms / op Undtagelse

Ved ikke at udfylde undtagelsen med stacksporingen reducerede vi udførelsens varighed med mere end 100 gange. Tilsyneladende, at gå gennem stakken og tilføje dens rammer til undtagelsen medføre den træghed, vi har set.

3.5. At kaste en undtagelse og afvikle dens stackspor

Lad os endelig se, hvad der sker, hvis vi kaster en undtagelse og afvikler staksporet, når vi fanger det:

@Benchmark public void throwExceptionAndUnwindStackTrace (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {prøv {throw new Exception (); } fange (Undtagelse e) {blackhole.consume (e.getStackTrace ()); }}}

Her er resultatet:

Benchmark Mode Cnt Score Fejl Enheder ExceptionBenchmark.createExceptionWithoutTrowowingIt avgt 10 16.605 ± 0.988 ms / op ExceptionBenchmark.doNotThrowException avgt 10 0.047 ± 0.006 ms / op ExceptionBenchmark.throwAndCatchException avgt 10 16.449 ± 0.304 ms / op undtagelse ExceptionBenchmark.throwExceptionWithoutAddingStackTrace avgt 10 1.185 ± 0,015 ms / op

Bare ved at afvikle stacksporingen ser vi en kæmpestigning på cirka 20 gange i udførelsens varighed. Sagt på en anden måde, ydeevnen er meget dårligere, hvis vi ekstraherer stakkspor fra en undtagelse ud over at smide det.

4. Konklusion

I denne vejledning analyserede vi ydeevneeffekterne af undtagelser. Specifikt fandt det ud af, at præstationsomkostningerne for det meste er tilføjelsen af ​​staksporet til undtagelsen. Hvis dette stakspor udrulles bagefter, bliver overhead meget større.

Da det er dyrt at kaste og håndtere undtagelser, vi skal ikke bruge det til normale programflow. I stedet for, som navnet antyder, bør undtagelser kun bruges i ekstraordinære tilfælde.

Den komplette kildekode kan findes på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found