#1 Java Performance – Stackless Exceptions

java-logo

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:

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

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.

  • Dzięki za ten wpis, mimo że krótki to dowiedziałem się czegoś nowego po X latach kodowania w Javie :)

    • CodeCouple.pl

      Cześć Tomku, mam podobnie, kiedy już wydaje mi się że nic mnie nie zaskoczy czasem pojawia się coś co daje dużą wartość dodaną. P.S świetna robota z JVM Bloggers 😉

  • Koziolek666

    Pytanie, czy to jest rzeczywiście poprawa wydajności? Oczywiście aplikacja działa szybciej, ale tracimy dużą ilość informacji w przypadku wystąpienia błędu.
    W przypadku błędów pochodzących z JSE możemy przyjąć, że ma to sens, ale czy błąd biznesowy nie powinien nieść ze sobą większej ilości informacji?

  • Pamiętajmy też o przełączniku -XX:MaxJavaStackTraceDepth, którym można szukać kompromisu pomiędzy wydajnością a ilością informacji.

    • Ciekawe jak tutaj będzie z szybkością…