php 8 jit как включить

Понимаем JIT в PHP 8

Перевод статьи подготовлен в преддверии старта курса «Backend-разработчик на PHP»

Компилятор Just In Time в PHP 8 реализован как часть расширения Opcache и призван компилировать операционный код в инструкции процессора в рантайме.

Это означает, что с JIT некоторые операционные коды не должны интерпретироваться Zend VM, такие инструкции будут выполняться непосредственно как инструкции уровня процессора.

JIT в PHP 8

Одной из наиболее комментируемых фич PHP 8 является компилятор Just In Time (JIT). Он на слуху во множестве блогов и сообществ — вокруг него много шума, но пока я нашел не очень много подробностей о работе JIT в деталях.

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

Упрощая вещи: когда JIT работает должным образом, ваш код не будет выполняться через Zend VM, вместо этого он будет выполняться непосредственно как набор инструкций уровня процессора.

Но чтобы лучше это понять, нам нужно подумать о том, как php работает внутри. Это не очень сложно, но требует некоторого введения.

Я уже писал статью с кратким обзором того, как работает php. Если вам покажется, что эта статья становится чересчур сложной, просто прочитайте ее предшественницу и возвращайтесь. Это должно немного облегчить ситуацию.

Как выполняется PHP-код?

Мы все знаем, что php — интерпретируемый язык. Но что это на самом деле означает?

Всякий раз, когда вы хотите выполнить код PHP, будь то фрагмент или целое веб-приложение, вам придется пройти через интерпретатор php. Наиболее часто используемые из них — PHP FPM и интерпретатор CLI. Их работа очень проста: получить код php, интерпретировать его и выдать обратно результат.

Это обычная картина для каждого интерпретируемого языка. Некоторые шаги могут варьироваться, но общая идея та же самая. В PHP это происходит так:

Чтобы сделать это немного нагляднее, я составил диаграмму:

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить
Упрощенная схема процесса интерпретации PHP.

Достаточно прямолинейно, как вы можете заметить. Но здесь есть и узкое место: какой смысл лексировать и парсить код каждый раз, когда вы его выполняете, если ваш php-код может даже не меняется так часто?

В конце концов, нас интересуют только опкоды, верно? Правильно! Вот зачем существует расширение Opcache.

Расширение Opcache

Расширение Opcache поставляется с PHP, и, как правило, нет особых причин его деактивировать. Если вы используете PHP, вам, вероятно, следует включить Opcache.

Что он делает, так это добавляет слой оперативного общего кэша для опкодов. Его задача состоит в том, чтобы извлекать опкоды, недавно сгенерированные из нашего AST, и кэшировать их, чтобы при дальнейших выполнениях можно было легко пропустить фазы лексирования и синтаксического анализа.

Вот схема того же процесса с учетом расширения Opcache:

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить
Поток интерпретации PHP с Opcache. Если файл уже был проанализирован, php извлекает для него кэшированный операционный код, а не анализирует его заново.

Это просто завораживает, как красиво пропускаются шаги лексирования, синтаксического анализа и компиляции.

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

Вы можете начать задумываться, а куда сюда можно прилепить JIT, верно?! По крайней мере я на это надеюсь, именно поэтому я и пишу эту статью…

Что делает компилятор Just In Time?

Прослушав объяснение Зива в эпизоде ​​подкастов PHP и JIT от PHP Internals News, мне удалось получить некоторое представление о том, что на самом деле должен делать JIT…

Если Opcache позволяет быстрее получать операционный код, чтобы он мог переходить непосредственно к Zend VM, JIT предназначить заставить его работать вообще без Zend VM.

Zend VM — это программа, написанная на C, которая действует как слой между операционным кодом и самим процессором. JIT генерирует скомпилированный код во время выполнения, поэтому php может пропустить Zend VM и перейти непосредственно к процессору. Теоретически мы должны выиграть в производительности от этого.

Поначалу это звучало странно, потому что для компиляции машинного кода вам нужно написать очень специфическую реализацию для каждого типа архитектуры. Но на самом деле это вполне реально.

Реализация JIT в PHP использует библиотеку DynASM (Dynamic Assembler), которая отображает набор команд ЦП в конкретном формате в код сборки для многих различных типов ЦП. Таким образом, компилятор Just In Time преобразует операционный код в машинный код для конкретной архитектуры, используя DynASM.

Хотя одна мысль все-таки не давала мне покоя…

Если предварительная загрузка способна парсить php-код в операционный перед выполнением, а DynASM может компилировать операционный код в машинный (компиляция Just In Time), почему мы, черт возьми, не компилируем PHP сразу же на месте, используя Ahead of Time компиляцию?!

Одна из мыслей, на которые меня натолкнул эпизода подкаста, заключалась в том, что PHP слабо типизирован, то есть часто PHP не знает, какой тип имеет переменная, пока Zend VM не попытается выполнить определенный опкод.

Это можно понять, посмотрев на тип объединения zend_value, который имеет много указателей на различные представления типов для переменной. Всякий раз, когда виртуальная машина Zend пытается извлечь значение из zend_value, она использует макросы, подобные ZSTR_VAL, которые пытаются получить доступ к указателю строки из объединения значений.

Источник

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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

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

Источник

Понимаем JIT в PHP 8

Перевод статьи подготовлен в преддверии старта курса «Backend-разработчик на PHP»

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

Компилятор Just In Time в PHP 8 реализован как часть расширения Opcache и призван компилировать операционный код в инструкции процессора в рантайме.

Это означает, что с JIT некоторые операционные коды не должны интерпретироваться Zend VM, такие инструкции будут выполняться непосредственно как инструкции уровня процессора.

JIT в PHP 8

Одной из наиболее комментируемых фич PHP 8 является компилятор Just In Time (JIT). Он на слуху во множестве блогов и сообществ — вокруг него много шума, но пока я нашел не очень много подробностей о работе JIT в деталях.

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

Упрощая вещи: когда JIT работает должным образом, ваш код не будет выполняться через Zend VM, вместо этого он будет выполняться непосредственно как набор инструкций уровня процессора.

Но чтобы лучше это понять, нам нужно подумать о том, как php работает внутри. Это не очень сложно, но требует некоторого введения.

Я уже писал статью с кратким обзором того, как работает php. Если вам покажется, что эта статья становится чересчур сложной, просто прочитайте ее предшественницу и возвращайтесь. Это должно немного облегчить ситуацию.

Как выполняется PHP-код?

Мы все знаем, что php — интерпретируемый язык. Но что это на самом деле означает?

Всякий раз, когда вы хотите выполнить код PHP, будь то фрагмент или целое веб-приложение, вам придется пройти через интерпретатор php. Наиболее часто используемые из них — PHP FPM и интерпретатор CLI. Их работа очень проста: получить код php, интерпретировать его и выдать обратно результат.

Это обычная картина для каждого интерпретируемого языка. Некоторые шаги могут варьироваться, но общая идея та же самая. В PHP это происходит так:

Чтобы сделать это немного нагляднее, я составил диаграмму:

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить
Упрощенная схема процесса интерпретации PHP.

Достаточно прямолинейно, как вы можете заметить. Но здесь есть и узкое место: какой смысл лексировать и парсить код каждый раз, когда вы его выполняете, если ваш php-код может даже не меняется так часто?

В конце концов, нас интересуют только опкоды, верно? Правильно! Вот зачем существует расширение Opcache.

Расширение Opcache

Расширение Opcache поставляется с PHP, и, как правило, нет особых причин его деактивировать. Если вы используете PHP, вам, вероятно, следует включить Opcache.

Что он делает, так это добавляет слой оперативного общего кэша для опкодов. Его задача состоит в том, чтобы извлекать опкоды, недавно сгенерированные из нашего AST, и кэшировать их, чтобы при дальнейших выполнениях можно было легко пропустить фазы лексирования и синтаксического анализа.

Вот схема того же процесса с учетом расширения Opcache:

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить
Поток интерпретации PHP с Opcache. Если файл уже был проанализирован, php извлекает для него кэшированный операционный код, а не анализирует его заново.

Это просто завораживает, как красиво пропускаются шаги лексирования, синтаксического анализа и компиляции.

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

Вы можете начать задумываться, а куда сюда можно прилепить JIT, верно?! По крайней мере я на это надеюсь, именно поэтому я и пишу эту статью…

Что делает компилятор Just In Time?

Прослушав объяснение Зива в эпизоде ​​подкастов PHP и JIT от PHP Internals News, мне удалось получить некоторое представление о том, что на самом деле должен делать JIT…

Если Opcache позволяет быстрее получать операционный код, чтобы он мог переходить непосредственно к Zend VM, JIT предназначить заставить его работать вообще без Zend VM.

Zend VM — это программа, написанная на C, которая действует как слой между операционным кодом и самим процессором. JIT генерирует скомпилированный код во время выполнения, поэтому php может пропустить Zend VM и перейти непосредственно к процессору. Теоретически мы должны выиграть в производительности от этого.

Поначалу это звучало странно, потому что для компиляции машинного кода вам нужно написать очень специфическую реализацию для каждого типа архитектуры. Но на самом деле это вполне реально.

Реализация JIT в PHP использует библиотеку DynASM (Dynamic Assembler), которая отображает набор команд ЦП в конкретном формате в код сборки для многих различных типов ЦП. Таким образом, компилятор Just In Time преобразует операционный код в машинный код для конкретной архитектуры, используя DynASM.

Хотя одна мысль все-таки не давала мне покоя…

Если предварительная загрузка способна парсить php-код в операционный перед выполнением, а DynASM может компилировать операционный код в машинный (компиляция Just In Time), почему мы, черт возьми, не компилируем PHP сразу же на месте, используя Ahead of Time компиляцию?!

Одна из мыслей, на которые меня натолкнул эпизода подкаста, заключалась в том, что PHP слабо типизирован, то есть часто PHP не знает, какой тип имеет переменная, пока Zend VM не попытается выполнить определенный опкод.

Это можно понять, посмотрев на тип объединения zend_value, который имеет много указателей на различные представления типов для переменной. Всякий раз, когда виртуальная машина Zend пытается извлечь значение из zend_value, она использует макросы, подобные ZSTR_VAL, которые пытаются получить доступ к указателю строки из объединения значений.

Источник

Самое интересное в PHP 8

PHP 7.4 только-только объявлена stable, а нам уже подавай еще больше усовершенствований. И лучше всех о том, что ждет PHP, может рассказать Дмитрий Стогов — один из ведущих разработчиков Open Source PHP и, наверное, старейший активный контрибьютор.

Все доклады Дмитрия только о тех технологиях и решениях, над которыми он работает лично. В лучших традициях Онтико под катом текстовая версия рассказа о самых интересных с точки зрения Дмитрия нововведениях PHP 8, которые могут открыть новые use-case-ы. В первую очередь JIT и FFI — не в ключе «потрясающих перспектив», а с подробностями реализации и подводными камнями.

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

Развитие производительности PHP

Работать над производительностью PHP я начал 15 лет назад, когда пришел в Zend. Тогда мы выпустили версию 5.0 — первую, в которой язык стал по-настоящему объектно-ориентированным. С тех пор нам удалось повысить производительность на синтетических тестах в 40 раз, а на реальных приложениях в 6 раз.

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

За это время было два прорывных момента:

На диаграмме видно, что как в конце разработки 5-й версии, так и в конце цикла разработки 7-й версии мы выходим на плато и замедляемся. Так за последний год работы над v7.4 удалось добиться только 2% прироста производительности. И это неплохо, потому что появились такие новые фичи как typed properties и ковариантные типы, которые замедляют PHP (об этих новинках на PHP Russia рассказывал Никита Попов).

И теперь всем интересно, что ждать от 8-й версии, сможет ли она повторить успех v7?

To JIT or not to JIT

Идеи по усовершенствованию интерпретатора еще не исчерпаны, но все они требуют очень существенной проработки. Многие из них приходится отбросить на этапе proof of concept, потому что выигрыш, который удается получить, оказывается несоизмерим с усложнением или налагаемыми техническими ограничениями.

Но остается надежда на новую прорывную технологию — конечно, вспоминается JIT и история успеха JavaScript-движков.

На самом деле работы над JIT для PHP ведутся еще с 2012 года. Было 3 или 4 реализации, мы работали с коллегами из Intel, JavaScript-хакерами, но как-то все не получалось включить JIT в главную ветку. В конце концов в PHP 8 мы включили JIT в компилятор и увидели двукратное ускорение, но только на синтетических тестах, а на реальных приложениях наоборот — замедление.

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

Разумеется, это не то, к чему мы стремимся.

В чем же дело? Может, мы что-то делаем не так, может быть, WordPress такой плохой, и никакой JIT ему не поможет (да, на самом деле это так). Может, мы уже сделали слишком хороший интерпретатор, а в JavaScript он хуже. На вычислительных тестах это действительно так: интерпретатор PHP один из лучших.

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

На тесте Mandelbrot он обгоняет даже такие жемчужины, как LuaJIT — интерпретатор, написанный на ассемблере. На этом тесте мы уступаем всего в 4 раза оптимизирующему компилятору GCC-5.3. С помощью JIT мы могли бы получить лучшие результаты в тесте Mandelbrot. Собственно, мы уже это делаем, то есть способны генерировать код, который конкурирует с компилятором C.

Почему же тогда мы не можем ускорить реальные приложения? Чтобы разобраться, расскажу, как мы делаем JIT. Начнем с самых азов.

Как работает PHP

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

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

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

Но скорее всего никто не использует PHP в голом виде, все его используют с OPcache.

PHP + OPcache

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

Главная задача OPcache — избавиться от перекомпиляции скриптов на каждом запросе. Он встраивается в специально предназначенную для него точку, перехватывает все запросы на компиляцию и кэширует скомпилированный байт-код в shared memory.

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

В эту схему JIT уже можно встроить, что мы и сделаем. Но сначала покажу, как работает интерпретатор.

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

Интерпретатор — это в первую очередь цикл, который для каждой инструкции вызывает свой обработчик.

У нас используются два регистра:

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

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

В примере выше справа представлен обработчик, написанный для инструкции сложения. Он принимает операнды (здесь первый и второй могут быть константой, временной или локальной переменной), читает операнды, проверяет типы, производит непосредственную логику — сложение — и дальше возвращается обратно в цикл, который передает управление на следующий обработчик.

Из этого описания генерируются специализированные функции. Так как было три возможных первых операнда, три возможных вторых, то получается 9 разных функций.

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

В данных функциях уже вместо универсальных методов для получения операндов используются конкретные, которые не делают никаких проверок.

Гибридная виртуальная машина

Еще одно усложнение, которое мы сделали в версии 7.2 — это так называемая гибридная виртуальная машина.

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

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

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

Гибридная виртуальная машина позволила поднять производительность еще на 5-10%.

PHP + OPcache + JIT

JIT реализуется как часть OPcache.

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

После того, как байт-код скомпилирован и оптимизирован, для него запускается JIT-компилятор, который уже не работает с исходными текстами. Из PHP байт-кода JIT-компилятор генерирует нативный код, после чего в байт-коде изменяется адрес первой инструкции (по сути дела функции).

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

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

Слева на PHP написана некая функция, которая считает сумму чисел от 0 до 100. Справа сгенерированный байт-код. Первая инструкция присваивает сумме 0, вторая то же делает для i, потом безусловный переход по метке. На метке L1 проверяется условие выхода из цикла: если оно выполнилось, то выходим, если нет, то идем в цикл. Дальше прибавляем к сумме i, записываем результат в сумму, увеличиваем i на 1.

Непосредственно отсюда генерируем ассемблерный код, который получается достаточно неплохим.

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

Первая инструкция QM_ASSIGN скомпилирована всего в две машинные инструкции (2-3 строка). В регистре %esi лежит указатель текущего фрейма активации. По смещению 30 лежит переменная сумма. Первая инструкция записывает значение 0, вторая записывает 4 — это идентификатор целого типа ( IS_LONG ). Для переменной i компилятор понял, что она всегда long, и для нее не нужно хранить тип. Более того, её можно хранить в машинном регистре. Поэтому здесь просто XOR регистра c самим с собой — самая простая и дешевая инструкция обнуления.

Видно, что код близок к оптимальному. Можно было бы оптимизировать его еще больше, избавившись от проверки суммы на тип на каждой итерации цикла. Но это уже достаточно сложная оптимизация, мы пока таких не делаем. Мы развиваем JIT как достаточно простую технологию, не пытаемся делать то, что пытается делать Java HotSpot, V8 — у нас сил меньше.

Что не так с JIT

Почему же при таком хорошем ассемблерном коде мы не можем ускорить реальные приложения?

Собственно, а должны ли?

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

Скромные планы на PHP 8

Одно из усовершенствований, которого мы хотим добиться в PHP 8, — это генерировать меньше кода. Сейчас, как я сказал, мы генерируем нативный код для всего скрипта, который грузим на этапе загрузки. Но половина функций наверняка не будет вызываться. Поэтому мы пошли немного дальше и ввели некий триггер, который позволяет сконфигурировать, когда мы хотим запускать JIT. Его можно запускать:

В этих условиях мы собираемся уйти от честной компиляции и начать делать ее спекулятивно.

php 8 jit как включить. Смотреть фото php 8 jit как включить. Смотреть картинку php 8 jit как включить. Картинка про php 8 jit как включить. Фото php 8 jit как включить

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

Для всего остального будем ставить заглушки. Все равно будут проверки и возможные выходы, при которых будет запускаться процесс деоптимизации, то есть мы будем восстанавливать необходимое для интерпретации состояние виртуальной машины и отдавать на исполнение интерпретатору.

Подобная схема используется и в HotSpot Java VM, и в V8. Но адаптация технологии к PHP имеет ряд сложностей. В первую очередь это то, что мы имеем shared байт-код и shared нативный код, используемый из разных процессов. Мы не можем их менять непосредственно в shared memory, нужно сначала куда-то копировать, изменять, а потом уже коммитить обратно в shared memory.

Preloading. Проблема связывания классов

На самом деле, многие идеи усовершенствований PHP, уже давно включенных в PHP 7 и даже PHP 5, пришли из работ, связанных с JIT. Сегодня я расскажу еще об одной такой технологии — это preloading. Эта технология уже включена в PHP 7.4 и дает возможность задать набор файлов, загрузить их при старте сервера и сделать все функции из этих файлов постоянными.

Одна из проблем, которую решает preloading-технология — это проблема связывания классов. Дело в том, что когда мы просто компилируем файлы в PHP, каждый файл компилируется отдельно от других. Делается это потому, что каждый из них может изменяться отдельно. Нельзя связать класс из одного скрипта с классом из другого скрипта, потому что при следующем запросе один из них может измениться, и что-то пойдет не так. Более того, в нескольких файлах может быть одноименный класс, и при одном запросе один из них используется в качестве parent, а при другом —используется другой класс из другого файла (с тем же именем, но совсем другой). Получается, что генерируя код, который будет выполняться на нескольких запросах, нельзя ссылаться ни на классы, ни на методы, потому что они каждый раз пересоздаются заново (время жизни кода превышает время жизни классов).

Preloading позволяет связать классы изначально и, соответственно, генерировать код более оптимально. Как минимум, для фреймворков, которые будут загружаться с помощью preloading’а.

Эта технология помогает не только для связывания классов. Что-то подобное реализовано в Java как Class Data Sharing. Там эта технология в первую очередь предназначена для ускорения старта приложений и уменьшения общего количества потребляемой памяти. Те же самые плюсы получаются и в PHP, поскольку теперь связывание классов не делается в runtime, а выполняется единожды. Кроме того связанные классы теперь хранятся не в адресном пространстве каждого процесса, а в shared memory, и следовательно общее потребление памяти падает.

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

Но есть и минусы. Загруженные при старте скрипты нельзя заменить без рестарта PHP. Если мы что-то загрузили и сделали постоянным, то уже не можем это выгрузить. Поэтому технология может быть использована со стабильными фреймворками, но если вы деплоите приложение по нескольку раз в день, скорее всего, она вам не подойдет.

Технически preloading включается с помощью всего одной директивы конфигурации opcache.preload, на вход которой вы даете файл сценария — обычный PHP-файл, который будет запущен на этапе старта приложения (не просто загружен, а именно выполнен).

Это один из возможных сценариев, который рекурсивно читает все файлы в какой-то директории (в данном случае ZendFramework). Можно реализовать абсолютно любой сценарий на PHP: читать списком, добавить исключения, или вообще скрестить с composer, чтобы он подсовывал файлы, которые нужны для preloading. Это всё дело техники, и более интересно не как грузить, а что грузить.

Что загружать в preloading

Я попробовал эту технологию на WordPress. Если просто загрузить все *.php файлы, то WordPress перестанет работать из-за упомянутой ранее особенности: в нем есть проверка function_exists, которая становится всегда истиной. Поэтому пришлось немного модифицировать сценарий из предыдущего примера (добавить исключения), и тогда, без каких-либо изменений в самом WordPress, он заработал.

Скорость [req/seq]Память [MB]Количество скриптовКоличество функцийКоличество классов
Ничего3780000
Все (почти*)3957,52541770148
Только используемые скрипты3964,584153251

5% ускорение, что уже неплохо.

FFI — Foreign Function Interface

Еще одна смежная c JIT технология, которая была разработана для PHP, — это FFI (Foreign Function Interface) или, говоря по-русски, возможность вызывать функции написанные на других компилируемых языках программирования без компиляции. Реализации подобной технологии в Python впечатлили моего шефа (Zeev Surazki), а сам я проникся, когда стал адаптировать ее к PHP.

В PHP уже было несколько попыток создать расширение для FFI, но все они использовали собственный язык или API для описания интерфейсов. Я же подсмотрел идею в LuaJIT, где для описания интерфейсов используется просто язык C (подмножество), и в результате получилась очень крутая игрушка. Теперь, когда мне нужно проверить как что-то работает на C, я пишу это на PHP — бывает, прямо в командной строке.

FFI позволяет работать со структурами данных, определенными на С, и может быть интегрирован с JIT для генерации более эффективного кода. Его реализация на основе libffi уже включена в PHP 7.4.

Покажу, как использовать FFI, чтобы реализовать настоящее GUI-приложение под Linux.

Не пугайтесь С-кода, я сам GUI на С писал лет 20 назад, а этот пример нашел в интернете.

Программка создает приложение, навешивает на событие activate callback, запускает приложение. В callback создаем окошко, назначаем ему title размер и показываем.

А теперь, то же самое, переписанное на PHP:

Здесь в первую очередь создается FFI-объект. Ему на вход передается описание интерфейса — по сути дела h-файл — и библиотека, которую хотим загрузить. После этого все функции, описанные в интерфейсе, становятся доступны как методы объекта ffi, а все передаваемые параметры автоматически и абсолютно прозрачно транслируются в необходимое машинное представление.

Видно, что здесь все точно так же, как и в предыдущем примере. Отличие лишь в том, что в C мы посылали callback, как адрес, а в PHP связь происходит по имени заданному строкой.

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

В данном случае эти С-определения скопированы из h-файлов библиотеки GTK, практически без изменений.

Чтобы не мешать в одном файле C и PHP, можно вынести весь С-код в отдельный файл, например, с названием gtk-ffi.h и добавить в начало пару специальных define’ов, которые задают имя интерфейса и библиотеку для загрузки:

Таким образом, мы выделили все описание С интерфейса в один файл. Этот gtk-ffi.h почти настоящий, но к сожалению, у нас пока не реализован С препроцессор, а значит макросы и include’ы работать не будут.

Теперь давайте загрузим этот интерфейс в PHP:

Остальной код практически не изменился, только в качестве обработчика события мы стали использовать безымянную функцию и передавать title с помощью лексического связывания. То есть мы используем и C, и сильные стороны PHP, которые в C недоступны.

Библиотеку, созданную подобным образом, можно было бы уже использовать в вашем приложении. Но хорошо, если она будет работать только в командной строке, а если засунуть её внутрь веб-сервера, то на каждом запросе будет читаться файл gtk_ffi.h, создаваться и загружаться библиотека, делаться биндинг… И вся эта повторяющаяся работа будет грузить ваш сервер.

Чтобы избежать этого и, по сути, позволить писать расширения PHP на самом PHP, мы решили скрестить FFI с preloading.

FFI + preloading

Код практически не изменился, только теперь h-файлы отдаем в preloading, и FFI::load выполняем непосредственно в момент preloading, а не когда создаем объект. То есть загрузка библиотеки, все разборы и связывания производятся один раз (при старте сервера), а с помощью FFI::scope(«GTK») мы в своем скрипте получаем доступ к заранее загруженному интерфейсу по имени.

В таком варианте FFI можно использовать из веб-сервера. Конечно, это не для GUI, но таким образом можно написать, например, биндинг к базе данных.

Созданное подобным образом расширение можно использовать прямо из командной строки:

Еще один плюс скрещивания FFI и preloading — это возможность запретить использовать FFI для всех user level скриптов. Можно указать ffi.enable = preload, что будет означать, что мы доверяем предзагружаемым файлам, но вызов FFI из обычных PHP-скриптов запрещен.

Работа со структурами данных С

Еще одна интересная особенность FFI в том, что он умеет работать с нативными структурами данных. Можно в любой момент создать в памяти любую структуру данных, описанную на C.

Примеры использования FFI:

К сожалению, скорость работы FFI пока оставляет желать лучшего.

Происходит это из-за динамических особенностей PHP. Он просто не знает, что обращается к определенной структуре данных, и каждый раз с помощью callback’ов и загруженного описания структур данных определяет тип элемента, его размер и положение в памяти. Подобное происходит и в других динамических языках с FFI. Как говорится, чудес не бывает. Хотя если скрестить FFI c JIT, то, как показывает LuaJIT, можно добиться и чудес. Для нас эта планка пока недоступна, но это то направление, в котором мы предполагаем двигаться.

На таком простом примере использование FFI дает двукратное замедление.

Автор выражает благодарность: Zeev Surasky (Zend), Andi Gutmans (ex-Zend, Amazon), Xinchen Hui (ex-Weibo, ex-Zend, Lianjia), Nikita Popov (JetBrains), Anatol Belsky (Microsoft), Anthony Ferrara (ex-Google, Lingo Live), Joe Watkins, Mohammad Reza Haghighat (Intel) и команду Intel, Andy Wingo (JS hacker, Igalia), Mike Pall (автор LuaJIT).

В вопросах после доклада тоже есть много интересного, и не только связанного с темой доклада, послушать можно с этого момента.

PHP Russia 2020 быть! Следите за новостями в рассылке или telegram-канале, видео с конференции 2019 года смотрите на соответствующем youtube-канале и думайте, а чем вы можете поделиться с сообществом, — подать доклад уже можно.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *