Da li hvatanje izuzetaka pomoću Java try-catch zaista utiče na performanse?
"Erge, gledaj ovaj užasan kod — for petlja unutra ima try-catch, zar ne znaš da try-catch donosi gubitak performansi?" Lao Wang je s punom ozbiljnošću pokazivao na kod na ekranu:
for (int i = 0; i < 5000; i++) {
try {
dosth
} catch (Exception e) {
e.printStackTrace();
}
}Pognuo sam se da pogledam kod. "A kako bi onda ti, Lao Wang, to promenio?"
"Pa da se try-catch izbaci napolje, naravno!" Lao Wang je izgovorio bez razmišljanja.
"Zar si lud? Bez obzira na performanse, svrha ovog koda je očigledno da greška u jednom pozivu unutar petlje ne zaustavi rad petlje; ako ga izbaciš napolje, poslovna logika će se promeniti!"
Lao Wang je počeškao svoju ćelu glavu. "Pa, i jeste tako."
"Da se vratimo — hvatanje cele for petlje i hvatanje unutar petlje, kada nema greške, zapravo imaju slične performanse," pomenuo sam uz gutljaj kafe, usput, spremao sam se da se pokažem pred Lao Wang-om.
"Šta to znači?" Lao Wang me gledao pomalo zbunjeno. "try-catch donosi gubitak performansi, ja sam to pročitao na internetu!"
Kao što sam i očekivao, Lao Wang je zagrizeo. Bez reči sam otvorio IDEA i nabrzinu otkucao sledeći kod:
public class TryCatchTest {
// @Benchmark anotacijom označava se metod kao metod za benchmark test
@Benchmark
public void tryfor(Blackhole blackhole) {
// try-catch iskazom se obavija for petlja
try {
for (int i = 0; i < 1000; i++) {
// u petlji se poziva metod Blackhole.consume()
blackhole.consume(i);
}
} catch (Exception e) {
// hvata izuzetak i ispisuje stack trace
e.printStackTrace();
}
}
// @Benchmark anotacijom označava se još jedan metod kao metod za benchmark test
@Benchmark
public void fortry(Blackhole blackhole) {
// for petljom se obavija try-catch iskaz
for (int i = 0; i < 1000; i++) {
try {
// u try bloku se poziva metod Blackhole.consume()
blackhole.consume(i);
} catch (Exception e) {
// hvata izuzetak i ispisuje stack trace
e.printStackTrace();
}
}
}
}Ovde mi dozvolite da dodam nekoliko pojmova kako biste bolje razumeli ovaj kod.
Prvo:
@Benchmarkje anotacija iz biblioteke JMH (Java Microbenchmark Harness) kojom se metod označava kao metod za benchmark test. JMH je paket alata specijalno namenjen pisanju Java mikro-benchmarka, koji sadrži alate i biblioteke za testiranje performansi Java koda i fino podešavanje JVM-a. Metod označen anotacijom@BenchmarkJMH automatski prepoznaje kao metod za benchmark test i u vreme izvršavanja vrši benchmark testiranje. Tokom benchmark testiranja JMH više puta pokreće označeni metod, meri vreme izvršavanja, propusnost, latenciju i druge metrike i generiše statističke rezultate.
Drugo: prilikom JMH benchmark testiranja, da bismo sprečili JIT kompilator da optimizuje pojedine operacije u testiranom kodu, potrebno je u test kodu koristiti neke placeholdere, kako bi kompilator smatrao da te operacije imaju smisao i da ih ne treba optimizovati. Metod
Blackhole.consume()je baš takav placeholder. On zauzima izvesno CPU vreme i memorijski prostor kako bi se osigurala tačnost i pouzdanost rezultata testa.
"Prazna priča ne vredi koliko show code — vidiš li, Lao Wang, izvukao sam try-catch iz for petlje i poredim sa onim unutar for petlje; pogađaš koliko se razlikuju?"
"Ta, sigurno tryfor ima bolje performanse, ne treba ni da se razmišlja; ako nije tako, pratiću kosu naopako!" Lao Wang je to rekao pun samopouzdanja.
Bilo mi je mrsko da mu dalje prazno pričam, pa sam direktno pokrenuo benchmark. Rezultat je:

Može se videti da su performanse obeju zapravo slične (što je broj veći, to bolje): 551063.024 VS 551525.861.
Ovde ćemo ukratko predstaviti vodič za korišćenje JMH-a.
Prvi korak, u datoteku pom.xml dodajte zavisnosti.
<!-- Uvesti JMH paket alata -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>Drugi korak, u Intellij IDEA instalirajte JMH dodatak.

Treći korak, u editoru koda kliknite na ikonicu sa vremenom i play. Zatim mirno sačekajte rezultate — na mojoj mašini (32G RAM-a, Intel i7) trajalo je 16 minuta, jezavo sporo, jer JMH teži potpunoj nepristrasnosti 😂

Lao Wang je ostao zapanjen. "Gde je onaj uticaj na performanse? Zašto ga nema?"
Direktno sam pustio javap, da pokažem Lao Wang-u da zapravo na nivou bajtkoda nema razlike između te dve implementacije:
tryfor bajtkod
Tabela izuzetaka obuhvata linije 0–20; ako se u tim linijama pojavi problem, skače se direktno na liniju 23 radi obrade.

fortry bajtkod
Razlika je samo u tome što je opseg tabele izuzetaka nešto manji, obuhvata linije 9–14, a ostalo je isto kao kod tryfor.

Dakle, na nivou bajtkoda, kada se ne baca izuzetak, nema razlike u efikasnosti izvršavanja.
"Pa zašto se onda po internetu priča da try-catch ima probleme sa performansama?" Lao Wonga je to veoma čudilo.
Ta tvrdnja zaista postoji; u knjizi "Effective Java" pominje se problem performansi try-catch-a:

Kaže se da ne treba slušati pola priče; dok sam učio, najviše sam se plašio onoga što delimično razumem — ko potpuno razume, na testu će izabrati tačno; ko uopšte ne razume, možda pogodi; ali ko delimično razume, nužno bira pogrešnu opciju!
U knjizi "Effective Java" zapravo se kaže da try-catch ne treba koristiti kao zamenu za normalan kod. Knjiga navodi primer u kome se normalna for petlja zasigurno implementira ovako:
for ( Mountain m : range )
m.climb();Ali neki pametnjaković upravo to ne radi, nego pomoću try-catch zaobilazno implementira petlju:
/* Horrible abuse of exceptions. Don't ever do this! */
try {
int i = 0;
while ( true )
range[i++].climb();
} catch ( ArrayIndexOutOfBoundsException e ) {
}Ovaj potez je pomalo preturao; poređenje ovih dveju implementacija već pokazuje gubitak performansi.
Hajde da direktno uporedimo for petlju sa try-catch i bez try-catch; kod je sledeći:
public class TryCatchTest1 {
@Benchmark
public void fornotry(Blackhole blackhole) {
for (int i = 0; i < 1000; i++) {
blackhole.consume(i);
}
}
@Benchmark
public void tryfor(Blackhole blackhole) {
for (int i = 0; i < 1000; i++) {
try {
blackhole.consume(i);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}Rezultat je:

± je slično; pogledajte direktno poređenje rezultata napred — zaista, bez try-catch performanse su nešto bolje, što se poklapa sa tvrdnjom iz knjige da try-catch utiče na neke specifične JVM optimizacije. Međutim, knjiga ne navodi na koje tačno optimizacije utiče; pretpostavljam da je u pitanju nešto poput preuređivanja instrukcija (instruction reordering).
U redu, sad da sumiram tvrdnje o problemu performansi try-catch-a:
try-catchu poređenju sa odsustvomtry-catchzaista ima izvestan uticaj na performanse, ali cilj je da nas odvrati od korišćenjatry-catchkao zamene za implementacije koje inače ne moraju da ga koriste, a ne da zabrani korišćenjetry-catch.try-catchunutar for petlje itry-catchkoji obavija celu for petlju imaju slične performanse; u suštini, to je razlika u načinu obrade posla i nema veze sa performansama — ključno je kako obrađujete poslovni tok.- Iako se zna da
try-catchima uticaj na performanse, u poslu ne treba izbegavati njegovo korišćenje — implementacija posla je na prvom mestu (sve dok nije onaj preturani kod iz primera u knjizi). U nestandardnim situacijama performanse su na drugom mestu; svesno izbegavajtetry-catchna velikom opsegu, hvatajte samo ono što je potrebno (ako niste sigurni, možete uhvatiti sve — bezbedno izvršavanje koda je na prvom mestu).
"Dobro, Lao Wang, jesi li razumeo?"
"Baš si u formi, Erge, pričaš ko iz knjige; ajde, častim te ovsenim latteom!" Lao Wang me uhvatio za ruku. Ja sam se istrgao: "Dosta, tek što sam popio kafu; a ono tvoje pranje kose naopako, ajde, brzo!" Odmah sam primetio da Lao Wang pokušava da promeni temu.
"Praću, praću; hajde da prvo popijemo kafu, večeras kad se vratim kući pratiću!"
Link za preuzimanje: https://mp.weixin.qq.com/s/H870jLz32oEI_HCMVt1m5Q, autor: yes, revizija i optimizacija: Chenmo Wang Er
