Перейти до вмісту

Тестування продуктивності

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.