Przejdź do treści

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::execute i 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() i this.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 klonowania JSON.parse(JSON.stringify(o)).

Pomiar własnego obciążenia

Powłoka
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:

Powłoka
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_MEMBER przechowywać 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 await w migawkach — specyfikacja łączy zawieszenie generatora z kolejką mikrozadań. Gdy treść bajtkodu będzie obsługiwać zarówno yield, jak i await, 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.