Kompilator JIT w PHP 8
PHP jako język interpretowany od dawna zmagał się z trudnym zadaniem poprawy swojej wydajności, szczególnie na tle konkurencyjnych języków programowania używanych w rozwiązaniach webowych. Pośród tego co nowe w PHP 8 na szczególną uwagę zasługuje kompilator JIT, który zdecydowano się oficjalnie dołączyć do najnowszej wersji.
Czy krok ten faktycznie będzie przełomowy, a może jednak będzie to tylko ciekawy dodatek do języka? Sprawdźmy zatem jakie realne korzyści może on przynieść i czy naprawdę podniesie wydajność Twojej aplikacji.
Zapewne nie każdy pamięta, ale od wersji 5.5 integralną cześcią PHP stał się OPcache wykonywany przez Zend VM. Był to przełom jeśli chodzi o poprawę wydajności aplikacji, a tym samym w kontekście dostarczania usług PHP web developmentu. Szczególną poprawę przy jego użyciu można było odczuć wraz z wydaniem wersji PHP 7. Już na tym etapie padały pierwsze propozycje implementacji i dołączenia JIT, mimo to społeczność próbowała zmaksymalizować wydajność przy pomocy dostępnych mechanizmów.
Czym jest JIT?
JIT (just-in-time compilation) to w zasadzie technika pozwalająca na kompilowanie programu do kodu maszynowego bezpośrednio przed jego wykonaniem. W praktyce pozwala to na znaczne przyspieszenie wykonywanego kodu względem tradycyjnego interperetera. Niekiedy JIT ocenia się jako złoty środek między elastycznością w rozwijaniu kodu aplikacji, a efektywnością jego uruchamiania.
Zasada działania
Jak wspominałem, do tej pory przyspieszona wydajność PHP opierała się na OPCache korzystając tzw. OPCode, czyli prekompilowanej porcji kodu w formie rozkazów przekazywanych do wykonania do procesora, utworzonej po pierwszym uruchomieniu skryptu. Tak zcachowany kod może być niemal natychmiast uruchamiany przez wirtualną maszynę, lecz nie jest to jednak nadal natywny kod maszynowy.
JIT jest natomiast faktycznym kodem maszynowym, który został tak zaimplementowany, żeby współgrać z OPCache. Przy uruchomieniu skryptu, jeżeli został on już zcachowanych w OPCache, zostaje on natychmiastowo zwrócony i skompilowany (jeśli nie został skompilowany już wcześniej). Jeśli natomiast skrypt nie został jeszcze zcachowany, w takim przypadku przechodzi on najpierw pełny proces generacji do OPCode, tak jak miało miejsce do tej pory. Całość procesu dobrze obrazuje zamiesczony poniżej diagram.
Konfiguracja JIT
Jeżeli ktoś z Was myślał, że JIT jest po prostu częścią języka/interpretera i zadziała z automatu, to jest w błędzie. Niestety, wymaga on dodatkowej konfiguracji, która na pierwszy rzut oka wcale nie wydaje się szczególnie przyjazna i oczywista. Całość operacji przeprowadzana jest w dobrze znanym programistom PHP pliku konfiguracyjnym php.ini.
Pierwszą ważną rzeczą jest to, że JIT działa tylko wówczas, kiedy mamy włączony OPCache. Każda domyślna instalacja PHP posiada tę wartość (opcache.enable) od razu ustawioną na 1. Następnie, aby odblokować JIT musimy ustawić dwa parametry:
- opcache.jit_buffer_size
- opcache.jit
Pierwszy z nich (opcache.jit_buffer_size) jest odpowiedzialny za zdefiniowanie ilości pamięci jaką chcemy przydzielić na kompilację kodu. Ustawienie przykładowej wartości jest zatem dość proste:
opcache.jit_buffer_size=256M
Jeśli chodzi natomiast o drugi parametr (opcache.jit), to ma on w zasadzie mówić, jak JIT ma działać. Dla odmiany zacznę najpierw od przykładu:
opcache.jit=1255
Tak, mnie również zdziwiła tajemnicza wartość liczbowa. Z początku myślałem, że to jakaś maska bitowa lub coś podobnego, ale analizując RFC, w którym znajdziecie więcej szczegółów na temat poszczególnych opcji, okazało się, że każda z tych cyfr z osobna to poszczególna wartość konfiguracyjna. W miejsce prezentowania wszystkich dostępnych opcji chciałbym przedstawić trzy wartości konfiguracyjne, które mogą okazać się najczęściej używane i są swego rodzaju drogą na skróty ułatwiającą programistom ustawienie pożądanego trybu.
- opcache.jit=1205 - cały kod poddany zostaje kompilacji przez JIT
- opcache.jit=1235 - tylko wybrane porcje kodu (na podstawie ich relatywnego użycia) zostają przekazane do kompilacji JIT
- opcache.jit=1255 - kod aplikacji jest śledziony pod kątem możliwości skompilowania przez JIT i tak wybrane partie kodu są przekazywane do kompilatora
Oczywiście nie ja jedyny zwróciłem uwagę na wątpliwą przystępność takiej konfiguracji. Dlatego do wspomnianego RFC dodano poprawki, a w zasadzie dwa aliasy tracing i function, których można używać w miejsce wartości numerycznych np.
opcache.jit=tracing
Różnica między tymi trybami polega na tym, że JIT będzie próbował zoptymalizować kod tylko w zasięgu jednej funkcji w przypadku użycia wartości function, a w przypadku użycia wartości tracing patrzy na cały ślad stosu i szuka porcji kodu nadających się do zoptymalizowania. Spośród tych dwóch opcji najczęściej polecaną jest tracing JIT, który daje najbardziej obiecujące wyniki w benchmarkach, zwiększając w znacznym stopniu wydajnośc aplikacji.
Wpływ na wydajność webaplikacji
Każdy dobry programista powinien wiedzieć, że w aplikacji najważniejszą kwestią wpływającą na jej wydajność jest przede wszystkim jakość napisanego kodu. Równie ważnym czynnikiem jest dobór technologii, które składają się na całą jej architekturę i na to w jaki sposób są one rozwijane. W PHP OPCache był faktycznie pewną rewolucją, jednakże JIT, pomimo wielkiego szumu medialnego, wydaje się jednak nie być takim samym krokiem milowym, a szczególnie w zastosowaniach webowych. Dlaczego tak jest?
JIT został wprowadzony z myślą o kompilowaniu partii kodu, który nie jest poddawany znacznym fluktuacjom. Wykrywa te porcje kodu, które wykonywane są więcej niż raz i odpowiednio je kompiluje. Jak więc możecie się domyśleć, wykonanie kodu podczas obsługi pojedynczego zapytania/żądania zależy od zbyt wielu zmiennych i w praktyce okazuje się, że tych identycznych porcji kodu jest o wiele mniej. Co więcej, może się okazać, że jest go na tyle mało, że JIT zamiast realnie przyspieszyć aplikację, może ją jeszcze spowolnić przez dodatkowy narzut na kompilaję kodu.
W jednym z ogólnie dostępnych testów projektu opartego o framework Laravel i opisanego w artykule na portalu medium.com możemy się dowiedzieć, że jego uruchomienie w wersji PHP 8 z JIT daje tylko nieznaczne przyspieszenie:
PHP 7.3: 131.37 req/s
PHP 8.0 + JIT: 133.57 req/s
Gołym okiem można zauważyć, że w zastosowaniach webowych przyspieszenie będzie ledwo dostrzegalne.
Ta teza zdaje się także być potwierdzona w ogólnodostępnym benchmarku, który zaprezentowano przez PHP Group w ramach wydania wersji PHP 8.
Jak widać, aplikacje przeznaczone do zastosowań webowych: WordPress, MediaWiki czy Symfony demo uzyskują wyniki zbliżone, nieznacznie większe lub nawet niższe (w przypadku użycia function JIT), aniżeli w przypadku uruchomienia w PHP 8 bez użycia JIT.
W pozostałych przypadkach sytuacja jest jednak zgoła odmienna. Dla benchmarków syntetycznych, czy przy zadaniach takich jak generowanie fraktali, wydajność może być nawet 3 razy większa. W innych wypadkach, jak np. długo wykonujących się aplikacji to zysk od 1,5 - 2 razy na wydajności. Jest to faktycznie przełom dający nowe możliwości w zastosowaniu języka poza typowymi aplikacjami webowymi.
Zastosowanie
Wprowadzenie kompilatora JIT to krok w kierunku otwarcia języka na nowe możliwości i spowodowaniu, że na przykład PHP będzie idealny dla startupów. JIT zdecydowanie poprawia wydajność kodu w aplikacjach wykonujących intesywne obliczenia na procesorze. Mówimy tu o zastosowaniu PHP do zupełnie nowych celów, takich jak np.: uczenie maszynowe, skomplikowane obliczenia matematyczne, czy też przetwarzanie/modelowanie obrazów 2D/3D.
W sieci od dłuższego czasu krąży wideo z proof of concept stworzonego przez jednego z czołowych developerów PHP (Zeev Suraski), w którym pokazuje, jak JIT może poprawić wydajność (w jego przypadku do generowania fraktali w czasie rzeczywistym).
Według różnego typu benchmarków ogólnodostępnych w internecie (np. https://www.phoronix.com/scan.php?page=article&item=php8-jit-june&num=2), widać że w zależności od zastosowania, JIT wnosi realne przyspieszenie: od kilku do kilkunastu a nawet kilkudziesięciu procent.
Wady i zalety
Wprowadzenie JIT to krok w kierunku zmiany PHP na język ogólnego zastosowania. Możliwość bezpośredniego kompilowania kodu PHP da możliwość rozluźnienia zależności od języka C i realnym stanie się rozszerzanie go bez szczególnego przełożenia na wydajność.
Niezaprzeczalnie też JIT podnosi w większym lub mniejszym stopniu wydajność aplikacji.
Z drugiej strony JIT na obecnym poziomie nie jest w stanie w sposób wyraźny podnieść wydajności aplikacji webowych, a w szczególnych wypadkach może wpłynąć nawet na jej spadek. Sposób w jaki działa może wpłynać także na process developmentu, utrudniając m.in. process debugowania kodu (znane są problemy np. z narzędziem xDebug). JIT będzie wymagał także od developerów dodatkowej wiedzy odnośnie jego konfiguracji.
Podsumowanie
W Droptica, w której zajmujemy się dostarczaniem usług drupalowych, wsparciem Drupala i gdzie w naszym zespole jest wielu programistów PHP wiemy, że choć JIT w świecie to niezupełnie nowość (prekursorem był Facebook i ich HHVM), to dla szerszego grona stanie się on wkrótce natywną częścią języka wraz z wydaniem wersji PHP 8. Dzięki JIT podniesie się wydajność wykonywania kodu i rozszerzą się możliwości wykorzystania samego języka. Myślę, że w kolejnych latach powinniśmy baczniej przyglądać się pracom nad jego rozwojem, które mogą przynieść jeszcze lepsze rezultaty, a zwłaszcza w kontekście zastosowania w aplikacjach webowych.