Final – zmienne lokalne i argumenty metod, a wydajność

W poprzednim wpisie pisałem o final pod kątem klas i metod. W tym skupie się na zastosowaniu final przy zmiennych lokalnych oraz argumentach metod.

Zmienne lokalne

Pierwszym miejscem, gdzie moglibyśmy szukać optymalizacji jest kompilacja do bytecode’u. Jak już pisałem we wcześniejszych wpisach, na tym etapie zbyt dużo optymalizacji się nie dzieje.

Jednak jeśli skompilujemy następujący fragment kodu zauważymy pewne ciekawe fakty.

    public void finalVariablePresentation() {
        final String final1 = "a";
        final String final2 = "b";
        final String finalConcatenation = final1 + final2;

        String nonFinal1 = "a";
        String nonFinal2 = "b";
        String nonFinalConcatenation = nonFinal1 + nonFinal2;
    }

Po kompilacji powyższego kodu, a następnym zdekompilowaniu (z użyciem Bytecode Viewer oraz widoku JD-GUI Decompiler) możemy zobaczyć następujący kod:

    public void finalVariablePresentation() {
        String final1 = "a";
        String final2 = "b";
        String finalConcatenation = "ab";
    
        String nonFinal1 = "a";
        String nonFinal2 = "b";
        String nonFinalConcatenation = nonFinal1 + nonFinal2;
    }

W tym listingu widzimy 2 ciekawe rzeczy.
Pierwszą jest to, że jeśli mamy w kodzie dwie finalne zmienne lokalne (takie stałe lokalne), które chcemy ze sobą konkatenować, to ta konkatenacja jest robiona na etapie kompilacji do bytecodu. Dzięki temu nie musimy robić konkatenacji przy każdym wywołaniu metody. Skutkuje zmniejszeniem czasu potrzebnego do uzyskania danej wartości z 10,8 ns do 6,5 ns (na moim komputerze, po rozgrzaniu i kompilacji C2).
Zysk może być jeszcze większy w przypadku wcześniejszych wersji Javy niż 8. Dopiero w tej wersji Javy tworzenie nowych Stringów przy użyciu operatora + jest w czasie kompilacji zamieniana na new StringBuilder().append().append().toString().

Drugim ciekawym faktem, który widzimy we wspomnianych listingach jest utrata informacji o final. Zatem poza wspomnianym wcześniej mechanizmem ewaluacji wyrażeń, nie ma żadnych dodatkowych wydajnościowych zalet stosowania słowa final, ponieważ… tej informacji nie ma w bytecodzie.

Constant Folding

Technika obliczania wyrażeń w czasie kompilacji, jeśli znamy składowe tego wyrażenia nazywa się Constant Folding.

W czasie kompilacji do bytecode’u oprócz Stringów jest ona używana do ewaluacji wyrażeń typu prymitywnego. Jednak w przeciwieństwie do Stringów nie powoduje przyspieszenia działania programu. Jest tak, ponieważ C2potrafi sam „wywnioskować”, które zmienne są stałe (nawet bez final) oraz C2 również wykorzystuje Constant Folding dla zmiennych prymitywnych (dla Stringów nie), zatem dla zminnych prymitywnych, nie ma znaczenia, czy jakieś wyrażenie zostanie wyliczone w czasie kompilacji do bytecode’u, czy w czasie kompilacji C2.

Argumenty metod

Również dla argumentów metod warto sprawdzić, co można wyczytać z bytecode’u. Zatem po skompilowaniu danego fragmentu kodu:

    public void countSomeHash() {
        final int a1 = countHashPrivate(2);
        final int b1 = countHashPrivate(4);
        final int n1 = 20;

        final int result1 =  a1 * b1 + n1 * b1;

        int a2 = countHashPrivateWithoutFinals(2);
        int b2 = countHashPrivateWithoutFinals(4);
        int n2 = 20;

        final int result2 =  a2 * b2 + n2 *b2;
    }

    private int countHashPrivate(final int n) {
        final int a = 3;
        final int b = 2;
        return a * b + n *b;
    }

    private int countHashPrivateWithoutFinals(int n) {
        int a = 3;
        int b = 2;
        return a * b + n *b;
    }

a następnie zdekompilowaniu, otrzymujemy podany fragment kodu:

  public void countSomeHash() {
    int a1 = countHashPrivate(2);
    int b1 = countHashPrivate(4);
    int n1 = 20;
    
    int result1 = a1 * b1 + 20 * b1;
    
    int a2 = countHashPrivateWithoutFinals(2);
    int b2 = countHashPrivateWithoutFinals(4);
    int n2 = 20;
    
    int result2 = a2 * b2 + n2 * b2;
  }
  
  private int countHashPrivate(int n) {
    int a = 3;
    int b = 2;
    return 6 + n * 2;
  }

  private int countHashPrivateWithoutFinals(int n) {
    int a = 3;
    int b = 2;
    return a * b + n * b;
  }

W czasie kompilacji do bytecode’u nie widać żadnych rezultatów optymalizacji. Ponadto, nie widać też informacji, że dany argument metody jest finalny.

Okazuje się, że generalnie o argumentach metod mało wiemy. Nie znamy żadnych modyfikatorów argumentów (final), nie znamy również ich nazw. Jednak to domyślne zachowanie można od Javy 8 zmienić przez dodanie do javac argumentu -parameters.

Niestety dodanie wspomnianego parametru nie wpływa na wydajność…

Podsumowanie

Niestety utrata informacji o final w czasie kompilacji do bytecode’u zamyka ewentualne możliwości optymalizacji kodu.

Jedyną sensowną optymalizacją jest wspomniane Constant Folding w celu wyliczenia String. Dla wartości prymitywnych ta technika może pozytywnie wpłynąć na czas wykonywania tylko w trybie interpretowanym lub po kompilacji przez C1.

Autor: jgardo

Programista Java od 2013 roku. Interesuje się niskopoziomową Javą, ekosystemem Jvm i jego wydajnością. Co jednak nie przeszkadza w przywiązywaniu uwagi do czystości kodu w życiu codziennym ;) Pracuje w PayU.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *