Dziś krótszy wpis jednakże pierwszy z serii “Java Performance“, w której będę opisywał rozwiązania związane z wydajnością JVM’a oraz Javy. Pierwszy wpis dotyczy optymalizacji stosu wywołań (ang. stack trace), który jest wywoływany w momencie wystąpienia wyjątku.
Do testów posłużyłem się JMH. Niebawem opiszę jak wykorzystywać go do przeprowadzania benchamarków, dzięki którym można się dowiedzieć, które rozwiązanie jest szybsze. Jak wspominałem we wstępie w momencie wystąpienia wyjątku wywoływany jest stos wywołań:
Exception in thread "main" benchmarks.ExampleException: CodeCouple.pl - Exception at Test.main(Test.java:10) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Znajdują się tu wpisy ramek (ang. frames), które przechowywane są na stosie (ang. stack) JVM. Wyświetlane są one w takiej kolejność w jakiej przechowywane są na stosie czyli FIFO (ang. first in first out). Sam stos wywołań jest przydatny w momencie, gdy chcemy dowiedzieć się jaka była ścieżka wystąpienia błędu. Jedna z informacji, która jest dla nas przydatna to exception message:
Exception in thread "main" org.openjdk.jmh.runner.RunnerException:
CodeCouple.pl – Exception
W klasie Exception
znajduję się metoda fillInStackTrace
, która odpowiedzialna jest za wypełnianie stosu wywołań. Tworząc nasz wyjątek możemy tą metodę nadpisać:
public class ExampleException extends Exception { public ExampleException(String message) { super(message); } @Override public synchronized Throwable fillInStackTrace() { return this; } }
Dzięki temu podczas wywołania wyjątku dostaniemy tylko exception message bez stosu wywołań co jest pożądanym zachowaniem jeśli chcemy poprawić wydajność:
Exception in thread "main" benchmarks.ExampleException:
CodeCouple.pl – Exception
Dla porównania czasów wykonań użyłem narzędzia JMH. Testy prezentowały się w następujący sposób:
@Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @Fork(1) @State(Scope.Benchmark) public class MyBenchmark { @Benchmark public void withoutStacktrace() { try { throw new ExampleException( "CodeCouple" ); } catch (ExampleException e) { } } @Benchmark public void withStacktrace() { try { throw new SimpleException( "CodeCouple" ); } catch (SimpleException e) { } } }
Natomiast nas interesują same wyniki:
[table “” not found /]Na podstawie tych wyników można określić, iż wywołanie wyjątku, który jest Stackless jest około 90 razy szybsze.
Warto wspomnieć że twórcy JIT’a (kompilator wbudowany w JVM), przewidzieli tą optymalizację (jest włączona domyślnie) i w przypadku bardzo częstego występowania danego wyjątku niweluje ona stos wywołań. Opcje tą można wyłączyć wykorzystując flagę -XX:-OmitStackTraceInFastThrow
. Na czas testów flaga ta została wyłączona.
The compiler in the server VM now provides correct stack backtraces for all “cold” built-in exceptions. For performance purposes, when such an exception is thrown a few times, the method may be recompiled. After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. To disable completely the use of preallocated exceptions, use this new flag:
-XX:-OmitStackTraceInFastThrow
.