Перейти к содержимому

Бенчмарки

Phasis примерно в 100 раз медленнее V8 при нагрузках, ограниченных диспетчеризацией JavaScript. Такова неизбежная цена интерпретатора на PHP по сравнению с нативным JIT-компилятором. Для встраивания важно не то, «как он выглядит на фоне V8», а то, «достаточно ли он быстр для моей задачи». Для большинства сценариев встраивания — да.

Когда это ограничение несущественно

  • Шаблонизация — серверный рендеринг типичной страницы в стиле SSR занимает значительно меньше 10 ms.
  • Правила проверки — проверка предоставленного пользователем JS для форм занимает микросекунды.
  • Преобразование данных — array.map(transform) для сотен элементов выполняется за десятки миллисекунд.
  • Изолированный запуск — короткие скрипты пользователя с ограничением хоста в 1 секунду оставляют большой запас.

Когда это ограничение существенно

  • Вычислительно затратные внутренние циклы — обработка изображений, свертки больших массивов, симуляции. Перенесите вычисления в нативную библиотеку PHP и предоставьте ее как функцию хоста.
  • Анимация в реальном времени — все, что должно обеспечивать 60 fps. Такую JS лучше реализовать на клиенте (в браузере или нативном приложении), а не во встроенном движке.
  • Реализации на JS алгоритмов с высокой вычислительной сложностью — криптография, сжатие, разбор данных. У PHP есть нативные аналоги, которые быстрее в 1000 раз; используйте их.

Правильная модель: Phasis — это связующий слой для логики в стиле JS, встроенной в приложения в стиле PHP. Хост на PHP выполняет вычисления, а слой JS отвечает за настройку, ветвление и простые преобразования.

За счет чего движок быстрее (по сравнению с наивным обходчиком дерева)

  • Виртуальная машина байткода — большая часть горячей диспетчеризации проходит через переключатель кодов операций VM::execute, а не через AST.
  • Пользовательский стек вызовов — состояние каждого вызова JS сохраняется в VM::execute, а кадр вызывающего кода помещается в пул; для каждого вызова JS не создается новый кадр PHP. См. Виртуальная машина байткода § Пользовательский стек вызовов.
  • Встроенные кэши — мономорфный кэш для каждой точки выполнения позволяет пропустить обход цепочки прототипов при чтении метода прототипа экземпляра (шаблон c.method()).
  • Генераторы со снимком кадра — простые тела генераторов приостанавливаются, сохраняя состояние диспетчеризации в объекте кучи вместо перехода через PHP Fiber.
  • Транспилятор 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.