Dzisiejsze wydanie mamy stricte tematyczne, ponieważ od różnych stron będziemy przyglądać się różnym inicjatywom dotyczącym wydajności w Javie.
1. Project Skogsluft obiecuje duże zmiany w Java Flight Recorder
Wraz z rychłym pojawieniem się Foreign Function and Memory API, które już w marcu w stabilnej wersji trafią do JDK 22, już niedługo okazać się może że coraz popularniejsze stanie się używanie większej ilości „natywnego” kodu. Temat staje się coraz popularniejszy, przewija się w różnych wariantach w zasadzie każdej kolejnej edycji tego przeglądu (a nawet dzisiaj jeszcze do niego wrócimy trochę później), dlatego niezbędne jest również dopasowanie całej javowej „narzędziowki”, aby ta gotowa była na ten spodziewany wzrost użycia. Dlatego też nie powinno dziwić, że pojawiła się propozycja nowego projektu, którego celem będzie badanie wydajności aplikacji.
Projekt „Skogsluft”, zaproponowany przez Jaroslava Bachoríka z DataDog, ma na celu znaczące wzmocnienie możliwości profilowania Javy w ramach Java Flight Recorder (JFR) poprzez wprowadzenie zaawansowanych funkcji, które zniwelują różnice między profilowaniem wykonania kodu Java i kodu natywnego. Skogsluft koncentrować ma się na trzech głównych usprawnieniach: zintegrowany mechanizmie przechodzenia po stosie również dla kodu natywnego, elastycznym harmonogramie próbkowania CPU dostosowanym do możliwości różnych systemów operacyjnych oraz rozszerzonym wsparciu dla labelowania wątków w JFR. Mają one na celu dostarczenie programistom bardziej elastycznych opcji profilowania oraz bogatszego kontekstu dla analizy.
Teraz projekt poszukuje wsparcia ze strony twórców OpenJDK. Proponowany rozwój projektu ma odbywać się w oddzielnym forku (od wersji JDK 23) z zamiarem stopniowego wdrażania nowych funkcji w serii JEP.
Skąd nazwa „Skogsluft”? Pochodzi z norweskiego, w którym „skog” oznacza „las”, a „luft” oznacza „powietrze”. Dosłownie przetłumaczone, „Skogsluft” oznacza „powietrze leśne” lub „powiew lasu”. Myślę, że to całkiem dobra nazwa, biorąc pod uwagę powiew świeżości, które projekt może wprowadzić do profilowania w JFR, choć pracowałem kiedyś dla norweskiej firmy i ze zwrotami z ichniejszego języka to nigdy nie wiadomo.
A jak już przy JFR, to podrzucę Wam jeszcze tekst Java Flight Recorder na Kubernetes. Artykuł Piotra Minkowskiego dostarcza kompleksowego przewodnika na temat użycia Java Flight Recorder (JFR) na Kubernetes przy pomocy Cryostat do ciągłego monitorowania aplikacji Java. Cryostat to narzędzie umożliwiające zdalne zarządzanie nagraniami Java Flight Recorder dla aplikacji działających w kontenerach, ułatwiające monitorowanie i diagnozowanie wydajności aplikacji. Wyjaśnia pełen proces od instalacji Cryostat za pomocą operatora (komponentu w Kubernetes, który automatyzuje wdrażanie, skalowanie i zarządzanie aplikacjami i usługami, ułatwiając zarządzanie złożonymi, stanowymi aplikacjami) lub wykresu Helm, po utworzenie przykładowej aplikacji Spring Bootowej generującej niestandardowe zdarzenia JFR. Post tłumaczy też, jak zarządzać niestandardowymi szablonami zdarzeń i analizować dane za pomocą JDK Mission Control.
I ostatnia wrzutka – Gunnar Morling, który ostatnio jest znany z opisywanego choćby i tutaj 1BRC, opublikował kiedyś specyfikacje nieodokumentowanego do tej pory formatu pliku wynikowego JDK Flight Recorder. Ot, takie mały crossover, nie mogłem sobie odpuścić.
Zainstaluj teraz i czytaj tylko dobre teksty!
2. It’s benchmarking time! FMA vs Unsafe
Zaczeliśmy od profilingu, ale to nie koniec tematów związanych z szeroko rozumianą wydajnością. Jak już wcześniej obiecałem, wracamy do tematu Foreign Memory Access API.
W społeczności Java niedawno dużo dyskutowano o nowym JEP, który proponuje wycofanie i usunięcie metod dostępu i alokacji pamięci z klasy Unsafe – sam w poprzedniej edycji komentowałem, że w obliczu wyników 1BRC twórcy mogą mieć ciężki orzech do zgryzienia. Unsafe pozwalało na alokację pamięci poza modelem pamięci Java, oferując bardzo szybki dostęp do pamięci i umożliwiając operacje, które w innym przypadku byłyby niemożliwe w ramach modelu pamięci Java. Ta funkcjonalność była wykorzystywana w wielu bibliotekach wysokiej wydajności, takich jak Netty, Spark, Avro, Kryo, które teraz muszą znaleźć alternatywy. Jednak Tomer Zeltzer, Senior Software Engineer at Yahoo, w swoim artykule Java’s New FMA: Renaissance Or Decay? zwraca uwagę, że pomimo początkowego szoku, istnieją alternatywy dla Unsafe, które mogą okazać się obiecujące.
Dokonuje on bowiem benchmarkingu nowego API dostępu do pamięci obcej (FMA), opracowanego jako część Projektu Panama (JEP-424), który ma zastąpić Unsafe, oferując bezpieczniejsze i oficjalnie wspierane rozwiązanie. FMA prezentuje się obiecująco, oferując porównywalną prędkość odczytu i 42% szybszy zapis na stercie w porównaniu z Unsafe, a także prawie trzykrotnie szybszy dostęp do pamięci poza stertą. Nowe API wykorzystuje obiekt Arena
do alokacji pamięci, a segmenty pamięci, które z niej pochodzą, nie mogą być indywidualnie zwalniane, co może ułatwić unikanie wycieków pamięci. Choć zwolnienie pamięci jest mniej granularne niż w Unsafe, nowe API wydaje się zachować te same możliwości przy jednoczesnym oferowaniu nieco wygodniejszego interfejsu.
Tutaj mały disclaimer: oryginalny artykuł był nieco dłuższy, ale po dyskusji na Reddicie – w której zaangażowany był sam Ron Pressler – niektóre fragmenty zostały wyedytowane. Dla porządku: oryginalną wersje znajdziecie tutaj, ale osobiście polecam po prostu sprawdzić poprawną, najnowszą wersje.
Jeśli czujecie się zainteresowani nowymi API i czujecie się zaciekawieni, aby poznać jego „bebechy”, nadarza się świetna okazja. Początkiem tygodnia na kanale Javy pojawiło się bowiem wideo Foreign Function & Memory API – A (Quick) Peek Under the Hood, które sam miałem okazje już obejrzeć i naprawdę polecam. Dowiecie się z niego nie tylko o funkcjonalnościach Foreign Function and Memory API (te już pewnie są mniej więcej znane) ale dowiecie się założenia dotyczące projektu samego API, aby oferowało podejście „Java-first” do wywoływania funkcji natywnych i zarządzania segmentami pamięci, omijając przy tym ograniczenia JNI i Direct Buffer API
, szczególnie w obszarach wymagających intensywnej obliczeń numerycznych, jak uczenie maszynowe.
Zainstaluj teraz i czytaj tylko dobre teksty!
3. Wielkie Porównanie kompilatorów JIT
Ale mimo, że porównanie F(F)MA oraz Unsafe to ciekawy temat, blednie on nieco przy monumentalności kolejnego z benchmarków. Tekst JVM Performance Comparison for JDK 21 autorstwa Ionuta Balosina i Florina Blanaru przedstawia bowiem szczegółowe porównanie wydajności różnych kompilatorów Just-In-Time (JIT), ze szczególnym uwzględnieniem JDK 21. Całość to ponad godzina (!) lektury (i to w wypadku jak szybko czytacie), a benchmarki zostały podzielone na różne kategorie, pokrywające szerokie spektrum scenariuszy, od niskopoziomowych optymalizacji kompilatora po wysokopoziomowe użycie API Javy i klasyczne problemy programistyczne, takie typowo programistyczne, jakich spodziewacie się od rekrutacji do FAANG czy Advent of Code.
Oceniane kompilatory to C2 (Server) JIT z OpenJDK 21 oraz dwie wersje kompilatora Graal JIT (z GraalVM CE 21+35.1 i Oracle GraalVM 21+35.1), testowane na architekturach x86_64 i arm64. Benchmarki przeprowadzono przy użyciu Java Microbenchmark Harness (JMH) wersji 1.37 na MacBooku Pro z chipem M1 (przyznaje – jest to bardzo ciekawy, nietypowy wybór) i Dell XPS 15 z procesorem Intel Core i7-9750H, w kontrolowanych warunkach, aby zminimalizować zmienność wydajności. Ogólnie wygląda na to, że twórcy bardzo przyłożyli się do odrobienia pracy domowej.
Ogólnie rzecz biorąc, kompilator Oracle GraalVM JIT okazał się najszybszy spośród testowanych kompilatorów, wykazując znaczącą przewagę wydajności nad kompilatorem C2 JIT, sięgający od 23% na x86_64 do 17% na arm64. Zyski te są wynikają z optymalizacji dawnego Enterprise Edition, takich jak ulepszona analiza częściowej ucieczki i bardziej agresywne strategie inliningu. Co ciekawe, choć wyniki kompilatorów C2 JIT i GraalVM CE JIT uśredniając były zbliżone, różniły się znacznie pod względem konkretnych możliwości optymalizacji. C2 oferuje zaawansowane wsparcie dla intrinsics (wbudowanych funkcji specjalnie obsługiwanych przez procesor) i wektoryzacji (przetwarzanie danych w blokach zamiast pojedynczo) oraz lepsze zarządzanie wyjątkami niż GraalVM CE JIT, ale ma ograniczenia w heurystyce inliningu (wstawiania treści funkcji w miejsce jej wywołania), devirtualizacji (optymalizacji wywołań metod w obiektach) skomplikowanych wywołań i w rzadkich przypadkach może nie dokonać kompilacji, co skutkuje użyciem mniej optymalnych ścieżek wykonania (takich jak zatrzymanie kompilacji na poziomie C1 lub nawet kodu interpretowanego).
Tak jak wspomniałem, tekst jest naprawdę, naprawdę przekrojowy, więc jeśli jesteście ciekawi (i macie cierpliwość), możecie sprawdzić poszczególne wyniki. Ja będę używał go pewnie nie raz, i będzie mi służył jako szczegółowe (naprawdę szczegółowe) źródło informacji na temat mocnych i słabych strony różnych kompilatorów JIT. Oczywiście, jak to zwykle w tego typu publikacjach, artykuł kończy się przypomnieniem, że choć kompilator Oracle GraalVM JIT rzeczywiście prowadzi pod względem wydajności, wybór dystrybucji JVM nie powinien opierać się wyłącznie na wynikach mikro-benchmarków – nie da się jedna ukryć, że w 2024 GraalVM ma naprawdę dobrą passę.