Ile razy kod Javy trzeba kompilować, aby optymalny…
Wpis ten jest wstępem do kolejnych wpisów, więc może być dosyć ogólny, względnie nudny dla przeciętnego Senior Tech JVM Performance Leada 😉
OpenJDK/OracleJDK
Największą częścią rynku (91%) są dystrybucje JVMa z rodziny OpenJDK (z OracleJDK włącznie). Dystrybucje te bazują na tym samym kodzie źródłowym, zatem można je spokojnie omawiać wspólnie.
Kompilacja plików .java
Chronologicznie należałoby zacząć od kodu źródłowego zapisanego w plikach.java
.
Kod kompilowany jest do standardowo bytecode
‚u i ta kompilacja jest wykonywana przed uruchomieniem programu. Spokojnie tę kompilację można nazwać pierwszą kompilacją i – bynajmniej – nie jest ona optymalna 😉 Wszelkie metody optymalizacji są tutaj bardzo ograniczone. Można zrobić jakieś Constant Folding
(o którym pisałem już chociażby we wpisach o finalach), jednak nie jest to szczyt technik optymalizacyjnych.
Kod kompilowany do bytecode
‚u kompiluje się jednak znacznie szybciej, niż kompilowany do kodu natywnego. Jest też bardziej elastyczny. Czyli do developmentu „good enough”.
JIT
Kompilacja just-in-time
(swobodne tłumaczenie to „rychło w czas”) polega na kompilowaniu kodu w czasie działania aplikacji.
Jakby się głębiej zastanowić, to widać podobieństwa do samolotu w tejże reklamie.
Założenie jest takie, że większość kodu jest wykonywana na tyle rzadko, że można ją po prostu wykonać w trybie interpretowanym. Szkoda zatem cykli procesora na szalone optymalizacje fragmentów kodu, wykonywanych jednokrotnie. Warto się jednak skupić na metodach wykonywanych tysiące razy.
Co więcej, skoro mamy już pewien narzut na interpretację kodu bajtowego, to do czasu porządnej optymalizacji warto poświęcić procesor i zbierać dodatkowe statystyki. Mogą one być bardzo przydatne – jeśli w ciągu przykładowo 1000 wykonań program ani razu nie wszedł w jakiegoś ifka, to prawdopodobnie przy 1001 wywołaniu również nie wejdzie. Można przykładowo pominąć treść sekcji, którego ten ifek dotyczy (zaoszczędzimy bajtów i nie tylko, acz o tym kiedy indziej). W razie czego, gdyby przez przypadek ifek stał się prawdziwy, można przykładowo odesłać do wykonywania źródeł w bajtkodzie (to tylko przykładowy przykład; trochę inaczej się to robi w realu).
C1, C2
Dawno temi, w zamierzchłych czasach Javy 1.6 istniały dwa osobne kompilatory C1 (szybki, acz niedokładny – kliencki) oraz C2 (wolny, ale zaawansowany – serwerowy/opto
). Na starcie wybierało się pożądany kompilator flagą lub JVM sam nam go wybierał bazując na parametrach naszego sprzętu.
W Javie 1.7 została wprowadzona tzn. kompilacja warstwowa. Według niej interpretujemy kod. Po przekroczeniu progu 2 000
wywołań kompilujemy metodę z użyciem C1. Jednak życie toczy się dalej, a metoda dalej jest wywoływana. Po wywoływaniu metody 15 000
razy jest ona kompilowana z użyciem C2.
Jest jednak kilka „ale”.
Wspomniana kompilacja warstwowa ma ponumerowane warstwy. Poziom 0 to nieskompilowana metoda w trybie interpretowanym. Poziom 4 to C2. Za to za warstwy 1-3 odpowiada C1, która ma różne „warianty”. Istnieje wariant z pełnym profilowaniem (3), ale też istnieje lżejsza wersja ze ograniczonym profilowaniem(2) i najlżejsza – bez profilowania (1).
W idealnej sytuacji kompilujemy metodę do warstwy 3 (po 2 000
wykonaniach), a następnie do warstwy 4 (po 15 000
wykonaniach). Jednak nie zawsze tak jest. Trzeba mieć świadomość, że po przekroczeniu tych progów metoda jest wrzucana do kolejki metod do kompilacji. Przykładowo czasem kolejka do kompilatora C2 jest na tyle długa, że może do czasu zwolnienia C2 można ograniczyć profilowanie (przejść z 3 warstwy na 2). Jak głoszą slajdy Oracle’owe, trzeci poziom jest 30% wolniejszy niż drugi.
AoT?
Jak wiemy istnieje też GraalVM i kompilator Graal. Jest to jednak rozwiązanie typowo Oracle’owe, więc w tym wpisie nie będę rozwijał tego tematu.
Są jednak dwie ciekawostki.
Pierwsza jest taka, że jeśli macie ochotę napisać własny kompilator, to nie ma sprawy, OpenJDK (od Javy 9) wesprze Cię w tym wystawiając interfejs,który trzeba zaimplementować pisząc kompilator. Proste 5 metodek 😉
Druga ciekawostka jest mniej znana – również w Javie 9 powstał eksperymentalnie kompilator AoT. Pozwala on kompilować program do kodu natywnego. Istnieje jednak jeszcze drugi tryb kompilacji – kompilowanie do kodu natywnego z profilowaniem. Taka opcja pozwala na rekompilację z użyciem C2 i dodatkowych metadanych zbieranych w czasie działania programu. W założeniu ten tryb miał przyspieszyć włączanie projektu, jednak benchmarki powiadają, że tak się nie dzieje…
Podsumowanie
O ile trochę wiemy co się dzieje, to nie wiemy w jaki sposób. Zatem w kolejnych wpisach napiszę nieco o tym co się kompiluje, kiedy, jak oraz skąd to wszystko mamy wiedzieć…
Z ciekawych linków zostawiam tylko wpis na blogu Microsoftu o AoT i nie tylko.
Gwiazdkujcie, komentujcie i bywajcie zdrów 😉
Pax et Bonum