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.