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] Benchmark,Mode,Cnt,Score,Error,Units MyBenchmark.withStacktrace,avgt,10,1.612,0.044,us/op MyBenchmark.withoutStacktrace,avgt,10,0.018,0.001,us/op [/table]
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.
