Ytelseseffekter av unntak i Java

1. Oversikt

I Java blir unntak generelt sett på som dyre og bør ikke brukes til flytkontroll. Denne opplæringen vil bevise at denne oppfatningen er riktig og finne ut hva som forårsaker ytelsesproblemet.

2. Sette opp miljø

Før vi skriver kode for å evaluere ytelseskostnadene, må vi sette opp et referansemiljø.

2.1. Java Microbenchmark sele

Å måle unntakskostnader er ikke like enkelt som å utføre en metode i en enkel sløyfe og ta hensyn til den totale tiden.

Årsaken er at en just-in-time kompilator kan komme i veien og optimalisere koden. Slik optimalisering kan gjøre at koden fungerer bedre enn den faktisk ville gjort i et produksjonsmiljø. Med andre ord, det kan gi falskt positive resultater.

For å skape et kontrollert miljø som kan redusere JVM-optimalisering, bruker vi Java Microbenchmark Harness, eller kort sagt JMH.

Følgende underavsnitt vil gå gjennom å sette opp et referansemiljø uten å gå inn i detaljene til JMH. For mer informasjon om dette verktøyet, vennligst sjekk ut Microbenchmarking with Java tutorial.

2.2. Innhenting av JMH-gjenstander

For å få JMH-gjenstander, legg til disse to avhengighetene i POM:

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

Se Maven Central for de nyeste versjonene av JMH Core og JMH Annotation Processor.

2.3. Referanseklasse

Vi trenger en klasse for å holde referanser:

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

La oss gå gjennom JMH-kommentarene vist ovenfor:

  • @Gaffel: Angi antall ganger JMH må gyte en ny prosess for å kjøre referanser. Vi setter verdien til 1 for å generere bare en prosess, og unngår å vente for lenge på å se resultatet
  • @Varme opp: Bærer oppvarmingsparametere. De iterasjoner element er 2 betyr at de to første løpene blir ignorert når du beregner resultatet
  • @Mål: Bære måleparametere. An iterasjoner verdien 10 indikerer at JMH vil utføre hver metode 10 ganger
  • @BenchmarkMode: Slik skal JHM samle gjennomføringsresultater. Verdien Gjennomsnittstid krever at JMH teller gjennomsnittlig tid en metode trenger for å fullføre operasjonene
  • @OutputTimeUnit: Angir utgangstidsenheten, som er millisekundet i dette tilfellet

I tillegg er det et statisk felt inne i klassekroppen, nemlig GRENSE. Dette er antall iterasjoner i hver metodekropp.

2.4. Utføre referanser

For å utføre referanser trenger vi a hoved- metode:

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

Vi kan pakke prosjektet inn i en JAR-fil og kjøre det på kommandolinjen. Hvis du gjør det nå, vil det selvfølgelig gi et tomt resultat, ettersom vi ikke har lagt til noen benchmarking-metode.

For enkelhets skyld kan vi legge til maven-jar-plugin til POM. Denne pluginen lar oss utføre hoved- metode inne i en IDE:

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

Den siste versjonen av maven-jar-plugin finner du her.

3. Ytelsesmåling

Det er på tide å ha noen benchmarking-metoder for å måle ytelse. Hver av disse metodene må bære @Benchmark kommentar.

3.1. Metode tilbake normalt

La oss starte med en metode som kommer tilbake normalt; det vil si en metode som ikke kaster et unntak:

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

De svart hull parameter refererer til en forekomst av Svart hull. Dette er en JMH-klasse som hjelper til med å forhindre eliminering av død kode, en optimalisering en just-in-time kompilator kan utføre.

Referansen, i dette tilfellet, kaster ikke noe unntak. Faktisk, Vi bruker den som referanse for å evaluere ytelsen til de som kaster unntak.

Gjennomføre hoved- metoden vil gi oss en rapport:

Referansemodus Cnt Score Feilenheter ExceptionBenchmark.doNotThrowException avgt 10 0,049 ± 0,006 ms / op

Det er ikke noe spesielt i dette resultatet. Den gjennomsnittlige utførelsestiden for referanseindeksen er 0,049 millisekunder, noe som i seg selv er ganske meningsløst.

3.2. Opprette og kaste et unntak

Her er et annet mål som kaster og fanger unntak:

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

La oss ta en titt på utgangen:

Referansemodus Cnt Score Feilenheter ExceptionBenchmark.doNotThrowException avgt 10 0,048 ± 0,003 ms / op ExceptionBenchmark.throwAndCatchException avgt 10 17.942 ± 0.846 ms / op

Den lille endringen i utførelsestiden for metoden doNotThrowException er ikke viktig. Det er bare svingningene i tilstanden til det underliggende operativsystemet og JVM. Nøkkelen til takeaway er at å kaste et unntak gjør at en metode går hundrevis av ganger tregere.

De neste få delene vil finne ut hva som fører til en så dramatisk forskjell.

3.3. Opprette et unntak uten å kaste det

I stedet for å lage, kaste og fange et unntak, lager vi det bare:

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

La oss nå utføre de tre målene vi har erklært:

Benchmark Mode Cnt Score Error Units 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: utførelsestiden for den første og den tredje metoden er nesten den samme, mens den for den andre er vesentlig mindre.

På dette punktet er det klart det de kaste og å fange uttalelser i seg selv er ganske billige. Opprettelsen av unntak gir derimot høye omkostninger.

3.4. Kaste et unntak uten å legge til stabelspor

La oss finne ut hvorfor det er mye dyrere å konstruere et unntak enn å gjøre et vanlig objekt:

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

Den eneste forskjellen mellom denne metoden og den i underavsnitt 3.2 er jvmArgs element. Dens verdi -XX: -StackTraceInThrowable er et JVM-alternativ, slik at stabelsporet ikke blir lagt til unntaket.

La oss kjøre referansene igjen:

Referansemodus Cnt Score Feilenheter ExceptionBenchmark.createExceptionWithoutTrowingIt 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 ExceptionBegr.

Ved ikke å fylle ut unntaket med stakksporingen, reduserte vi utførelsens varighet med mer enn 100 ganger. Tilsynelatende, Å gå gjennom stabelen og legge rammene til unntaket, forårsaker den svakheten vi har sett.

3.5. Kaste et unntak og avvikle stakkens spor

Til slutt, la oss se hva som skjer hvis vi kaster et unntak og slapper av stabelsporet når vi fanger det:

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

Her er utfallet:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutTrowingIt 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 Unntaket ExceptionBenchmark.throwExceptionWithoutAddingStackTrace avgt 10 1.185 ± 0,015 ms / op

Bare ved å koble stakkesporet, ser vi en enorm økning på rundt 20 ganger i utførelsesvarigheten. Sagt på en annen måte, ytelsen er mye dårligere hvis vi trekker ut stack-spor fra et unntak i tillegg til å kaste den.

4. Konklusjon

I denne opplæringen analyserte vi ytelseseffektene av unntak. Spesielt fant det ut at ytelseskostnaden hovedsakelig er i tillegg til stakkesporet til unntaket. Hvis dette stabelsporet blir spolet etterpå, blir overhead mye større.

Siden det er dyrt å kaste og håndtere unntak, vi skal ikke bruke den til normale programflyter. I stedet, som navnet antyder, bør unntak bare brukes i unntakstilfeller.

Den komplette kildekoden finner du på GitHub.


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