Testy wydajnościowe
Phasis jest mniej więcej dwa rzędy wielkości wolniejsze niż V8 przy ograniczeniu przepustowości przez dyspozycję JavaScript. Ten limit wynika z nieuniknionego kosztu interpretera działającego w środowisku PHP w porównaniu z natywnym kompilatorem JIT. Pytanie dla osoby osadzającej nie brzmi „jak wypada w porównaniu z V8”, lecz „czy jest wystarczająco szybki do tego, co robię”. W przypadku większości obciążeń związanych z osadzaniem odpowiedź brzmi: tak.
Kiedy ograniczenie nie ma znaczenia
- Szablony — renderowanie typowej strony w stylu SSR zajmuje znacznie mniej niż 10 ms.
- Reguły walidacji — dostarczony przez użytkownika JS do walidacji formularza wykonuje się w mikrosekundach.
- Transformacje danych —
array.map(transform)dla setek elementów kończy się w dziesiątkach milisekund. - Uruchamianie w piaskownicy — krótkie skrypty użytkownika z limitem hosta równym 1 sekundzie pozostawiają duży zapas czasu.
Kiedy ograniczenie ma znaczenie
- Intensywne obliczeniowo pętle wewnętrzne — przetwarzanie obrazów, redukcje dużych tablic, symulacje. Przenieś obliczenia do natywnej biblioteki PHP i udostępnij je jako funkcję hosta host.
- Animacje w czasie rzeczywistym — wszystko, co wymaga 60 fps. Taka logika JS powinna działać po stronie klienta (w przeglądarce lub aplikacji natywnej), a nie w osadzonym silniku.
- Reimplementacje JS algorytmów wymagających dużych obliczeń — kryptografia, kompresja, parsowanie. PHP ma natywne odpowiedniki działające 1000× szybciej; użyj ich.
Przyjmij następujący model: Phasis to warstwa łącząca logikę w stylu JS osadzoną w aplikacjach w stylu PHP. Host PHP obsługuje obliczenia; warstwa JS — konfigurację, rozgałęzienia i niewielkie transformacje.
Co sprawia, że silnik jest szybki (w porównaniu z naiwnym walkerem drzewa)
- Maszyna wirtualna kodu bajtowego — większość gorących wywołań przechodzi przez przełącznik kodów operacji
VM::execute, a nie przez AST. - Własny stos wywołań — każde wywołanie JS zachowuje stan w
VM::executei zapisuje ramkę wywołującego w puli; dla każdego wywołania JS nie powstaje nowa ramka PHP. Zob. Własny stos wywołań maszyny wirtualnej kodu bajtowego. - Pamięci podręczne inline — monomorficzna pamięć podręczna dla każdego PC pozwala uniknąć przechodzenia łańcucha prototypów przy odczycie metody prototypu z instancji (wzorzec
c.method()). - Generatory z migawką ramki — proste ciała generatorów zawieszają działanie, zapisując stan dyspozytora w obiekcie na stercie, zamiast przechodzić przez Fiber PHP.
- Transpiler JsToPhp — ciała intensywnie korzystające z liczb lub zmiennych lokalnych (w tym odczyty
obj.method(),new Ctor()ithis.member) są kompilowane do domknięć PHP, które śledzący JIT w wersji PHP 8.5 obniża do natywnego kodu maszynowego. - Pamięć podręczna JSON.stringify —
JSON.stringify(o)zapisuje wynik w obiekcie wejściowym i unieważnia go pośrednio na podstawie wersji, przyspieszając idiom głębokiego klonowaniaJSON.parse(JSON.stringify(o)).
Pomiar własnego obciążenia
php bench/run.php
Uruchamia to zestaw mikrotestów wydajnościowych z pakietu bench/microbench.js — kilka syntetycznych obciążeń (arytmetyka w pętli, funkcje rekurencyjne, tworzenie obiektów, wywołania metod prototypu, dodawanie elementów do tablicy, cykl JSON i inne) — i wypisuje medianę czasu rzeczywistego dla każdego testu. Przydatne jako szybka kontrola; aktualne wyniki są zapisywane w BENCH.md przy każdym uruchomieniu testów wydajnościowych CI.
Aby sprawdzić własny kod, podmień skrypt:
php -d xdebug.mode=off bench/run.php path/to/your-script.js
Do profilowania na poziomie PHP profiler Xdebug działa z Phasis tak samo jak z każdą inną aplikacją PHP. Uruchom skrypt pod xdebug.mode=profile i sprawdź dane wyjściowe cachegrind, aby zobaczyć, które klasy Phasis\ dominują.
Plan rozwoju
Co najbardziej zmniejszyłoby różnicę w V8, w przybliżonej kolejności od najłatwiejszych do najtrudniejszych:
- Polimorficzne pamięci podręczne inline — pozwalają pojedynczemu miejscu wywołania
LOAD_MEMBERprzechowywać 2–4 kształty w pamięci podręcznej, zanim nastąpi przejście do wolnego mechanizmu dispatch. Pomaga to w przypadku kodu przetwarzającego niejednorodne odbiorniki. yield*i try-with-yield w ścieżce generatora z migawką ramki — te kształty są obecnie obsługiwane przez oparty na Fiber walker drzewa. Każdy wymaga jawnej obsługi jako maszyna stanów (przekazywania delegowanego iteratora, zachowania stosu obsługi między zawieszeniami), zanim będzie bezpieczny dla migawek.- Generatory asynchroniczne i
awaitw migawkach — specyfikacja łączy zawieszenie generatora z kolejką mikrozadań. Gdy treść bajtkodu będzie obsługiwać zarównoyield, jak iawait, generatory asynchroniczne również będą obsługiwać migawki. - Wnioskowanie o typach parametrów w JsToPhp — parametry funkcji domyślnie mają typ numeryczny, więc
function (obj) { obj.method() }powoduje przerwanie kompilacji. Wnioskowanie o typie obiektowym parametrów na podstawie sposobu ich użycia w treści funkcji umożliwiłoby wywoływanie metod na argumentach funkcji bez konieczności lokalnego tworzenia odbiornika za pomocąnew. - Bezpośrednie emitowanie opcache — pominięcie kroku
eval()w JsToPhp przez bezpośrednie zapisywanie kodów operacji w formacie opcache. To najtrudniejszy element; pozostałe można zrealizować w ramach istniejącej architektury.
Żadne z tych rozwiązań nie jest konieczne w przypadku obciążeń związanych z osadzaniem, które obsługuje Phasis.