Тестування продуктивності
Phasis приблизно у сто разів повільніший за V8 під час обробки JavaScript, обмеженої диспетчеризацією. Така межа зумовлена самою природою інтерпретатора на базі PHP порівняно з нативним JIT-компілятором. Розробнику, який вбудовує рушій, важливо не те, «як він порівнюється з V8», а те, «чи достатньо він швидкий для моїх завдань». Для більшості сценаріїв вбудовування відповідь — так.
Коли обмеження не має значення
- Шаблонізація — рендеринг типової сторінки у стилі SSR значно швидше ніж за 10 ms.
- Правила перевірки — перевірка форм за допомогою наданого користувачем JS завершується за мікросекунди.
- Перетворення даних —
array.map(transform)для сотень елементів завершується за десятки мілісекунд. - Середовища запуску ізольованого коду — для коротких скриптів користувача з обмеженням у 1 second на боці хоста запасу продуктивності більш ніж достатньо.
Коли обмеження має значення
- Обчислювально інтенсивні внутрішні цикли — обробка зображень, редукція великих масивів, симуляції. Перенесіть обчислення до нативної бібліотеки PHP і відкрийте її як функцію хоста.
- Анімація в реальному часі — усе, що має працювати з частотою 60 fps. Таку JS слід виконувати на клієнті (у браузері чи нативному застосунку), а не у вбудованому рушії.
- Повторні реалізації JS обчислювально дорогих алгоритмів — криптографії, стиснення, синтаксичного аналізу. У PHP є нативні аналоги, які у 1000× швидші; використовуйте їх.
Правильна модель: Phasis — це рівень зв’язування для логіки у стилі JS, вбудованої в застосунки у стилі PHP. Обчисленнями займається хост PHP, а шар JS відповідає за конфігурацію, розгалуження та невеликі перетворення.
Що робить рушій швидким (порівняно з наївним інтерпретатором дерева)
- Віртуальна машина байткоду — більшість гарячих операцій диспетчеризації проходить через перемикач кодів операцій
VM::execute, а не через AST. - Власний стек викликів — кожен виклик JS зберігає стан у
VM::executeі кладе кадр виклику до пулу; для кожного виклику JS не створюється новий кадр PHP. Докладніше див. Віртуальна машина байткоду § Власний стек викликів. - Вбудовані кеші — мономорфний кеш для кожного PC дає змогу читанню методу прототипу в екземплярі (форма
c.method()) оминати обхід ланцюжка прототипів. - Генератори зі знімком кадру — прості тіла генераторів призупиняються, зберігаючи стан диспетчеризації в об’єкті купи, замість переходу до Fiber PHP.
- Транспілятор JsToPhp — тіла з числовими операціями чи багатьма локальними змінними (зокрема читання
obj.method(),new Ctor()іthis.member) компілюються в замикання PHP, які трасувальний JIT-компілятор PHP 8.5 оптимізує до машинного коду. - Кеш JSON.stringify —
JSON.stringify(o)кешує результат у вхідному об’єкті з інвалідацією за транзитивною версією, пришвидшуючи ідіому глибокого клонуванняJSON.parse(JSON.stringify(o)).
Вимірювання власного навантаження
php bench/run.php
Ця команда запускає набір мікротестів продуктивності з bench/microbench.js — кількох синтетичних навантажень (арифметика в циклі, рекурсивні функції, створення об’єктів, виклики методів прототипу, додавання до масиву, обхідне перетворення JSON тощо) — і виводить медіанний реальний час для кожного тесту. Це корисна перевірка здорового глузду; поточні результати фіксуються в BENCH.md під час кожного запуску тестів продуктивності в CI.
Для власного коду підставте будь-який скрипт:
php -d xdebug.mode=off bench/run.php path/to/your-script.js
Щоб профілювати на рівні PHP, скористайтеся профілювальником Xdebug — він працює з Phasis так само, як із будь-яким іншим застосунком PHP. Запустіть скрипт у xdebug.mode=profile і перегляньте вивід cachegrind, щоб побачити, які класи Phasis\ створюють найбільше навантаження.
План розвитку
Що найбільше скоротило б розрив із V8 — у приблизному порядку складності реалізації:
- Поліморфні вбудовані кеші — один сайт виклику
LOAD_MEMBERзможе зберігати 2-4 кешовані форми, перш ніж перейти до повільної диспетчеризації. Це допоможе коду, який обробляє різнорідні об’єкти-приймачі. yield*і try-with-yield у шляху генераторів зі знімком кадру — наразі ці форми спрямовуються до інтерпретатора дерева на базі Fiber. Перш ніж їх можна буде безпечно зберігати у знімку, потрібна явна обробка як автомата станів (пересилання викликів делегованому ітератору, збереження стека обробників під час призупинення).- Асинхронні генератори та
awaitу знімках — специфікація поєднує призупинення генератора з чергою мікрозадач. Коли тіло байткоду підтримуватиме іyield, іawait, асинхронні генератори також можна буде зберігати у знімках. - Виведення типів параметрів у JsToPhp — параметрам функцій типово призначається числовий тип, тому
function (obj) { obj.method() }змушує компілятор відмовитися від оптимізації. Виведення типу об’єктних параметрів на основі їх використання в тілі функції дасть змогу викликати методи для аргументів без потреби створювати об’єкт-приймач локально черезnew. - Безпосередня генерація opcache — обійти етап
eval()у JsToPhp, генеруючи опкоди у форматі opcache напряму. Це найскладніший пункт; решту можна реалізувати в межах поточної архітектури.
Жодне з цього не є необхідним для сценаріїв вбудовування, на які орієнтується Phasis.