Kivételek teljesítményhatásai a Java-ban

1. Áttekintés

A Java-ban a kivételeket általában drágának tekintik, és nem szabad felhasználni áramlásszabályozásra. Ez az oktatóanyag bebizonyítja, hogy ez a felfogás helyes, és pontosan meghatározza, mi okozza a teljesítmény problémáját.

2. Környezet beállítása

Mielőtt kódot írnánk a teljesítményköltség értékeléséhez, fel kell állítanunk egy benchmarking környezetet.

2.1. Java Microbenchmark hám

A kivétel rezsijének mérése nem olyan egyszerű, mint egy módszer egyszerű ciklusban történő végrehajtása és a teljes idő figyelembevétele.

Ennek oka az, hogy egy éppen időben érkező fordító akadályozhatja az utat és optimalizálhatja a kódot. Az ilyen optimalizálás révén a kód jobban teljesíthet, mint amilyen a termelési környezetben lenne. Más szavakkal, hamisan pozitív eredményeket hozhat.

A JVM optimalizálását mérsékelni képes ellenőrzött környezet létrehozásához a Java Microbenchmark Harness-t, röviden a JMH-t fogjuk használni.

A következő alfejezetek egy benchmarking környezet felállítását mutatják be anélkül, hogy a JMH részleteibe kerülnének. Ha további információra van szüksége erről az eszközről, olvassa el a Microbenchmarking with Java oktatóanyagot.

2.2. JMH Artifacts megszerzése

A JMH-leletek megszerzéséhez adja hozzá ezt a két függőséget a POM-hoz:

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

A JMH Core és a JMH Annotation Processor legújabb verzióit a Maven Central oldalon találja meg.

2.3. Benchmark osztály

Szükségünk lesz egy osztályra a referenciaértékek megtartásához:

@Fork (1) @Warmup (iterációk = 2) @Measurement (iterációk = 10) @BenchmarkMode (Mode.AverageTime) @OutputTimeUnit (TimeUnit.MILLISECONDS) public class ExceptionBenchmark {private static final int LIMIT = 10_000; // ide kerülnek a referenciaértékek}

Menjünk át a fent látható JMH kommentárokon:

  • @Villa: Annak meghatározása, hogy hányszor kell a JMH-nak új folyamatot létrehoznia a benchmarkok futtatásához. Az értékét 1-re állítottuk, hogy csak egy folyamatot generálhassunk, elkerülve a túl hosszú várakozást az eredmény megtekintéséhez
  • @Bemelegítés: Bemelegedési paraméterek hordozása. A iterációk A 2 elem azt jelenti, hogy az első két menetet figyelmen kívül hagyják az eredmény kiszámításakor
  • @Mérés: Mérési paraméterek hordozása. An iterációk A 10 érték azt jelzi, hogy a JMH 10-szer végrehajtja az egyes módszereket
  • @BenchmarkMode: Így kell a JHM-nek gyűjteni a végrehajtási eredményeket. Az érték Átlagos idő megköveteli a JMH-tól, hogy számolja az átlagos időtartamot, amellyel egy módszer a műveleteinek befejezéséhez szükséges
  • @OutputTimeUnit: A kimeneti időegységet jelzi, amely ebben az esetben milliszekundum

Ezenkívül van egy statikus mező az osztálytesten belül, nevezetesen HATÁR. Ez az iterációk száma az egyes metódustestekben.

2.4. Benchmarkok végrehajtása

A benchmarkok végrehajtásához szükségünk van a fő- módszer:

public class MappingFrameworksPerformance {public static void main (String [] args) dobja a Kivételt {org.openjdk.jmh.Main.main (args); }}

Csomagolhatjuk a projektet JAR fájlba, és a parancssoron futtathatjuk. Ezzel most természetesen üres kimenetet eredményez, mivel nem adtunk hozzá benchmarking módszert.

A kényelem érdekében felvehetjük a maven-jar-plugin a POM-nak. Ez a bővítmény lehetővé teszi számunkra a fő- módszer egy IDE-n belül:

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

A legfrissebb verziója maven-jar-plugin itt található.

3. Teljesítménymérés

Itt az ideje, hogy rendelkezzen néhány benchmarking módszerrel a teljesítmény mérésére. Ezen módszerek mindegyikének tartalmaznia kell a @Viszonyítási alap annotáció.

3.1. Módszer Normál visszatérés

Kezdjük egy normálisan visszatérő módszerrel; vagyis olyan módszer, amely nem vet ki kivételt:

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

A fekete lyuk paraméter hivatkozik egy példányára Fekete lyuk. Ez egy JMH osztály, amely segít megakadályozni az elhalt kódok megszüntetését, egy optimalizálást, amelyet egy éppen időben fordító végezhet.

A viszonyítási alap ebben az esetben nem vet ki kivételt. Valójában, referenciaként fogjuk használni, hogy értékeljük azok kivételes teljesítményét.

A. Végrehajtása fő- módszer jelentést ad nekünk:

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

Ebben az eredményben nincs semmi különös. A benchmark átlagos végrehajtási ideje 0,049 milliszekundum, ami önmagában eléggé értelmetlen.

3.2. Kivétel létrehozása és dobása

Itt van egy másik viszonyítási alap, amely kivételt dob ​​és elkap:

@Benchmark public void dobAndCatchException (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {try {dobj új Exceptiont (); } catch (e kivétel) {blackhole.cumsume (e); }}}

Vessünk egy pillantást a kimenetre:

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

A módszer végrehajtási idejének kis változása doNotThrowException nem fontos. Ez csak az alapul szolgáló operációs rendszer és a JVM állapotának ingadozása. A legfontosabb elvihető az Kivételt dobva a módszer több százszor lassabban fut.

A következő néhány szakasz megtudja, mi vezet pontosan ilyen drámai különbséghez.

3.3. Kivétel létrehozása dobás nélkül

Kivétel létrehozása, eldobása és elkapása helyett csak létrehozzuk:

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

Most hajtsuk végre az általunk deklarált három referenciaértéket:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt 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

Az eredmény meglepetés lehet: az első és a harmadik módszer végrehajtási ideje közel azonos, míg a másodiké lényegesen kisebb.

Ezen a ponton egyértelmű, hogy a dobás és fogás maguk a nyilatkozatok meglehetősen olcsók. A kivételek létrehozása viszont magas rezsit eredményez.

3.4. Kivétel dobása a verem nyomának hozzáadása nélkül

Találjuk ki, hogy miért sokkal drágább a kivétel létrehozása, mint egy közönséges objektum elvégzése:

@Benchmark @Fork (érték = 1, jvmArgs = "-XX: -StackTraceInThrowable") public void dobExceptionWithoutAddingStackTrace (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {try {dobja új kivétel (); } catch (e kivétel) {blackhole.cumsume (e); }}}

Az egyetlen különbség e módszer és a 3.2. Alpontban szereplő módszer között az jvmArgs elem. Értéke -XX: -StackTraceInThrowable egy JVM opció, amely megakadályozza, hogy a verem nyomát hozzáadják a kivételhez.

Futtassuk újra a referenciaértékeket:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt 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

Azzal, hogy nem töltöttük fel a kivételt a verem nyomkövetésével, több mint 100-szor csökkentettük a végrehajtás időtartamát. Látszólag, a verem áthaladása és a képkockák hozzáadása a kivételhez hozza a lassúságot, amelyet láttunk.

3.5. Kivétel dobása és veremnyomának letekerése

Végül nézzük meg, mi történik, ha dobunk egy kivételt és kikapcsoljuk a verem nyomát, amikor elkapjuk:

@Benchmark public void throwExceptionAndUnwindStackTrace (Blackhole blackhole) {for (int i = 0; i <LIMIT; i ++) {try {dobj új kivételet (); } catch (e kivétel) {blackhole.consume (e.getStackTrace ()); }}}

Íme az eredmény:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt 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.30 ExceptionBenchmark.throwExceptionWithoutAddingStackTrace avgt 10 1,185 ± 0,015 ms / op

Csak a verem nyomkövetésének felszámolásával a végrehajtás időtartama mintegy 20-szoros növekedést tapasztalhatunk. Másképp fogalmazva, a teljesítmény sokkal rosszabb, ha a dobás mellett kivonatból kivonjuk a verem nyomát.

4. Következtetés

Ebben az oktatóanyagban a kivételek teljesítményhatásait elemeztük. Pontosabban kiderítette, hogy a teljesítmény költségei többnyire a verem nyomkövetésének a kivételhez való hozzáadásában rejlenek. Ha ezt a verem nyomot később kibontják, a rezsi sokkal nagyobb lesz.

Mivel a kivételek dobása és kezelése drága, nem szabad normál programfolyamatokhoz használni. Ehelyett, amint a neve is mutatja, a kivételeket csak kivételes esetekben szabad felhasználni.

A teljes forráskód megtalálható a GitHub oldalon.