Účinky výnimiek na výkon v Jave

1. Prehľad

V prostredí Java sa výnimky všeobecne považujú za drahé a nemali by sa používať na riadenie toku. Tento návod preukáže, že toto vnímanie je správne, a presne určí, čo spôsobuje problém s výkonom.

2. Nastavenie prostredia

Pred napísaním kódu na vyhodnotenie nákladov na výkon musíme nastaviť testovacie prostredie.

2.1. Java Microbenchmark Harness

Meranie réžie výnimky nie je také ľahké ako vykonanie metódy v jednoduchej slučke a zaznamenanie celkového času.

Dôvod je ten, že kompilátor just-in-time môže prekážať a optimalizovať kód. Takáto optimalizácia môže spôsobiť, že kód bude fungovať lepšie, ako by to skutočne bolo v produkčnom prostredí. Inými slovami, mohlo by to priniesť falošne pozitívne výsledky.

Na vytvorenie kontrolovaného prostredia, ktoré dokáže zmierniť optimalizáciu JVM, použijeme Java Microbenchmark Harness alebo skrátene JMH.

Nasledujúce podkapitoly sa dočkajú nastavenia testovacieho prostredia bez toho, aby sme zachádzali do podrobností JMH. Viac informácií o tomto nástroji nájdete v našom výučbe Microbenchmarking with Java.

2.2. Získanie artefaktov JMH

Ak chcete získať artefakty JMH, pridajte do POM tieto dve závislosti:

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

Najnovšie verzie JMH Core a anotačného procesora JMH nájdete v serveri Maven Central.

2.3. Benchmark Class

Aby sme mohli porovnávať, budeme potrebovať triedu:

@Fork (1) @Warmup (iterations = 2) @Measurement (iterations = 10) @BenchmarkMode (Mode.AverageTime) @OutputTimeUnit (TimeUnit.MILLISECONDS) verejná trieda ExceptionBenchmark {private static final int LIMIT = 10_000; // referenčné hodnoty nájdete tu}

Poďme si prejsť vyššie uvedenými anotáciami JMH:

  • @Vidlička: Zadanie počtu opakovaní, za ktoré musí JMH spustiť nový proces na spustenie referenčných hodnôt. Nastavili sme jeho hodnotu na 1, aby sa vygeneroval iba jeden proces, aby sme sa vyhli príliš dlhému čakaniu na zobrazenie výsledku
  • @ Zahrievanie: Nesie parametre zahrievania. The iterácie prvok s 2 znamená, že prvé dva cykly sú pri výpočte výsledku ignorované
  • @ Meranie: Nesie parametre merania. An iterácie hodnota 10 znamená, že JMH vykoná každú metódu 10-krát
  • @BenchmarkMode: Takto by mal JHM zhromažďovať výsledky vykonávania. Hodnota Priemerný čas vyžaduje, aby JMH spočítal priemerný čas, ktorý metóda potrebuje na dokončenie svojich operácií
  • @OutputTimeUnit: Označuje jednotku času výstupu, ktorá je v tomto prípade milisekunda

Vo vnútri tela triedy je navyše statické pole, a to LIMIT. Toto je počet iterácií v každom tele metódy.

2.4. Vykonávanie referenčných hodnôt

Na vykonávanie referenčných kritérií potrebujeme a hlavný metóda:

verejná trieda MappingFrameworksPerformance {public static void main (String [] args) vyvolá výnimku {org.openjdk.jmh.Main.main (args); }}

Projekt môžeme zabaliť do súboru JAR a spustiť ho na príkazovom riadku. Ak to urobíte teraz, samozrejme to spôsobí prázdny výstup, pretože sme nepridali žiadnu metódu porovnávania.

Pre pohodlie môžeme pridať maven-jar-plugin do POM. Tento plugin nám umožňuje vykonávať hlavný metóda vnútri IDE:

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

Najnovšia verzia servera maven-jar-plugin nájdete tu.

3. Meranie výkonu

Je načase mať k dispozícii nejaké metódy porovnávania na meranie výkonu. Každá z týchto metód musí obsahovať: @Benchmark anotácia.

3.1. Metóda návratu normálne

Začnime s metódou, ktorá sa vracia normálne; to znamená metóda, ktorá nevyvoláva výnimku:

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

The čierna diera parameter odkazuje na inštanciu Čierna diera. Toto je trieda JMH, ktorá pomáha predchádzať eliminácii mŕtveho kódu, optimalizácii, ktorú môže kompilátor just-in-time vykonať.

Benchmark v tomto prípade nevyvoláva žiadnu výnimku. V skutočnosti, použijeme ho ako referenciu na vyhodnotenie výkonu tých, ktorí robia výnimky.

Vykonávanie hlavný metóda nám dá správu:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark.doNotThrowException priem. 10 0,049 ± 0,006 ms / op

Na tomto výsledku nie je nič zvláštne. Priemerný čas vykonania referenčnej hodnoty je 0,049 milisekundy, čo je samo o sebe dosť nezmyselné.

3.2. Vytvorenie a vyhodenie výnimky

Tu je ďalší benchmark, ktorý vyhadzuje a zachytáva výnimky:

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

Poďme sa pozrieť na výstup:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark.doNotThrowException priem. 10 0,048 ± 0,003 ms / op ExceptionBenchmark.throwAndCatchException priem. 10 17 942 ± 0,846 ms / op

Malá zmena v čase vykonania metódy doNotThrowException nie je dôležité. Je to len kolísanie stavu základného OS a JVM. Kľúčovým riešením je to vrhnutie výnimky spôsobí, že metóda beží stokrát pomalšie.

Nasledujúcich niekoľko podkapitol zistí, čo presne vedie k takémuto dramatickému rozdielu.

3.3. Vytvorenie výnimky bez jej odhodenia

Namiesto vytvárania, hádzania a chytania výnimky ju iba vytvoríme:

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

Teraz vykonajme tri referenčné hodnoty, ktoré sme deklarovali:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt priem. 10 17,601 ± 3,152 ms / op ExceptionBenchmark.doNotThrowException priem. 10 0,054 ± 0,014 ms / op ExceptionBenchmark.throwAndCatchException priem. 10 17 174 ± 0,474 ms / op

Výsledok môže byť prekvapený: čas vykonania prvej a tretej metódy je takmer rovnaký, zatiaľ čo čas vykonania druhej metódy je podstatne menší.

V tejto chvíli je zrejmé, že the hodiť a chytiť samotné vyhlásenia sú dosť lacné. Vytváranie výnimiek naopak spôsobuje vysoké režijné náklady.

3.4. Vylúčenie výnimky bez pridania trasovania zásobníka

Poďme na to, prečo je zostavenie výnimky oveľa nákladnejšie ako vykonanie bežného objektu:

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

Jediný rozdiel medzi touto metódou a metódou v pododdiele 3.2 je jvmArgs element. Jeho hodnota -XX: -StackTraceInThrowable je voľba JVM, ktorá zabraňuje pridaniu sledovania zásobníka k výnimke.

Znova spustíme referenčné hodnoty:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt priem. 10 17,874 ± 3,199 ms / op ExceptionBenchmark.doNotThrowException priem. 10 0,046 ± 0,003 ms / op ExceptionBenchmark.throwAndCatchException priem. 10 16,268 ± 0,239 ms.op ExceptionBench

Nevyplnením výnimky sledovaním zásobníka sme skrátili trvanie vykonávania o viac ako 100-krát. Zrejme prechádzanie stohom a pridanie jeho rámcov k výnimke spôsobí pomalosť, ktorú sme videli.

3.5. Vyhodenie výnimky a rozvinutie jej stohovej stopy

Na záver sa pozrime, čo sa stane, ak pri jeho chytení zrušíme výnimku a odvíjame trasovanie zásobníka:

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

Výsledok:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt priem. 10 16 605 ± 0,988 ms / op ExceptionBenchmark.doNotThrowException priem. 10 0,047 ± 0,006 ms / op ExceptionBenchmark.throwAndCatchException priem. 10 16,449 ± 0,304 ms / op ExceptionBenchmark.throwExceptionWithoutAddingStackTrace priem. 10 1,185 ± 0,015 ms / op

Len odvíjaním sledovania zásobníka vidíme ohromné ​​zvýšenie trvania vykonávania asi 20-krát. Inak povedané, výkon je oveľa horší, ak okrem vyhodenia extrahujeme stopu zásobníka z výnimky.

4. Záver

V tomto tutoriáli sme analyzovali účinky výnimiek na výkon. Konkrétne sa zistilo, že náklady na výkon sú väčšinou pridaním sledovania zásobníka k výnimke. Ak sa toto trasovanie zásobníka neskôr odvinie, réžia sa stane oveľa väčšou.

Pretože hádzanie a manipulácia s výnimkami je drahá, nemali by sme ho používať na bežné toky programov. Ako už vyplýva z názvu, výnimky by sa mali používať iba vo výnimočných prípadoch.

Celý zdrojový kód nájdete na GitHub.


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