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ż C2
potrafi 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
.
Następny artykuł z serii final: pola statyczne.