Ruby 3.5 i stan YJIT oraz ZJIT
Historia JIT Ruby po cichu stała się jedną z najciekawszych rzeczy w języku. Z Ruby 3.5 YJIT jest dojrzały i rozsądnym domyślnym — a nowy JIT oparty na metodach, ZJIT, wyłania się obok niego. Spojrzenie na koniec roku na to, gdzie stoi praca wydajnościowa Ruby i dlaczego posiadanie dwóch podejść JIT to zdrowy znak.
Są święta, więc jest nowe Ruby — 3.5 — a nicią, którą chcę pociągnąć na koniec roku, jest ta, która po cichu stała się najciekawszą trwającą historią w języku: jego kompilatory JIT. Od lat omawiam stały marsz YJIT od eksperymentu do dojrzałego produkcyjnego domyślnego. Z Ruby 3.5 ten marsz jest w zasadzie ukończony — YJIT to coś, co po prostu włączasz — a jednocześnie nowy JIT, ZJIT, wyłania się obok niego z inną filozofią projektową. Dwa poważne wysiłki JIT w jednym języku to niezwykłe, i warto zrozumieć, czym każdy jest, dlaczego oba istnieją i co istnienie dwóch mówi o zdrowiu pracy wydajnościowej Ruby. To stan JIT na koniec roku.
Gdzie stoi YJIT teraz
Zacznij od zasiedziałego, bo jego historia to historia cierpliwego sukcesu. YJIT — kompilator just-in-time Ruby, który kompiluje gorący Ruby do kodu maszynowego w czasie wykonania — był nicią przewodnią niedawnych wydań, stając się szybszym, bardziej oszczędnym pamięciowo i dojrzalszym z każdym. Do Ruby 3.5 ta trajektoria osiągnęła swój naturalny wniosek: YJIT to bezpieczny, rozsądny produkcyjny domyślny, nie eksperymentalna flaga, którą starannie ważysz. Włączasz go, mierzysz swoją aplikację, bierzesz przyspieszenie. Długi projekt uczynienia JIT, który jest niezawodnie wart włączenia dla prawdziwych obciążeń, jest, dla YJIT, zrobiony.
Warto trzymać uczciwy model mentalny, który podkreślałem wcześniej: YJIT przyspiesza wykonanie Ruby związane z CPU — czas, który twój kod spędza wykonując instrukcje Ruby — a nie I/O, na które twoja aplikacja spędza dużo czasu czekając. Więc zysk w realnym świecie wciąż waha się wedle obciążenia: kod ciężki obliczeniowo widzi dużo, aplikacja webowa związana z I/O widzi mniej (ale wciąż realne, i darmowe). To się nie zmieniło; co się zmieniło, to że YJIT jest teraz dość dojrzały, że pytaniem nie jest „czy mam zaryzykować?”, lecz po prostu „włącz go i zmierz”.
Co YJIT faktycznie robi, krótko
By zrozumieć, dlaczego drugi JIT w ogóle by się pojawił, pomaga znać podejście YJIT w jednym akapicie. YJIT jest zbudowany na technice zwanej wersjonowaniem bloków podstawowych — kompiluje Ruby leniwie, jeden mały kawałek wykonania naraz, generując kod maszynowy dla konkretnych ścieżek, które program faktycznie obiera, gdy działa. To czyni go szybkim w rozgrzewaniu i dobrym w obsłudze dynamizmu Ruby, bo kompiluje to, co faktycznie widzi, zamiast rozumować o całej metodzie z góry. To pragmatyczne, przyrostowe podejście, i służyło Ruby nadzwyczaj dobrze. Ale to jedno podejście z własnymi charakterystykami, i to jest otwarciem dla innego.
ZJIT: inne podejście wyłaniające się
Nowym rozwojem jest ZJIT, JIT następnej generacji rozwijany (z tego samego wysiłku prowadzonego przez Shopify, który dał Ruby YJIT) z inną filozofią kompilacji. Tam, gdzie YJIT robi leniwą kompilację blok-podstawowy-naraz, ZJIT jest JIT opartym na metodach zbudowanym na bardziej tradycyjnej architekturze kompilatora — analizuje i kompiluje całe metody, używając reprezentacji pośredniej, rodzaju projektu, którego używają klasyczne kompilatory optymalizujące. Motywacją jest to, że podejście na poziomie metody, oparte na IR, może umożliwić optymalizacje trudniejsze do wyrażenia w modelu bloku podstawowego — widzenie więcej kodu naraz otwiera drzwi do analizy i transformacji, których kompilacja blok-naraz nie może łatwo zrobić.
Parę uczciwych ujęć, bo jest wcześnie:
- ZJIT jest nowy i wyłaniający się, nie ukończony zamiennik. To następna faza badań i inżynierii nad JIT Ruby, pojawiająca się obok YJIT, a nie wypierająca go. Traktuj go jako kierunek przyszłej pracy, nie jako dzisiejsze narzędzie produkcyjne.
- Reprezentuje zakład na inny punkt w przestrzeni projektowej. Kompilacja oparta na metodach, napędzana IR, to dobrze-przetarta ścieżka w projektowaniu kompilatorów, a przyniesienie jej do Ruby to próba uchwycenia optymalizacji, które obecne podejście zostawia na stole. Czy i kiedy przewyższy YJIT w praktyce, jest dokładnie tym, co nadchodzące lata powiedzą.
- Dwa mogą się nawzajem informować. Lekcje z lat YJIT na produkcji zasilają projekt ZJIT, a istnienie obu pozwala zespołowi Ruby badać kompromisy między podejściami prawdziwym dowodem, a nie spekulacją.
Dlaczego dwa JIT to zdrowy znak
Może wyglądać dziwnie, nawet marnotrawnie, by jeden język miał dwa poważne kompilatory JIT — ale argumentowałbym, że to przeciwność: znak zdrowia. Rozważ, co to faktycznie wskazuje:
- Trwała, poważna inwestycja w wydajność Ruby. Język dostaje jeden dojrzały JIT i następnej generacji aktywnie rozwijany tylko, gdy jest prawdziwe, ciągłe zaangażowanie inżynierskie w uczynienie go szybkim. Ta inwestycja to głos zaufania w przyszłość Ruby jako wydajnego języka, nie tylko produktywnego.
- Badanie przestrzeni projektowej zamiast osiadania. Posiadanie YJIT udowodnionego na produkcji, podczas gdy ZJIT bada inną architekturę, znaczy, że zespół Ruby nie stawia wszystkiego na jedno podejście — uczą się, które kompromisy faktycznie się opłacają, z działającym fallbackiem przez cały czas. Tak robi się postęp na trudnych problemach bez ryzykowania stabilnej rzeczy, na której ludzie polegają.
- Historia wydajności jest daleka od zakończenia. Kilka lat temu pytaniem było, czy Ruby w ogóle dostanie produkcyjnie-zdatny JIT. Teraz ma dojrzały i ambitnego następcę w rozwoju. Trajektoria Ruby stającego się stale szybszym, wydanie po wydaniu, wyraźnie ma więcej miejsca, by biec.
To ta sama jakość dojrzałości-bez-stagnacji, którą podziwiałem w innej niedawnej pracy Ruby — ostrożna, napędzana dowodem, gotowa inwestować w lepszy fundament, jednocześnie trzymając udowodniony działającym. Dwa JIT to nie duplikacja; to język traktujący własną wydajność dość poważnie, by robić trudną, cierpliwą pracę poprawiania jej porządnie.
Werdykt
Historia JIT Ruby po cichu stała się jedną z najciekawszych rzeczy w języku, a Ruby 3.5 oznacza kamień milowy w niej. YJIT ukończył swój długi marsz od eksperymentu do dojrzałego produkcyjnego domyślnego — po prostu włączasz go i mierzysz, trzymając uczciwy model, że przyspiesza wykonanie Ruby związane z CPU, a nie I/O, na które twoja aplikacja czeka, więc zysk zależy od obciążenia, ale jest realny i darmowy. A nowy JIT oparty na metodach, ZJIT, wyłania się obok niego, zbudowany na bardziej tradycyjnej architekturze kompilatora napędzanej IR, która celuje w uchwycenie optymalizacji, których podejście bloku podstawowego nie może łatwo wyrazić — wczesny, nie ukończony zamiennik, ale jasne stwierdzenie, dokąd praca zmierza. Istnienie dwóch poważnych wysiłków JIT w jednym języku to nie marnotrawstwo; to znak zdrowia — trwała inwestycja w wydajność Ruby, gotowość badania przestrzeni projektowej z udowodnionym fallbackiem na miejscu i jasny dowód, że trajektoria Ruby stającego się szybszym wydanie po wydaniu jest daleka od zakończenia. Po latach omawiania każdego świątecznego wydania nić przewodnia jest jasna: Ruby to dojrzały język, który odmawia stagnacji, a jego historia JIT — jeden kompilator udowodniony, drugi budowany, by go przewyższyć — to najostrzejszy przykład tego. Włącz YJIT dziś; obserwuj ZJIT z zainteresowaniem, bo to tam zmierza wydajność Ruby następnie.