PHP 8: Как включить JIT

PHP 8: Как включить JIT

пробуем вместе настроить JIT в PHP

PHP 8 добавляет к ядру PHP JIT-компилятор, который может значительно повысить производительность

JIT-компиляторы очень сложны, потому что для того, чтобы добиться от них хорошей производительности, обычно нужно выбирать, какие именно части промежуточного языка должны компилироваться в машинный код, а какие нет. Преобразование в машинный код не всегда происходит быстрее, в зависимости от конкретных деталей кода и рассматриваемого языка. Кроме того, процесс преобразования упрощенного кода в машинный код может занять больше времени, чем просто однократный запуск упрощенного кода и мгновенного завершение его работы.

По этой причине большинство JIT-компиляторов анализируют код во время его выполнения, чтобы определить, какие части дают наилучшую отдачу, а затем компилируют только эти биты. Теоретически, отличный результат состоит в том, что программа буквально становится быстрее по мере ее выполнения, а JIT-компилятор в виртуальной машине узнает, какие части кода нужно оптимизировать.

Java была первым широко распространенным языком, включившим JIT в свою виртуальную машину. Большинство основных движков Javascript теперь тоже. И, начиная с PHP 8.0, к этому списку присоединился и PHP.

PHP JIT

JIT в PHP появилась давно. На самом деле он находился в разработке в течение нескольких лет и почти был выпущен в PHP 7.4. Работа по созданию JIT в PHP была тем импульсом, который привел к серьезному переписыванию движка, что дало версии 7.0 значительный прирост производительности.

PHP JIT построен как расширение кеша опкодов. Это означает, что его можно включать и отключать либо при сборке самого PHP, либо во время выполнения через php.ini.

Честно говоря, настройка JIT - один из самых запутанных способов настройки расширения PHP, который я когда-либо видел. К счастью, есть несколько сокращений конфигурации, которые упрощают настройку. Тем не менее, будет очень полезным знать как настраивать JIT, так что приступим.

Кратко для системных администраторов

В PHP 8.0 JIT как бы включен по умолчанию, но и выключен. Требуется только минимальная конфигурация php.ini, подобная этой:


opcache.enable=1
opcache.enable_cli=1
opcache.jit_buffer_size=128M

 

Как настроить JIT в PHP 8

Прежде всего, JIT будет работать, только если включен opcache, хоть по умолчанию в PHP он и включен, вы все равно должны убедиться, что в вашем php.ini файле установлено значение opcache.enable = 1. Включение самого JIT осуществляется путем установки в php.ini параметра opcache.jit_buffer_size в ненулевое значение. Это контролирует, сколько места в памяти JIT может заполнить оптимизированным машинным кодом. Однако больше не всегда лучше, поскольку JIT также может тратить время на компиляцию кода, который на самом деле не выигрывает от компиляции.

Обратите внимание, что если вы запускаете PHP через командную строку, вы также можете передать эти параметры через флаг -d вместо того, чтобы добавлять их в php.ini:


php -d opcache.enable=1 -d opcache.jit_buffer_size=128M

Если эта директива отсутствует, то значение по умолчанию будет равно 0, и JIT не будет работать.

Так же, если вы тестируете JIT в сценарии CLI, вы должны использовать opcache.enable_cli для включения opcache:


php -d opcache.enable_cli=1 -d opcache.jit_buffer_size=128M

Разница между opcache.enable и opcache.enable_cli заключается в том, что первый вариант следует использовать тогда, когда вы, например, используете встроенный PHP-сервер. Если вы на самом деле запускаете сценарий CLI, вам понадобится opcache.enable_cli.

Прежде чем продолжить, давайте убедимся, что JIT действительно работает, создадим PHP-скрипт, доступный через браузер или CLI (в зависимости от того, где вы тестируете JIT), и посмотрим на вывод opcache_get_status(): 


var_dump(opcache_get_status()['jit']);

Результат будет примерно таким:


array:7 [
"enabled" => true
"on" => true
"kind" => 5
"opt_level" => 4
"opt_flags" => 6
"buffer_size" => 4080
"buffer_free" => 0
]

 Если enabled и on стоят в true, то все работает как надо!

Далее есть несколько способов настроить JIT (и здесь мы попадем в круговорот беспорядка конфигурации). Вы можете настроить, когда должен работать JIT, с какой степенью он должен пытаться оптимизировать и т.д. Все эти параметры настраиваются с помощью всего одной записи конфигурации(!): opcache.jit. Выглядит это так:


opcache.enable=1 
opcache.jit=1255

В RFC можно почитать что означает каждое из чисел. Имейте в виду: это не битовая маска, каждое число просто представляет собой различный вариант конфигурации. Итак, RFC дает нам следующие варианты:


O - Уровень оптимизации 

0не JIT
1минимальный JIT (вызов стандартных обработчиков ВМ)
2выборочное встраивание обработчика ВМ
3оптимизированный JIT на основе статического вывода типов отдельной функции
4оптимизированный JIT на основе статического вывода типов и дерева вызовов
5оптимизированная JIT на основе статического вывода типов и анализа внутренних процедур


T - триггер JIT

0JIT все функции при первой загрузке скрипта
1Функция JIT при первом выполнении
2Профилировать по первому запросу и компилировать горячие функции по второму запросу
3Профилировать на лету и компилировать горячие функции
4Компиляция функций с тегом @jit в комментариях к документации (больше не используется)
5Трассировка JIT


R - распределение регистров

0не выполнять распределение регистров
1использовать локальный распределитель регистров liner-scan
2использовать глобальный распределитель регистров liner-scan


C - флаги оптимизации CPU

0нет
1включить генерацию инструкций AVX
Одно небольшое уточнение: Перечислять эти параметры нужно в обратном порядке, поэтому первая цифра представляет C значение, вторая - R, третья - T и четвертая - O. Почему просто не были добавлены четыре записи конфигурации, я не понимаю, вероятно, чтобы ускорить настройку JIT, может быть...

В любом случае, рекомендуется устанавливать значение 1255 в качестве наиболее оптимального, тогда будет выполняться максимальный "джитинг", используя JIT трассировки, глобальный распределитель регистров liner-scan - что бы это ни было, черт возьми, и позволяет генерировать инструкции AVX.

Итак, ваши ini-настройки (или флаги -d ) должны иметь следующие значения:


opcache.enable=1 
opcache.jit_buffer_size=128M
opcache.jit=1255

Имейте в виду, что необязательно настраивать opcache.jit. JIT будет использовать значение по умолчанию, если это свойство не указано. А какой по умолчанию, спросите вы? А вот какое opcache.jit=tracing.

Что еще за трассировка, спросите вы, это совсем не та странная структура, похожая на битовую маску, которую мы видели ранее. И вы будете правы: после того, как был принят исходный RFC, контрибьюторы ядра PHP осознали, что параметры-шифры, подобные битовой маске, не очень удобны для пользователя, поэтому они добавили два человечески понятные псевдонима, которые транслируются в ту самую битовую маску под капотом. Есть opcache.jit=tracing и opcache.jit=function.

Разница между ними заключается в том, что при параметре функции JIT пытается оптимизировать код только в рамках одной функции, в то время как JIT трассировка может просматривать всю трассировку стека, чтобы идентифицировать и оптимизировать горячий код. PHP Internals рекомендует использовать JIT-трассировку, потому что она почти всегда дает наилучшие результаты.

Таким образом, единственно что от вас потребуется, чтобы включить JIT с его оптимальной конфигурацией - это установка opcache.jit_buffer_size, но если вы хотите установить конфигурацию явно, включение параметра opcache.jit будет не такой уж плохой идеей:


opcache.enable=1 
opcache.jit_buffer_size=128M
opcache.jit=tracing


Отладка JIT (opcache.jit_debug)

PHP JIT обеспечивает способ выдачи отладочной информации JIT путем установки конфигурации INI. Когда установлен определенный флаг, он выводит код сборки для дальнейшей проверки.


opcache.jit_debug=1

Директива opcache.jit_debug принимает значение битовой маски для включения определенных функций. В настоящее время он принимает значение в диапазоне от 1 (0b1) до 20 двоичных разрядов. Значение 1048576 представляет максимальный уровень вывода отладки. 


php -d opcache.jit_debug=1 test.php


TRACE-1$/test.php$7: ; (unknown)
sub $0x10, %rsp
mov $EG(jit_trace_num), %rax
mov $0x1, (%rax)
.L1:
cmp $0x4, 0x58(%r14)
jnz jit$$trace_exit_0
cmp $0x4e20, 0x50(%r14)
...

JIT поддерживает несколько других параметров конфигурации, чтобы настроить, сколько вызовов функций делает ее «горячей» функцией, которая затем компилируется JIT, и порог для определения того, какие функции следует "JIT'ировать", на основе процента общих вызовов функций.

Полный список см. В разделе Конфигурация Opcache.  

Улучшит ли JIT производительность?

Короче, «это зависит от обстоятельств». Для веб-приложений может и не очень, но вот для PHP как экосистемы даже может чрезмерно.

PHP, по замыслу, обычно работает в конфигурации без совместного использования ресурсов. После обработки каждого запроса приложение полностью закрывается. Это дает JIT очень мало времени на анализ и оптимизацию кода, тем более, что большая часть кода в типичном веб-запросе выполняется только один раз, поскольку запрос обрабатывается линейно. Кроме того, большей частью этих приложений часто является ввод-вывод (в основном, общение с базой данных), и JIT вообще не может с этим помочь. Результаты тестов, которые были опубликованы на данный момент, показывают, что JIT предлагает лишь незначительное повышение производительности в типичных приложениях PHP, запускаемых через PHP-FPM.

JIT потенциально может быть действительно полезен в тех случаях, где PHP сегодня совсем не рассматривается. Реальные преимущества будут видны в работе постоянных демонов, парсерах, машинном обучении и других длительных процессах, интенсивно использующих CPU.

PHP-Parser - это «анализатор PHP, написанный на PHP». Он от того же Никиты Попова, и используется многими инструментами статического анализа, представленными сегодня на рынке, такими как, например, PHPStan и пр.  Никита сообщил, что PHP-Parser в некоторых случаях работает в два раза быстрее с новым движком JIT.

Асинхронные приложения, написанные на React PHP или AmPHP , вероятно, также увидят заметное улучшение, хотя и не настолько, как правило, они выполняют много операций ввода-вывода (где JIT бесполезен).

Может быть для вас будет шоком узнать, что для PHP доступны библиотеки машинного обучения, такие как Rubix ML или PHP-ML. Они не так широко используются, как их собраты на Python, отчасти потому, что при интерпретации они, как правило, медленнее, чем библиотеки C с красивыми оболочками Python. Однако с JIT эти задачи с интенсивным использованием CPU могут оказаться такими же быстрыми или, возможно, даже более быстрыми, чем те, которые доступны на других языках.

PHP больше не является "просто самым быстрым из основных языков веб-сценариев". Теперь это жизнеспособный высокопроизводительный язык общей обработки данных, с передачей постоянных воркеров, машинного обучения и других высокопроизводительных задач, в руки миллионов PHP-разработчиков по всему миру.

В первую очередь мы должны поблагодарить Дмитрия Стогова и Зеева Сураски за многолетние усилия по реализации этого RFC .

Сергей Мухин

Веб-разработчик со стажем программирования более 6 лет, постоянно учусь, люблю делать новые проекты.

Есть вопросы?

Я почти всегда в режиме онлайн

Связаться со мной