Final – klasy i metody 0 (0)

Czy oznaczenie klasy jako finalną może skutkować optymalizacjami? Jakkolwiek słowa kluczowego final powinniśmy używać przede wszystkim dla zabiegów projektowych, pewne podejrzenia o optymalizacje zdają się być uzasadnione…

Metody finalne są łatwiej inline’owalne

Pierwszą potencjalną optymalizacją jest prostsze wywoływanie metod finalnych. Prosty przykład:

public class Super {
    public void method() { (...) }
}

public class Sub extends Super {
    public void final method() { (...) }
}

public class InvokerClass {
    private Sub sub;
    void invokingMethod() {
        sub.method();
    }
}

Normalnie każde wywołanie metody publicznej modelowane jest instrukcją kodu bajtowego invokevirtual. Ta instrukcja odpowiada za wywołanie – nomen omen – metod wirtualnych. Oznacza to, że przeważnie musimy szukać, czy istnieje jakiś podtyp, który daną metodę nadpisuje, aby ją wykonać.

Jednak w przypadku klasy/metody finalnej wiemy, że żadnej podklasy nadpisującej tej metody nie ma. Zatem w wyżej ukazanym przypadku można by się pokusić o zamianę tej instrukcji na invokespecial, w której wykonujemy określoną metodę. Wówczas uniknęlibyśmy sprawdzania typu.

Mimo wszystko, nie zastosowano tej optymalizacji.

Po pierwsze mogłaby zaistnieć taka sytuacja, w której kompilujemy źródła z classpath posiadającym jakąś bibliotekę w jednej wersji. Następnie w runtime‚ie używamy drugiej wersji.
Załóżmy, że w pierwszej wersji był final, gdy w drugiej nie tylko nie ma final, lecz istnieje również podklasa. Gdybyśmy wówczas zastosowali tę optymalizację, wówczas w przypadku podklasy wykonywalibyśmy nie tę metodę, co prowadziłoby do błędów. Brak podmiany invokevirtual na invokespecial nas chroni od takiego błędu.

Po drugie nie użyto tej optymalizacji, ponieważ… nie. Po prostu tak napisano w specyfikacji i tyle 😉
„Changing a method that is declared final to no longer be declared final does not break compatibility with pre-existing binaries.” – tak twierdzi Java Language Specification.

JIT to the rescue

W trybie interpretowanym nie udało się znaleźć optymalizacji. Może jednak choć JIT jakoś wykorzysta tego finala do na klasie/metodzie do optymalizacji.

Spójrzmy na następujące przypadki (podobne do poprzednich):

public class Super {
    public void method() { (...) }
}

public class Final extends Super {
    public void final method() { (...) }
}

public class NonFinal extends Super {
    public void method() { (...) }
}

public class BenchmarkClass {
    private Super super = new Super();
    private Final final = new Final();
    private NonFinal nonfinal = new NonFinal();

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    void benchmarkSuper() {
        super.method();
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    void benchmarkFinal() {
        final.method();
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    void benchmarkNonfinal() {
        nonfinal.method();
    }
}

W pierwszym przypadku mamy do czynienia z nadklasą. W przypadku metody skompilowanej przez C2, możemy się domyślać, jakiego typu będzie wartość (na podstawie profilowania wywołania metody), jednak nie mamy żadnej pewności, czy typ wartości pola nie zostanie kiedyś zmieniony na jakąś z podklas (Final lub NonFinal). To też jest widoczne w kodzie maszynowym wygenerowanym przez C2.

W przypadku pola typu Final w runtimie jesteśmy pewni, że nie będzie żadnej podklasy, więc jedynym możliwym typem jest właśnie Final. Zatem możemy zaoszczędzić na odczycie z pamięci samego typu, co widać w listingu kodu maszynowego.

Jeśli jednak spojrzymy, że na listing NonFinal, to zauważymy, że jest on prawie identyczny z listingiem Final. Również w nim jest założenie, że obiekt w polu nonFinal będzie właśnie typu NonFinal. Skąd to założenie?

Otóż OpenJDK przy każdym ładowaniu klasy pielęgnuje informacje o hierarchii klas – jaka klasie klasy są liśćmi w hierarchii klas, jakie są nadklasami itd. Taka analiza hierarchii klas nazywa się Class Hierarchy Analysis (CHA) i jest dość starym mechanizmem w Javie.Jeśli dana klasa jest liściem w hierarchii klas, to zakładamy, że nie ma podklas.

Wówczas po kompilacji C2 – podobnie jak przy final – mamy jeden odczyt pamięci mniej.

Jeśliby po kompilacji C2 okazało się, że jest ładowana jakaś nowa podklasa NonFinal, wtedy trzeba unieważnić skompilowane fragmenty kodu maszynowego i wrócić w nich do trybu interpretowanego.

Taka refleksja tylko szybsza… 0 (0)

„Refleksja jest wolna…”

Ile razy o tym słyszeliśmy… „Nie używaj refleksji, bo to mało wydajne, lepiej użyć bezpośrednio metody, jeśli możesz”.

Wiecie, że Jackson korzysta z refleksji na setterach do ustawiania wartości pól? A jak już wspomniałem „refleksja jest wolna”… Może dałoby się ją zamienić na coś innego?

Cel

Naszym celem będzie wykonanie nieznanego nam settera w nieznanym obiekcie w sposób najbardziej wydajny. Warto na początek stwierdzić, co będzie naszym celem, do którego będziemy dążyć.

Załóżmy, że mamy taki zwykły obiekt, w którym chcemy ustawić 4 pola różnych typów: int, long, Object, String. Każde pole posiada setter, który nic nie zwraca (void). Załóżmy, że nie chcemy się bawić Garbage Collection, więc będziemy używać wciąż jednego obiektu, któremu będziemy ustawiać wciąż te same pola jednego obiektu.

Zatem najszybszy sposób ustawiania to będzie proste wywołanie setterów:

    @Benchmark
    public Pojo directSetters(PojoHolder pojoHolder) {
        Pojo pojo = pojoHolder.pojo;
        pojo.setI1(i1);
        pojo.setL1(l1);
        pojo.setO1(o1);
        pojo.setS1(s1);
        return pojo;
    }

Wynik na moim laptopie: 7,993 ± 0,182 ns/op.

Jak już wspomniałem, potencjalnie największe zło to refleksja:

    @Benchmark
    public Pojo reflectionSetters(PojoHolder pojoHolder) throws InvocationTargetException, IllegalAccessException {
        Pojo pojo = pojoHolder.pojo;
        methods[0].invoke(pojo, i1);
        methods[1].invoke(pojo, l1);
        methods[2].invoke(pojo, o1);
        methods[3].invoke(pojo, s1);
        return pojo;
    }

Wynik na moim laptopie: 60,494 ± 4,026 ns/op. Czyli 7 razy więcej… To dość dużo… Dlaczego tak dużo? Przyczyn jest wiele:

  • przy każdym wywołaniu Method.invoke:
    • sprawdzamy, czy dostępy się zgadzają… (lub czy setAccessable było wywołane),
    • sprawdzamy, czy obiekt na którym wykonujemy operację jest odpowiedniego typu,
    • czy ilość parametrów się zgadza i czy są odpowiednich typów,
    • jeśli parametry są typu prymitywnego, to trzeba wykonać autoboxing,
  • gdzieś wewnątrz wywołania Method.invoke jest wywoływana metoda natywna – plotki głoszą, że to jest powolne…
  • sama treść metody wywoływanej jest za każdym razem zagadką – takiej metody nie da się zinline‚ować treści tej metody do nadrzędnych metod. Wywoływana metoda nie jest rozumiana przez JITa.
    Jedynie można wykonać skok do skompilowanej przez C2 treści metody…

Gdyby tylko istniała taka lepsza wersja refleksji…

Generalnie refleksja się sprawdza, do czasu, gdy musimy przeiterować po klasie, żeby dowiedzieć się, jakie pola w niej istnieją. W założeniu nie miała być podstawą działania frameworków…

Postanowiono jednak stworzyć mechanizm, który jest jest bardziej odpowiedni dla opisu oraz wywoływania metod. Dzięki temu do Javy 7 wprowadzono MethodHandle. Sprawdźmy zatem jak działa.

    private static MethodHandles.Lookup lookup = MethodHandles.lookup();
    (...)
        methodHandles[0] = lookup.unreflect(methods[0]);
        methodHandles[1] = lookup.unreflect(methods[1]);
        methodHandles[2] = lookup.unreflect(methods[2]);
        methodHandles[3] = lookup.unreflect(methods[3]);

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public Pojo methodHandles(PojoHolder pojoHolder) throws Throwable {
        Pojo pojo = pojoHolder.pojo;
        methodHandles[0].invokeExact(pojo, i1);
        methodHandles[1].invokeExact(pojo, l1);
        methodHandles[2].invokeExact(pojo, o1);
        methodHandles[3].invokeExact(pojo, s1);;
        return pojo;
    }

Wynik na moim laptopie: 33,540 ± 0,410 ns/op.Jest lepiej, choć szału nie ma… 4x wolniejsze niż bezpośrednie wywołanie, ale 2 razy szybsze niż refleksja.

Dlaczego to działa szybciej? Przede wszystkim sprawdzenie dostępów odbywa się na etapie tworzenia MethodHandle (do tego służy wspomniane Lookup).

Czy można jeszcze bardziej przyspieszyć wywoływanie setterów?

W poście o Lambdach wspomniałem o LambdaMetafactory. Pozwala on wygenerować implementację danego interfejsu podając uchwyt do metody, którą chcemy wywołać. W przypadku settera ustawiającego pole nieprymitywne można by użyć istniejącego interfejsu java.util.function.BiConsumer, gdzie pierwszym parametrem jest obiekt, któremu ustawiamy pole, a drugim – wartość, którą ustawiamy. Dla typów prymitywnych należałoby użyć dedytkowanych analogicznych interfejsów, tzn. dla pól typu intjava.util.function.ObjIntConsumer, a pól typu longjava.util.function.ObjLongConsumer.
I tak dla każdego pola wygenerowalibyśmy implementację odpowiedniego interfejsu, który wywołuje setter na podanym obiekcie.

Jak widać stworzenie takich implementacji już nie jest tak trywialne jak method handle, ale może warto ze względu na performance się o to pokusić…

    private static ObjIntConsumer getIntSetter(MethodHandle methodHandle) throws Throwable {
        final Class functionKlaz = ObjIntConsumer.class;
        Object o = getSetter(methodHandle, functionKlaz);
        return (ObjIntConsumer) o;
    }

    private static ObjLongConsumer getLongSetter(MethodHandle methodHandle) throws Throwable {
        final Class functionKlaz = ObjLongConsumer.class;
        Object o = getSetter(methodHandle, functionKlaz);
        return (ObjLongConsumer) o;
    }

    private static BiConsumer getObjectSetter(MethodHandle methodHandle) throws Throwable {
        final Class functionKlaz = BiConsumer.class;
        Object o = getSetter(methodHandle, functionKlaz);
        return (BiConsumer) o;
    }

    private static Object getSetter(MethodHandle methodHandle, Class functionKlaz) throws Throwable {
        final String functionName = "accept";
        final Class functionReturn = void.class;
        Class aClass = !methodHandle.type().parameterType(1).isPrimitive()
                ? Object.class
                : methodHandle.type().parameterType(1);
        final Class[] functionParams = new Class[] { Object.class,
                aClass};

        final MethodType factoryMethodType = MethodType
                .methodType(functionKlaz);
        final MethodType functionMethodType = MethodType.methodType(
                functionReturn, functionParams);

        final CallSite setterFactory = LambdaMetafactory.metafactory( //
                lookup, // Represents a lookup context.
                functionName, // The name of the method to implement.
                factoryMethodType, // Signature of the factory method.
                functionMethodType, // Signature of function implementation.
                methodHandle, // Function method implementation.
                methodHandle.type() // Function method type signature.
        );

        final MethodHandle setterInvoker = setterFactory.getTarget();
        return setterInvoker.invoke();
    }
    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public Pojo generatedSetter(PojoHolder pojoHolder) throws Throwable {
        Pojo pojo = pojoHolder.pojo;
        ((ObjIntConsumer)setters[0]).accept(pojo, i1);
        ((ObjLongConsumer)setters[1]).accept(pojo, l1);
        ((BiConsumer)setters[2]).accept(pojo, o1);
        ((BiConsumer)setters[3]).accept(pojo, s1);
        return pojo;
    }

Wynik na moim laptopie: 10,839 ± 0,077 ns/op.
Narzut na takie wywoływanie setterów, to zaledwie 0.3x, zatem w porównaniu zarówno do refleksji jak i method handle jest to doskonały wynik.

Wywołujemy jednak różne interfejsy, co może być nieco problematyczne… Chcielibyśmy wywoływać jeden interfejs, żeby się przypadkami skrajnymi nie zajmować… Dlatego warto też sprawdzić jakie będą rezultaty dla jednego interfejsu – BiConsumer.

Tutaj muszę zrobić pewną dygresję. Byłoby naiwnością twierdzenie, że jako jedyny w internetach zgłębiam temat „wydajnej refleksji”. Istnieje niejeden artykuł na ten temat.
Przy szukaniu rozwiązania problemu unifikacji wywołań do wywołań interfejsu BiConsumer natrafiłem na ten artykuł, który pozwolił na proste rozwiązanie problemu castowania typów prymitywnych na Obiekt. Jest to zastosowanie zastosowanie zwykłej lambdy.

            biConsumerSetters[0] = (a, b) -> ((ObjIntConsumer) setters[0]).accept(a, (int) b);
            biConsumerSetters[1] = (a, b) -> ((ObjLongConsumer) setters[1]).accept(a, (long) b);
            biConsumerSetters[2] = getBiConsumerObjectSetter(lookup.unreflect(methods[2]));
            biConsumerSetters[3] = getBiConsumerObjectSetter(lookup.unreflect(methods[3]));
    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public Pojo biConsumerOnly(PojoHolder pojoHolder) throws Throwable {
        Pojo pojo = pojoHolder.pojo;
        biConsumerSetters[0].accept(pojo, i1);
        biConsumerSetters[1].accept(pojo, l1);
        biConsumerSetters[2].accept(pojo, o1);
        biConsumerSetters[3].accept(pojo, s1);
        return pojo;
    }

Jak można się spodziewać taka warstwa pośrednia nieco spowalnia. Jednak wynik jest wciąż zadawalający – 18,037 ± 0,132 ns/op.

To be continued…

Wstępne rozpoznanie możliwości zamiany refleksji jest zrobione. Jednak przeprowadzone badania były w dosyć „sterylnym” środowisku – wszystkie settery były wywoływane „po indeksie”. Można się spodziewać gorszych wyników w przypadku wywołań setterów „w pętli”.

Dodatkowo, zastosowanie tych metod na kodzie biblioteki ma znacznie szerszy kontekst, który może być trudniejszy do analizy dla C2. Może się też okazać, że refleksja jest tak małą częścią całej deserializacji, że refleksja jest „wystarczająco dobra”.

Zanim jednak opiszę swoje doświadczenia musi trochę czasu upłynąć. Aktualnie muszę skupić się przygotowaniu do prezentacji na 4developers w Poznaniu.

Zatem cierpliwości 😉

// Edit: Pojawił się wpis z zastosowaniem treści tego artykułu dla Jacksona

Jak wydajnie rzucać wyjątki? 0 (0)

Cóż… Najlepiej nie rzucać 😉

Generalnie mógłbym podlinkować tylko Blog Alexey’a Shipileva i zakończyć wpis… Alexey dokładnie zbadał temat wydajności wyjątków.
Jednak dla osób wolących artykuły w języku polskim również i ja pokrótce temat opiszę.

Na samym końcu dla odmiany napiszę coś życiowego 😉

Uwaga! Dla odmiany w tym wpisie nie będzie listingu Bytecode’u 😛

Ile kosztuje nas rzut wyjątkiem?

Na początek warto sprawdzić, ile kosztuje w ogóle rzucenie wyjątkiem.
Stworzyłem zatem benchmark (oczywiście używając frameworku JMH), który w bloku try-catch rzuca lub nie rzuca wyjątkiem. Sprawdziłem również, czy jakiś wpływ ma to, jaki wyjątek łapiemy.

Trzeba jednak wziąć pod uwagę, że taki kod w którym w jednej metodzie jest rzucany wyjątek, a ramkę niżej (wyżej?) go łapiemy byłby skompilowany przez C2 i z wysokim prawdopodobieństwem zinline’owany. To by doprowadziło do sytuacji, w której ten wyjątek nie byłby w kodzie maszynowym explicite rzucany – byłaby skompilowana wydajna symulacja rzucania wyjątku. Taka sytuacja nas jednak nie interesuje – wszak szukamy kosztu rzucenia wyjątku, a nie kosztu symulacji rzucania wyjątku.
Aby uniemożliwić inline’owanie, należało dodać dodatkową adnotacją @CompilerControl(CompilerControl.Mode.DONT_INLINE) (udostępnioną przez JMH – normalnie tę akcję da się wymusić parametrem wywołania java, co jest jednak mało wygodne).
Nie chcemy również badać, jak długo tworzony jest obiekt wyjątku, dlatego za każdym razem rzucamy tym samym wyjątkiem, który jest umieszczony w polu statycznym finalnym.

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public int throwBenchmark() {
        try {
            throwRuntimeException();
        } catch (RuntimeException e) {
            return 5;
        }
        return 1;
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public int throwThrowableBenchmark() {
        try {
            throwRuntimeException();
        } catch (Throwable e) {
            return 5;
        }
        return 1;
    }

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    private void throwRuntimeException() {
        throw RUNTIME_EXCEPTION;
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public int withoutthrowBenchmark() {
        try {
            dontThrowRuntimeException();
        } catch (RuntimeException e) {
            return 5;
        }
        return 1;
    }

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    private void dontThrowRuntimeException() {

    }

Wyniki tego benchmarku są następujące (dla OpenJDK w wersji 11):

Benchmark                                     Mode  Cnt    Score   Error  Units
SimpleThrowBenchmark.throwBenchmark           avgt   10  144,234 ± 1,407  ns/op
SimpleThrowBenchmark.throwThrowableBenchmark  avgt   10  146,276 ± 1,083  ns/op
SimpleThrowBenchmark.withoutthrowBenchmark    avgt   10    5,684 ± 0,055  ns/op

Czyli takie rzucenie wyjątku spowalnia nam tę bardzo prostą metodę jakieś 30 razy…
Oczywiście ta metoda i tak nic nie robi, jednak różnica w czasie wykonania jest bardzo duża…

Zarzucenie wyjątkiem głęboko w stacktrace

Warto również sprawdzić sytuację, w której wyjątek musi przewędrować przez wiele ramek zanim trafi na odpowiedni blok try-catch. To zachowanie jest sprawdzane przez kolejny benchmark. Polega on na rekurencyjnym odliczaniu do 10, 50 lub 100, aby ostatecznie rzucić wyjątkiem (albo go nie rzucać i zakończyć rekurencję).

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public int throw100Benchmark() {
        try {
            throwRuntimeException(100);
        } catch (RuntimeException e) {
            return 5;
        }
        return 1;
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public int throw50Benchmark() {
        try {
            throwRuntimeException(50);
        } catch (RuntimeException e) {
            return 5;
        }
        return 1;
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public int throw10Benchmark() {
        try {
            throwRuntimeException(10);
        } catch (RuntimeException e) {
            return 5;
        }
        return 1;
    }

    private void throwRuntimeException(int i) {
        if (i == 0) {
            throwThrow();
            return;
        } else {
            throwRuntimeException(i - 1);
        }
    }

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    private void throwThrow() {
        throw RUNTIME_EXCEPTION;
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public int without100throwBenchmark() {
        try {
            dontThrowRuntimeException(100);
        } catch (RuntimeException e) {
            return 5;
        }
        return 1;
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public int without50throwBenchmark() {
        try {
            dontThrowRuntimeException(50);
        } catch (RuntimeException e) {
            return 5;
        }
        return 1;
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public int without10throwBenchmark() {
        try {
            dontThrowRuntimeException(10);
        } catch (RuntimeException e) {
            return 5;
        }
        return 1;
    }

    private void dontThrowRuntimeException(int i) {
        if (i == 0) {
            dontThrowThrow(i);
        } else {
            dontThrowRuntimeException(i - 1);
        }
    }

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    private void dontThrowThrow(int i) {
        i++;
    }

Wyniki benchmarku są następujące:

Benchmark                                          Mode  Cnt      Score      Error  Units
StacktraceThrowBenchmark.throw100Benchmark         avgt   10  10047,087 ± 3463,884  ns/op
StacktraceThrowBenchmark.throw10Benchmark          avgt   10    922,528 ±   15,990  ns/op
StacktraceThrowBenchmark.throw50Benchmark          avgt   10   3846,247 ±   64,145  ns/op
StacktraceThrowBenchmark.without100throwBenchmark  avgt   10    152,059 ±    1,656  ns/op
StacktraceThrowBenchmark.without10throwBenchmark   avgt   10     17,163 ±    0,184  ns/op
StacktraceThrowBenchmark.without50throwBenchmark   avgt   10     92,268 ±   32,105  ns/op

Jak widać rzucenie wyjątkiem głęboko w stacktrace działa (w miarę) proporcjonalnie dłużej do odległości do najbliższego bloku try-catch. Najprostszy wniosek z tego jest taki – jeśli już musimy rzucać wyjątek, to nie daleko od łapiącego go bloku try-catch 😉
Mimo wszystko to nie są jakieś kosmiczne wartości – rzucenie wyjątku na głębokości 100 ramek to „koszt” 10 000 nanosekund, czyli 10 mikrosekund, czyli 0,01 milisekundy. Nie ma dramatu…

Skąd te różnice?

Algorytm działania rzucania wyjątku jest następujący (za grupą Hotspot projektu OpenJDK).
1. Sprawdzamy, czy w danej ramce na stosie istnieje odpowiedni blok try-catch. Informacja o tym znajduje się gdzieś na Off-Heapie.
2. Wśród znalezionych bloków try-catch sprawdzamy, czy któryś pasuje do naszego miejsca, gdzie wyjątek został rzucony/propagowany w dół.Jeśli jest takie miejsce, trzeba sprawdzić, czy blok łapie odpowiedni typ wyjątku.

3. Jeśli nie znaleźliśmy w tej ramce obsługi błędu, wówczas zdejmujemy ramkę ze stackframe i wracamy do punktu 1.

Niby nie brzmi to skomplikowanie, a jednak z 35 instrukcji procesora robi się 835, gdy rzucimy wyjątkiem (a przynajmniej tak twierdzi .addProfiler(LinuxPerfNormProfiler.class)). I to dla C2. W trybie interpretowanym ten stosunek to 550 do 2090 dla wykonania pojedynczej metody.
Jakby się dobrze zastanowić, to można by próbować jakoś optymalizować to rzucanie wyjątków. Można by utrzymywać jakąś tablicę, która dla danego typu wyjątku przechowuje adres skoku do którego ma trafić w przypadku rzucenia wyjątku… Może miałoby to sens, gdyby nie to, że wystąpienie wyjątku powinno być… wyjątkiem 😉 A utrzymywanie takiej tablicy tylko zajmowałoby czas i bezsensownie psuło wydajność…

A gdzie jest to coś życiowego?

Dość powszechną (i słuszną) praktyką jest walidacja parametrów wywołania. Często przy okazji walidacji można spotkać się z rzucaniem wyjątków jak choćby javax.validation.ValidationException. Jeśli tych wyjątków występowałoby dużo mogłoby w jakiś sposób ograniczyć wydajność. Być może warto by to zbadać.

Z tą motywacją stworzyłem prosty Restowy serwis w Spring Boot. A w nim 3 endpointy:
1. Rzucenie wyjątku na głębokości 100 ramek i „łapanie” z @ExceptionHandler,
2. Zagłębienie się na 100 ramek, powrót, a następnie rzucenie wyjątku na głębokości 1 ramki i „łapanie” z @ExceptionHandler
3. Zagłębienie się na 100 ramek, powrót i zwrócenie rezultatu walidacji w ResponseEntity.badRequest()

@SpringBootApplication
@RestController
public class ThrowExceptionSpringApplication {

    private static final String SORRY_NOT_VALID = "Sorry, not valid";

    @GetMapping("/response/deep/exception")
    public String throwDeepException() {
        Supplier supplier = () -> {
            throw new ValidationException(SORRY_NOT_VALID);
        };
        return deep(100, supplier);
    }

    @GetMapping("/response/shallow/exception")
    public String throwShallowException() {
        deep(100, () -> SORRY_NOT_VALID);
        throw new ValidationException(SORRY_NOT_VALID);
    }

    @GetMapping("/response/no-exception")
    public ResponseEntity dontThrowDeepException() {
        return ResponseEntity.badRequest()
                .body(deep(100, () -> SORRY_NOT_VALID));
    }

    private String deep(int i, Supplier producer) {
        if (i != 0) {
            return deep(i-1, producer);
        }
        return producer.get();
    }

    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ValidationException.class)
    public String handleException(ValidationException e) {
        return SORRY_NOT_VALID;
    }

    public static void main(String[] args) {
        SpringApplication.run(ThrowExceptionSpringApplication.class);
    }
}

Następnie wszystkie endpointy benchmarkowałem z użyciem komendy ab -n 10000 -c 100 . Sprawdzałem w jakim najkrótszym czasie skończy się test (sprawdzałem wielokrotnie po uprzednim rozgrzaniu serwisu). Rezultaty były następujące:

1.Time taken for tests:   1.573 seconds
2.Time taken for tests:   1.515 seconds
3.Time taken for tests:   1.365 seconds

Zatem rzucenie wyjątku na głębokości 100 ramek jest o około 4% mniej wydajne, niż rzucenie na głębokości 1 ramki. Niby bez szału, ale jeżeli jedyną różnicą jest tylko miejsce wykonania throw to w sumie jest to różnica…
Jeszcze większą różnicę widać, jeśli zamiast rzucać wyjątek po prostu zwrócimy ResponseEntity.badRequest() – tutaj różnica to około 13%.

Podsumowanie

Wracając do myśli z początku tekstu – najlepiej nie rzucać wyjątków. Takie podejście jest stosowane chociażby w Scali, gdzie zamiast rzucania wyjątku można zwracać Either.

Drugą myślą jest to, że wyjątki powinny być rzucane „wyjątkowo” 😉 Exception Driven Development raczej nie jest dobrym pomysłem.

Ostatnią myślą, którą chciałem się to ponowne polecenie Artykułu Alexey’a Shipileva. Można tam przeczytać, to o czym nawet nie wspomniałem, czyli:

  • Inline’owanie na odpowiednim poziomie może polepszyć wydajność rzucania wyjątków,
  • Tworzenie Stacktrace’a jest wolne i można z niego zrezygnować,
  • Jak rzadkie rzucanie wyjątków jest wg Alexey’a dostatecznie rzadkie

Interfejs w JVMie – niskopoziomowo 0 (0)

Standardowym pytaniem rekrutacyjnym jest: czym się różni interfejs od klasy abstrakcyjnej w Javie. O ile większość odpowiedzi zaczyna się od strony specyfikacji, o tyle w tym wpisie podejdę od strony implementacji.

Bytecode

Standardowo zaczniemy od spojrzenia w bytecode. Najpierw podejrzymy co siedzi w abstrakcyjnej klasie:

public abstract class AbstractClass {
    public abstract int doSth(int a);
    public int defaultDoSth(int a) {
        return doSth(a);
    }
}

Następnie podejrzymy analogiczny do niej interfejs:

public interface Interface {
    int doSth(int a);
    default int defaultDoSth(int a) {
        return doSth(a);
    }
}

Porównując obydwa twory zaczniemy od stwierdzenia banału, że po skompilowaniu otrzymujemy pliki z rozszerzeniem .class Gdzie zatem jest trzymana informacja o tym, że jedno jest interfejsem a drugie klasą? Na to pytanie odpowiada listing wywołania na tych plikachjavap -v -p.

A w nim zauważymy, że klasa abstrakcyjna posiada flagi flags: (0x0421) ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT, gdy interfejs posiada flagi flags: (0x0601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT.
Zatem o byciu interfejsem świadczy dodatkowa flaga ACC_INTERFACE.
Flaga ACC_SUPERw klasie abstrakcyjnej jest aktualnie nadmiarowa i istnieje tylko w celu wstecznej kompatybilności.

Drugą różnicą, którą zauważyć porównując bytecode obu plików .class jest obecność konstruktora w klasie abstrakcyjnej. Jak wiemy, jeśli jawnie nie stworzymy konstruktora w kodzie źródłowym, kompilator wygeneruje nam domyślny konstruktor danej klasy.

Trzecią różnicą dość oczywistą, aczkolwiek niewidoczną w tym porównaniu jest to, że klasa w przeciwieństwie do interfejsu może posiadać pola.

Ostatnią różnicę, którą dokładniej opiszę jest różnica w implementacji metody defaultDoSth(int).

Dla klasy abstrakcyjnej bytecode wygląda następująco:

 public int defaultDoSth(int);
    descriptor: (I)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: invokevirtual #2                  // Method doSth:(I)I
         5: ireturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Ldev/jgardo/jvm/miscellaneous/interfaces/AbstractClass;
            0       6     1     a   I
}

Domyślna metoda interfejsu Interface defaultDoSth(int) zdekomilowana wygląda tak:

  public int defaultDoSth(int);
    descriptor: (I)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: invokeinterface #1,  2            // InterfaceMethod doSth:(I)I
         7: ireturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Ldev/jgardo/jvm/miscellaneous/interfaces/Interface;
            0       8     1     a   I
}

W implementacji metody defaultDoSth(int) w przypadku klasy abstrakcyjnej widzimy instrukcję kodu bajtowego invokevirtual, a za nią parametr, w którym jest wskazywana metoda, którą chcemy wywołać.
W przypadku interfejsu zauważymy invokeinterface z dwoma parametrami.
A jakie są różnice między nimi? Czy były konieczne dwa różne kody operacji?

Invoke(…)

Ogólnie mówiąc, wywoływanie metod można wykonać za pomocą jednego z 5 różnych instrukcji kodu bajtowego (w zależności od kontekstu):

  • invokestatic – używamy go za każdym razem, kiedy wywołujemy metodę statyczną, czyli taką, w której nie potrzebujemy przekazywać wskaźnika na obiekt z którego jest ona wywoływana (nie ma przykazywanego w żaden sposób obiektu this).
  • invokespecial – tej instrukcji używamy, kiedy chcemy wywołać konkretną metodę danego obiektu. Nie zastanawiamy się, czy ta metoda jest nadpisana w klasie dziedziczącej, lecz wskazujemy dokładną metodę. Przykładem jest wywoływanie metody prywatnej, metody z nadklasy lub konstruktora. Przekazujemy niejawnie this jako pierwszy parametr metody.
  • invokedynamic – ta instrukcja jest raczej niedostępna w Javie i rzadko spotkana (oprócz lambd). Kluczowe jest w niej to, że generalnie na etapie kompilacji nie wiemy, co chcemy wywołać 😉 Na etapie kompilacji jedynie wskazujemy, kto może wiedzieć, co chcemy wywołać.
    Trochę poruszam temat we wpisie o lambdach.
  • invokevirtual – w tym przypadku mamy do czynienia z wywołaniami metod publicznych, chronionych i prywatnych w ramach pakiety (generalnie nieprywatnych). Na etapie kompilacji przeważnie nie wiemy, czy wywołujemy metodę z danej klasy, czy klasy po niej dziedziczącej (ze względu na polimorfizm). W runtime‚ie JVM ewaluuje, jaką metodę wykonać.
    W tej operacji równierz przemycamy this. Argumentem tego kodu bajtowego jest indeks metody, którą wywołujemy.
  • invokeinteface – bardzo podobna instrukcja do invokevirtual z tą różnicą, że nie znamy nawet klasy, której metodę wywołujemy. W tym przypadku jednak mamy dwa argumenty tego kodu bajtowego – pierwszym jest jawne przekazanie uchwytu („wskaźnika”) do interfejsu, którego metodę chcemy wywołać, a drugim – indeks tej metody w danym interfejsie.

Różnice między invokevirtual a invokeinterface

Jak wywnioskować, którą metodę należy wykonać?
Najprościej byłoby sprawdzić klasę (wskaźnik na klasę jest w każdym obiekcie w jego nagłówku), której metodę chcemy wywołać i poszukać, czy istnieje implementacja poszukiwanej metody. Jeśli nie ma jej w tej klasie, szukamy jej w nadklasie. Jeśli i w niej nie ma, szukamy dalej w zwyż w hierarchii.

Jednak takie podejście jest mało efektywne. Mogłoby się zdarzyć, że wielokrotnie obliczamy to samo wywołując wciąż tę samą metodę. Zatem lepiej by było na etapie ładowania klasy obliczyć od razu, jaką dokładnie metodę trzeba wykonać dla każdej dostępnej metody. Wynik takich obliczeń można składować w metadanych danej klasy.
I właśnie taka tablica, która przechowuje dla każdej metody wskaźnik do kodu, jest nazywana vtable. Zatem za każdym razem, gdy używamy invokevirtual, najpierw sprawdzamy klasę obiektu, następnie odczytujemy z klasy adres do pola z tablicą vtable, a następnie uruchamiamy kod znaleziony pod indeksem danej metody (indeks ten jest podany w argumencie invokevirtual).

Warto tutaj nadmienić, że indeksy metod dla danej hierarchii klas są takie same, tzn. jeśli jakaś klasa miała tablicę vtable, to każda jej podklasa będzie miała wszystkie jej metody z nadklasy w dokładnie tej samej kolejności, a ewentualne dodatkowe metody specyficzne dla podklasy będą znajdować się na końcu tabeli. Zatem jeden argument z indeksem metody wystarczy do precyzyjnego określenia metody, gdyż indeks metody w danej hierarchii klas jest stały.

Nieco inna sytuacja jest w przypadku invokeinterface. Jeśli klasa implementuje interfejs, to metody go implementujące mogą być w vtable w różnej kolejności. Zatem dla interfejsu potrzebujemy analogicznej struktury zwanej itable. Kolejność metod jest specyficzna dla danego interfejsu.
„Niestety” klasy mogą implementować wiele interfejsów, dlatego każda klasa posiada tablicę tablic itable. Skoro jest wiele itable, to trzeba je rozróżniać, stąd konieczność przekazywania wraz z kodem invokeinterface argumentu w postaci odniesienia do interfejsu, jak i metody.
Pewną konsekwencją możliwości implementacji wielu interfejsów jest to, że żeby znaleźć odpowiednią itable trzeba przeiterować po tablicy tablic itable, co jest mało wydajne.

Jeśli ten opis jest zbyt mglisty, warto zajrzeć pod ten pomocny w zrozumieniu link.

A to tylko początek

Można na ten temat jeszcze wiele pisać. Póki co jednak zostawiam garść linków omawiających tematy wokół. Dotyczą one przede wszystkim:

  • optymalizacji invokeinterface/invokevirtual. Należy wspomnieć, że znaczna większość interfejsów/klas posiada tylko jedną klasę implementującą lub jedną podklasę. Można się wówczas pokusić o optymalizację i skuteczne inline’owanie. Słowa klucze dla tego tematu to: monomorphic, bimorphic, megamorphic.

    Osobiście polecam wpis Aleksey’a Shipilev’a oraz Richarda Warburtona

  • opisu wywołań wirtualnych/interfejsu prosto ze strony OpenJDK
  • porównania wywołań wirtualnych w różnych językach
  • wydajności obu wywołań oraz o bugu, który nie pozwala inline’ować jedynej implementacji interfejsu

A na razie to by było ode mnie na tyle. Jeśli chcecie sami coś po kombinować z interfejsami źródła moich eksperymentów są na githubie. Dajcie znać w komentarzach, co o tym wszystkim myślicie 😉

Enum w JVM – szczegóły implementacyjne 0 (0)

Kolejnym słowem kluczowym, które chciałbym omówić jest enum. Ta konstrukcja została wprowadzona w Javie 1.5. Może się wydawać, że intuicyjnie wiemy, jak technicznie enum jest zaimplementowany, jednak warto zweryfikować domysły. Być może to słowo kluczowe niesie ze sobą jakieś dodatkowe „magiczne” właściwości, których zwykła klasa nie posiada…

Pierwsze spojrzenie na bytecode

Na początek stwórzmy i skompilujmy prostą klasę:

public enum Enum {
    VAL_1(1),
    VAL_2(2);

    private final int abc;

    Enum(int abc) {
        this.abc = abc;
    }
}

Następnie dekompilujmy ją z użyciem javap -v -p. Dekompilator wyświetla dosyć dużo linii, więc skupię się na tych ciekawszych rzeczach i krótko skomentuję. Cały listing na samym spodzie postu.

public final class dev.jgardo.jvm.miscellaneous.enums.Enum extends java.lang.Enum

Jak widzimy, enum jest w czasie kompilacji do bytecodu zamieniany na „zwykłą” klasę dziedziczącą z java.lang.Enum. Jednak różni się od „zwykłej” klasy flagami:

  flags: (0x4031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM

Obecność flagi ACC_ENUM może potencjalnie odpowiadać za jakieś zachowania, więc wrócimy do niej później.

  public static final dev.jgardo.jvm.miscellaneous.enums.Enum VAL_1;
    descriptor: Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final dev.jgardo.jvm.miscellaneous.enums.Enum VAL_2;
    descriptor: Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

Widzimy również dwa pola statyczne finalne z dodatkową flagą ACC_ENUM.

  private final int abc;
  private static final dev.jgardo.jvm.miscellaneous.enums.Enum[] $VALUES;
  public static dev.jgardo.jvm.miscellaneous.enums.Enum[] values();
  public static dev.jgardo.jvm.miscellaneous.enums.Enum valueOf(java.lang.String);
  private dev.jgardo.jvm.miscellaneous.enums.Enum(int);

W kolejnych liniach widzimy pole, które zadeklarowaliśmy w enumie, a następnie wygenerowane pole statyczne finalne z wszystkimi wartościami enuma o nazwie $VALUES, a po nim kolejne dwie wygenerowane statyczne metody. Pierwsza zwraca wszystkie możliwe wartości, a druga zwraca wartość enuma dla podanego Stringa.
Następnie widzimy zadeklarowany wcześniej konstruktor.

Na samym końcu listingu z javap znajduje się wygenerowana inicjalizacja wartości enumów (pól statycznych finalnych) oraz wypełnienie wartościami wspomnianej wcześniej tablicy $VALUES.

Czyli to zwykła klasa?

Można by się pokusić o stwierdzenie, że tak właściwie to słowo kluczowe enum służy do ograniczenia boilerplate’u poprzez wygenerowanie zwykłej klasy Javowej. Być może równie dobrze taki boilerplate możnaby ograniczyć jakąś adnotacją Lombokową….
Czy więc zatem można by taki enum stworzyć „ręcznie”? Warto spróbować zamienić klasę Enum

public enum Enum {
    VAL_1(1),
    VAL_2(2);

    private final int abc;
    Enum(int abc) {
        this.abc = abc;
    }
}

na odpowiadającą jej implementację wygenerowanej klasy Enuma czyli:

public class Enum extends java.lang.Enum {
    public static final Enum VAL_1 = new Enum("VAL_1", 1, 1);
    public static final Enum VAL_2 = new Enum("VAL_2", 2, 2);

    private final int abc;
    private static final Enum[] $VALUES = new Enum[] {VAL_1, VAL_2 };
    public static Enum[] values() {
        return $VALUES;
    }

    public static Enum valueOf(String name) {
        return valueOf(Enum.class, name);
    }
    Enum(String name, int ordinal, int abc) {
        super(name, ordinal);
        this.abc = abc;
    }
}

Okazuje się, że enum jest uprzywilejowany na kilka sposobów.

1. switch pozwala na używanie enumów w case. Polega to na wywołaniu metody ordinal() enuma, co jest równe liczbie porządkowej wartości danego enuma. Dzięki temu case może dotyczyć już zwykłych intów co jest standardowym mechanizmem (zamiana wartości Enuma na wartości ordinal(), również jest automatyczna i nie widać tego w kodzie, choć w bytecodzie jest to widoczne).
Jeśli chcielibyśmy stworzyć własnoręcznie klasę, wywołanie ordinal() musialo by być jawne, co zmniejsza czytelność kodu.

2. Tworzenie obiektem z użyciem refleksji jest dla enumów zablokowane. Szybki test:

    public static void main(String[] args) throws Exception {
        var constructor = Enum.class.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        var generated = constructor.newInstance("VAL_G", 2, 2);
        System.out.println(generated);
    }

powoduje równie szybki błąd:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:484)
        at dev.jgardo.jvm.miscellaneous.enums.EnumExperiment.main(EnumExperiment.java:18)

Jeśli spojrzymy w implementację (Constructor.newInstance(Constructor.java:484)), to za to rzucenie wyjątku jest uwarunkowane obecnością wspomnianej wcześniej flagi ACC_ENUM dla danej klasy.

3. Instancje enumów można wykorzystywać w adnotacjach, instancje zwykłych klas – nie. Generalnie to jest duża przewaga, a osiągana jest ona znów dzięki fladze ACC_ENUM dla klasy.

4. Enumy są dobrze przystosowane do serializacji obiektów, które je posiadają – po deserializacji otrzymywany jest istniejący enum, a nie jakiś kolejny nowo stworzony enum (a tak by było przy w przypadku zwykłej klasy).

5. W zasadzie na końcu najważniejsze – tego sie normalnie nie da skompilować 😛 Kompilator javac uniemożliwia „ręczne” stworzenie klasy dziedziczącej po java.lang.EnumOświadcza to dosadnie komunikatem przy kompilacji:

Enum.java:3: error: classes cannot directly extend java.lang.Enum

Podsumowanie

Można by w skrócie powiedzieć, że enum niby jest taką zwykła klasą, ale jednak nie 😉 Bez wsparcia ze strony JVMa i kompilatora nie można by go używać w tak elastyczny sposób (w adnotacjach, switchu, serializacji). Z drugiej strony można też powiedzieć, że całość implementacji jest dosyć intuicyjna i przewidywalna i że nie ma tam jakiejś specjalnej „magii”.

Z perspektywy czasu można śmiało powiedzieć, że dodanie osobnego słowa kluczowego było krokiem w dobrą stronę.

I na koniec obiecany cały listing javap -v -p:

Classfile /home/gardziol/repository/jvm-miscellaneous/target/classes/dev/jgardo/jvm/miscellaneous/enums/Enum.class
  Last modified 6 paź 2019; size 1137 bytes
  MD5 checksum 18c950a8da67456a2509b83e2dfe7d36
  Compiled from "Enum.java"
public final class dev.jgardo.jvm.miscellaneous.enums.Enum extends java.lang.Enum
  minor version: 0
  major version: 55
  flags: (0x4031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
  this_class: #4                          // dev/jgardo/jvm/miscellaneous/enums/Enum
  super_class: #13                        // java/lang/Enum
  interfaces: 0, fields: 4, methods: 4, attributes: 2
Constant pool:
   #1 = Fieldref           #4.#40         // dev/jgardo/jvm/miscellaneous/enums/Enum.$VALUES:[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
   #2 = Methodref          #41.#42        // "[Ldev/jgardo/jvm/miscellaneous/enums/Enum;".clone:()Ljava/lang/Object;
   #3 = Class              #20            // "[Ldev/jgardo/jvm/miscellaneous/enums/Enum;"
   #4 = Class              #43            // dev/jgardo/jvm/miscellaneous/enums/Enum
   #5 = Methodref          #13.#44        // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
   #6 = Methodref          #13.#45        // java/lang/Enum."":(Ljava/lang/String;I)V
   #7 = Fieldref           #4.#46         // dev/jgardo/jvm/miscellaneous/enums/Enum.abc:I
   #8 = String             #14            // VAL_1
   #9 = Methodref          #4.#47         // dev/jgardo/jvm/miscellaneous/enums/Enum."":(Ljava/lang/String;II)V
  #10 = Fieldref           #4.#48         // dev/jgardo/jvm/miscellaneous/enums/Enum.VAL_1:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #11 = String             #16            // VAL_2
  #12 = Fieldref           #4.#49         // dev/jgardo/jvm/miscellaneous/enums/Enum.VAL_2:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #13 = Class              #50            // java/lang/Enum
  #14 = Utf8               VAL_1
  #15 = Utf8               Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #16 = Utf8               VAL_2
  #17 = Utf8               abc
  #18 = Utf8               I
  #19 = Utf8               $VALUES
  #20 = Utf8               [Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #21 = Utf8               values
  #22 = Utf8               ()[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #23 = Utf8               Code
  #24 = Utf8               LineNumberTable
  #25 = Utf8               valueOf
  #26 = Utf8               (Ljava/lang/String;)Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #27 = Utf8               LocalVariableTable
  #28 = Utf8               name
  #29 = Utf8               Ljava/lang/String;
  #30 = Utf8
  #31 = Utf8               (Ljava/lang/String;II)V
  #32 = Utf8               this
  #33 = Utf8               Signature
  #34 = Utf8               (I)V
  #35 = Utf8
  #36 = Utf8               ()V
  #37 = Utf8               Ljava/lang/Enum;
  #38 = Utf8               SourceFile
  #39 = Utf8               Enum.java
  #40 = NameAndType        #19:#20        // $VALUES:[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #41 = Class              #20            // "[Ldev/jgardo/jvm/miscellaneous/enums/Enum;"
  #42 = NameAndType        #51:#52        // clone:()Ljava/lang/Object;
  #43 = Utf8               dev/jgardo/jvm/miscellaneous/enums/Enum
  #44 = NameAndType        #25:#53        // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  #45 = NameAndType        #30:#54        // "":(Ljava/lang/String;I)V
  #46 = NameAndType        #17:#18        // abc:I
  #47 = NameAndType        #30:#31        // "":(Ljava/lang/String;II)V
  #48 = NameAndType        #14:#15        // VAL_1:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #49 = NameAndType        #16:#15        // VAL_2:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
  #50 = Utf8               java/lang/Enum
  #51 = Utf8               clone
  #52 = Utf8               ()Ljava/lang/Object;
  #53 = Utf8               (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  #54 = Utf8               (Ljava/lang/String;I)V
{
  public static final dev.jgardo.jvm.miscellaneous.enums.Enum VAL_1;
    descriptor: Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final dev.jgardo.jvm.miscellaneous.enums.Enum VAL_2;
    descriptor: Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  private final int abc;
    descriptor: I
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

  private static final dev.jgardo.jvm.miscellaneous.enums.Enum[] $VALUES;
    descriptor: [Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x101a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC

  public static dev.jgardo.jvm.miscellaneous.enums.Enum[] values();
    descriptor: ()[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field $VALUES:[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
         3: invokevirtual #2                  // Method "[Ldev/jgardo/jvm/miscellaneous/enums/Enum;".clone:()Ljava/lang/Object;
         6: checkcast     #3                  // class "[Ldev/jgardo/jvm/miscellaneous/enums/Enum;"
         9: areturn
      LineNumberTable:
        line 3: 0

  public static dev.jgardo.jvm.miscellaneous.enums.Enum valueOf(java.lang.String);
    descriptor: (Ljava/lang/String;)Ldev/jgardo/jvm/miscellaneous/enums/Enum;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #4                  // class dev/jgardo/jvm/miscellaneous/enums/Enum
         2: aload_0
         3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #4                  // class dev/jgardo/jvm/miscellaneous/enums/Enum
         9: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  name   Ljava/lang/String;

  private dev.jgardo.jvm.miscellaneous.enums.Enum(int);
    descriptor: (Ljava/lang/String;II)V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=3, locals=4, args_size=4
         0: aload_0
         1: aload_1
         2: iload_2
         3: invokespecial #6                  // Method java/lang/Enum."":(Ljava/lang/String;I)V
         6: aload_0
         7: iload_3
         8: putfield      #7                  // Field abc:I
        11: return
      LineNumberTable:
        line 19: 0
        line 20: 6
        line 21: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Ldev/jgardo/jvm/miscellaneous/enums/Enum;
            0      12     3   abc   I
    Signature: #34                          // (I)V

  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=5, locals=0, args_size=0
         0: new           #4                  // class dev/jgardo/jvm/miscellaneous/enums/Enum
         3: dup
         4: ldc           #8                  // String VAL_1
         6: iconst_0
         7: iconst_1
         8: invokespecial #9                  // Method "":(Ljava/lang/String;II)V
        11: putstatic     #10                 // Field VAL_1:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
        14: new           #4                  // class dev/jgardo/jvm/miscellaneous/enums/Enum
        17: dup
        18: ldc           #11                 // String VAL_2
        20: iconst_1
        21: iconst_2
        22: invokespecial #9                  // Method "":(Ljava/lang/String;II)V
        25: putstatic     #12                 // Field VAL_2:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
        28: iconst_2
        29: anewarray     #4                  // class dev/jgardo/jvm/miscellaneous/enums/Enum
        32: dup
        33: iconst_0
        34: getstatic     #10                 // Field VAL_1:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
        37: aastore
        38: dup
        39: iconst_1
        40: getstatic     #12                 // Field VAL_2:Ldev/jgardo/jvm/miscellaneous/enums/Enum;
        43: aastore
        44: putstatic     #1                  // Field $VALUES:[Ldev/jgardo/jvm/miscellaneous/enums/Enum;
        47: return
      LineNumberTable:
        line 4: 0
        line 10: 14
        line 3: 28
}
Signature: #37                          // Ljava/lang/Enum;
SourceFile: "Enum.java"

Czym tak naprawdę jest Lambda w Javie 0 (0)

Jak dobrze pamiętamy, największym osiągnięciem w Javie 8 było wprowadzenie wyrażeń lambda. W tym poście skupię się na tym czym one technicznie są oraz jak one działają.

Pierwsze spostrzeżenia

Jeśli spojrzeć w przeszłość, to przed Javą 8 wyrażenia lambda były emulowane wewnętrznymi klasami anonimowymi. Przykładowo w dla wcześniejszych wersji Javy dla przetwarzania funkcyjnego stworzono klasę Iterables. A w niej zalecanym podejściem było tworzenie anonimowych klas wewnętrznych implementujące interfejsy z jedną metoda. Jakkolwiek było to dość toporne rozwiązanie trzeba było tworzyć dużo zbędnego kodu…

Czy może zatem lambda jest zwykłą wewnętrzna klasa anonimową? Wskazywałby na to również stacktrace zrobiony wewnątrz lambdy w którym widać charakterystyczny dla klas anonimowych znak $, a po nim numer klasy.

public class LambdaExperiment {
    public static void main(String[] args) {
        Function lambda = s -> { throw new RuntimeException(s); };
        lambda.apply("String");
    }
}
Exception in thread "main" java.lang.RuntimeException: String
        at dev.jgardo.jvm.miscellaneous.lambda.LambdaExperiment.lambda$main$0(LambdaExperiment.java:7)
        at dev.jgardo.jvm.miscellaneous.lambda.LambdaExperiment.main(LambdaExperiment.java:8)

I teoria ta miałaby szanse, gdyby nie to, że wśród skompilowanych plików .class nie ma żadnych dodatkowych klas, a każda klasa anonimowa tworzy osobny plik o sufiksie $

Gdzie zatem jest ta lambda?

Cóż… trzeba zajrzeć do kodu bajtowego, może tam coś znajdziemy…

  Last modified 27 wrz 2019; size 1461 bytes
  MD5 checksum f165faff2e94d85a93129e64e8cd7403
  Compiled from "LambdaExperiment.java"
public class dev.jgardo.jvm.miscellaneous.lambda.LambdaExperiment
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #6                          // dev/jgardo/jvm/miscellaneous/lambda/LambdaExperiment
  super_class: #7                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 3, attributes: 3
Constant pool:
   #1 = Methodref          #7.#29         // java/lang/Object."":()V
   #2 = InvokeDynamic      #0:#35         // #0:apply:()Ljava/util/function/Function;
   #3 = String             #36            // String
   #4 = InterfaceMethodref #37.#38        // java/util/function/Function.apply:(Ljava/lang/Object;)Ljava/lang/Object;
   #5 = Methodref          #39.#40        // java/lang/String.toUpperCase:()Ljava/lang/String;
   #6 = Class              #41            // dev/jgardo/jvm/miscellaneous/lambda/LambdaExperiment
   #7 = Class              #42            // java/lang/Object
   #8 = Utf8               
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Ldev/jgardo/jvm/miscellaneous/lambda/LambdaExperiment;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               lambda
  #20 = Utf8               Ljava/util/function/Function;
  #21 = Utf8               LocalVariableTypeTable
  #22 = Utf8               Ljava/util/function/Function;
  #23 = Utf8               lambda$main$0
  #24 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #25 = Utf8               s
  #26 = Utf8               Ljava/lang/String;
  #27 = Utf8               SourceFile
  #28 = Utf8               LambdaExperiment.java
  #29 = NameAndType        #8:#9          // "":()V
  #30 = Utf8               BootstrapMethods
  #31 = MethodHandle       6:#43          // REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #32 = MethodType         #44            //  (Ljava/lang/Object;)Ljava/lang/Object;
  #33 = MethodHandle       6:#45          // REF_invokeStatic dev/jgardo/jvm/miscellaneous/lambda/LambdaExperiment.lambda$main$0:(Ljava/lang/String;)Ljava/lang/String;
  #34 = MethodType         #24            //  (Ljava/lang/String;)Ljava/lang/String;
  #35 = NameAndType        #46:#47        // apply:()Ljava/util/function/Function;
  #36 = Utf8               String
  #37 = Class              #48            // java/util/function/Function
  #38 = NameAndType        #46:#44        // apply:(Ljava/lang/Object;)Ljava/lang/Object;
  #39 = Class              #49            // java/lang/String
  #40 = NameAndType        #50:#51        // toUpperCase:()Ljava/lang/String;
  #41 = Utf8               dev/jgardo/jvm/miscellaneous/lambda/LambdaExperiment
  #42 = Utf8               java/lang/Object
  #43 = Methodref          #52.#53        // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #44 = Utf8               (Ljava/lang/Object;)Ljava/lang/Object;
  #45 = Methodref          #6.#54         // dev/jgardo/jvm/miscellaneous/lambda/LambdaExperiment.lambda$main$0:(Ljava/lang/String;)Ljava/lang/String;
  #46 = Utf8               apply
  #47 = Utf8               ()Ljava/util/function/Function;
  #48 = Utf8               java/util/function/Function
  #49 = Utf8               java/lang/String
  #50 = Utf8               toUpperCase
  #51 = Utf8               ()Ljava/lang/String;
  #52 = Class              #55            // java/lang/invoke/LambdaMetafactory
  #53 = NameAndType        #56:#60        // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #54 = NameAndType        #23:#24        // lambda$main$0:(Ljava/lang/String;)Ljava/lang/String;
  #55 = Utf8               java/lang/invoke/LambdaMetafactory
  #56 = Utf8               metafactory
  #57 = Class              #62            // java/lang/invoke/MethodHandles$Lookup
  #58 = Utf8               Lookup
  #59 = Utf8               InnerClasses
  #60 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #61 = Class              #63            // java/lang/invoke/MethodHandles
  #62 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #63 = Utf8               java/lang/invoke/MethodHandles
{
  public dev.jgardo.jvm.miscellaneous.lambda.LambdaExperiment();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ldev/jgardo/jvm/miscellaneous/lambda/LambdaExperiment;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: invokedynamic #2,  0              // InvokeDynamic #0:apply:()Ljava/util/function/Function;
         5: astore_1
         6: aload_1
         7: ldc           #3                  // String String
         9: invokeinterface #4,  2            // InterfaceMethod java/util/function/Function.apply:(Ljava/lang/Object;)Ljava/lang/Object;
        14: pop
        15: return
      LineNumberTable:
        line 7: 0
        line 8: 6
        line 9: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            6      10     1 lambda   Ljava/util/function/Function;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            6      10     1 lambda   Ljava/util/function/Function;

  private static java.lang.String lambda$main$0(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #5                  // Method java/lang/String.toUpperCase:()Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0     s   Ljava/lang/String;
}
SourceFile: "LambdaExperiment.java"
InnerClasses:
  public static final #58= #57 of #61;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #31 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #32 (Ljava/lang/Object;)Ljava/lang/Object;
      #33 REF_invokeStatic dev/jgardo/jvm/miscellaneous/lambda/LambdaExperiment.lambda$main$0:(Ljava/lang/String;)Ljava/lang/String;
      #34 (Ljava/lang/String;)Ljava/lang/String;

W listingu javap -p -v znaleźliśmy ciało wyrażenia lambda w osobnej, wygenerowanej metodzie o nazwie lambda$main$0 (linia 114), które przyjmuje takie same argumenty jak nasza poszukiwana lambda oraz posiada nazwę, którą widzieliśmy w stacktrace. Jest tam również pewna rzadka instrukcja invokedynamic wprowadzona w Javie 7 (linia 95).

Wciąż jednak nie mamy tej wewnętrznej klasy anonimowej…

Teoria

W projekcie Lambda, twórcy Javy stwierdzili, że jest wiele możliwości implementacji lambd, a każda ma zalety i wady (anonimowa klasa wewnętrzna, dynamiczne proxy, MethodHandle). Nie chcieli się wiązać z żadną implementacją, żeby mieć w przyszłości ewentualną swobodę zmiany koncepcji. Dlatego też stwierdzili, dostarczeniem odpowiedniej implementacji lambdy zajmie się JVM w czasie wykonywania.

Implementacja lambd używa mechanizmu wprowadzonego w poprzedniej wersji Javy – wersji 7 – invokedynamic.
Gdy JVM po raz pierwszy dojdzie do danej instrukcji invokedynamic musi „dowiedzieć się” jaką metodę ma wykonać (w przeciwieństwie do pozostałych instrukcji bytecode z rodziny invoke, na etapie kompilacji nie znamy docelowej wywoływanej metody; znamy tylko metodę, która wskaże co trzeba robić). W przypadku definiowania lambdy, aby się tego dowiedzieć, wywoływana jest metoda java.lang.invoke.LambdaMetafactory.metafactory (linia 132 listingu). W parametrach przekazywane są m. in. uchwyt do wygenerowanej na etapie kompilacji metody tworzącej treść lambdy, jej sygnatura, sygnatura metody interfejsu funkcyjnego, którego implementacji szukamy. Metoda generuje docelową klasę z lambdą, ładuje ją oraz zwraca „fabrykę” obiektów tej klasy.

Po stworzeniu fabryki jest ona przypisywana do danej instrukcji invokedynamic.

Raz stworzona i związana z daną instrukcją invokedynamic fabryka jest następnie używana do uzyskania obiektu implementującego interfejs funkcyjny (lambdę).

Takie podejście ma swoje zalety:– nie wiążemy się z implementacją wyrażeń lambd (ewentualnie można je zmienić)– mniejszy rozmiar bytecode’u pozbawionego klas wewnętrznych oraz mniejsza ilość osobnych plików d0 załadowania– brak możliwości ingerowania w kod lambd – nikt nie będzie grzebał w bytecodzie, którego nie ma 😉

– możliwość cache’owania bezstanowych lambd – nie trzeba zawsze tworzyć nowych obiektów.

Hello World 0 (0)

Cześć!

będę tu pisać o programowaniu!

A tak będzie widać kod?
Nie… znajdę jakąś lepszą wtyczkę 😉

I generalnie to czekam na domenę… jgardo.dev 😉

Edit: domena jest, jakichś highlighter również, więc można blogować 😉

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}