php 8 jit benchmark
Бенчмарки PHP 8.0 и 7.4 на Symfony: повышение производительности JIT до +9% вместе с предварительной загрузкой OPCache
Также делимся переводом полезной статьи.
PHP 8.0 должен увидеть свет 26 ноября 2020 года, а вскоре за ним и последняя версия фреймворка Symfony 5.2. Здесь представлена серия бенчмарков, выполненных на последних версиях обоих проектов. Это чистый hello world сценарий, и его следует воспринимать как таковой. Имеет значение только относительная производительность. В реальном мире YMMV (ваши результаты могут отличаться от приведенных).
АПДЕЙТ 1: в этой статье изначально отсутствовали показатели для JIT PHP 8.0, поскольку он не был включен по умолчанию. Спасибо Андре Ремке, указавшему на это!
АПДЕЙТ 2: PHP 8.0.0 и Symfony 5.2.0 уже зарелизили. Я провел бенчмарки снова, и результаты оказались практически такими же, поэтому исходные значения все еще актуальны.
Бенчмарки проводились на PHP 8.0-RC5 и Symfony 5.2-RC2. Обе версии не являются окончательными, но это самые последние кандидаты на релиз, которые могут быть очень близки к финальным версиям. Средой выполнения послужил датацентр Hetzner Helsinki с виртуальным сервером CCX11 (2 выделенных виртуальных процессора, 8 ГБ ОЗУ), выполняющим работу, и еще одним для выполнения бенчмарков. В качестве ОС использовалась Ubuntu 20.20 с PHP, установленным из пакетов от Ондржея Сури.
Целью бенчмарка было простое приложение Symfony 5.2 с экшеном контроллера генерации счастливого числа в корне. Экшн выводит фрагмент HTML со случайным числом от 100 до 999. Я провел нагрузочный тест с пятью параллельными задачами (1, 10, 25, 50, 100) три раза для каждой конфигурации и использовал в отчете среднее значение.
В общей сложности были протестированы пять конфигураций с использованием PHP-FPM и сетапом Nginx:
PHP 7.4 без предварительной загрузки OPCache
PHP 7.4 с предварительной загрузкой OPCache
PHP 8.0 без предварительной загрузки OPCache
PHP 8.0 с предварительной загрузкой OPCache
PHP 8.0 с предварительной загрузкой OPCache и включенным JIT
Результаты и заключение
Балансировка нагрузки осуществлялась с помощью hey, и я дополнительно привожу показатели пропускной способности (req/s) и время отклика (мс). Приложение не подключается к удаленным службам, таким как базы данных.
Что касается пропускной способности, как PHP 7.4, так и 8.0 значительно выигрывают от включения предварительной загрузки OPCache. В среднем PHP 7.4 немного более производительный, но ненамного. Что касается пропускной способности, похоже, изменений при параллелизме от 10 до 100 нет, но PHP 8.0 с JIT, как ни странно, менее эффективен на небольшом параллелизме, но оживает и превосходит 7.4 с предварительной загрузкой OPCache, начиная с 25+ параллелизма и далее.
Что касается времени отклика, то здесь история аналогична производительности. Включение предварительной загрузки OPCache дает лучшие результаты, чем обновление с PHP 7.4 до 8.0. По времени отклика (чем меньше, тем лучше) PHP 8.0 немного лучше, чем 7.4, но незначительно. Время отклика выше при низком параллелизме, но остается постоянным от 10 до 100. 8.0 с JIT похожа: хуже при низком параллелизме, но лучше при высокой нагрузке.
В целом улучшения базовой производительности для такого приложения, как Symfony, очень похожи между PHP 8.0 и 7.4. Это неудивительно, поскольку известно, что JIT в PHP 8.0, который уже существовал в ранней форме для PHP 7.0, более полезен для вычислений и других задач обработки данных, завязанных на процессоре.
Что касается производительности, JIT здесь выглядит неоднозначно, замедляясь при низком уровне параллелизма и обеспечивая значительное улучшение пропускной способности только с предварительной загрузкой OPCache. Требуется дополнительные исследования, но здесь дело обстоит неясно, так как это может снизить производительность. Кажется, что JIT следует рассматривать в индивидуальном порядке для PHP.
Так что пока можно с уверенностью сказать, что торопиться обновляться с 7.4 до 8.0 из соображений производительности не следует. Будет лучше, если вы обновите PHP 7.3 до 7.4 и включите предварительную загрузку OPCache. Это не значит, что PHP 8.0 в разочаровал нс. Он укомплектован новыми языковыми фичами и важными депрекейшенами. Самое главное, приятно видеть, что не было никаких падений в производительности (как это было при переходе с 4.4 на PHP 5.0 еще в 2004 году). Это отличное начало для 8.x.
Ваши комментарии и отзывы приветствуются на Discourse: бенчмарки PHP 8.0 и 7.4
Записаться на открытый вебинар по теме «Микрофреймворки: сравнение производительности Symfony и Symlex».
Прямо сейчас в OTUS действуют максимальные новогодние скидки на все курсы. Ознакомиться с полным списком курсов вы можете по ссылке ниже. Также у всех желающих есть уникальная возможность отправить адресату подарочный сертификат на обучение в OTUS.
PHP 8: JIT performance in real-life web applications
For those interested in the JIT in PHP 8, I did some benchmarks for you in real-world web application scenario. Be aware that these benchmarks don’t say anything about whether the JIT is useful or not, they only show whether it can improve the performance of your average web application, or not.
# Setup
Let’s set the scene first. These benchmarks were run on my local machine. As so, they don’t say anything about absolute performance gains, I’m only interested in making conclusions about the relative impact the JIT has on real-life code.
I’ll be running PHP FPM, configured to spawn 20 child processes, and I’ll always make sure to only run 20 concurrent requests at once, just to eliminate any extra performance hits on the FPM level. Sending these requests is done using the following command, with ApacheBench:
# JIT Setup
In our real-life benchmarks, I’ll compare both modes with each other. So let’s start benchmarking!
# Establishing a baseline
First it’s best to establish whether the JIT is working properly or not. We know from the RFC that it does have a significant impact on calculating a fractal. So let’s start with that example. I copied the mandelbrot example from the RFC, and accessed it via the same HTTP application I’ll run the next benchmarks on:
After running ab for a few hundred requests, we can see the results:
requests/second (more is better) | |
Mandelbrot without JIT | 3.60 |
Mandelbrot with tracing JIT | 41.36 |
Great, it looks like the JIT is working! That’s even a ten times performance increase! Having verified it works as expected, let’s move on to our first real-life comparison. We’re going to compare no JIT with the function and tracing JIT; using 100MB of memory. The page we’re going to benchmark shows an overview of posts, so there’s some recursion happening. We’re also touching several core parts of Laravel as well: routing, the dependency container, as well as the ORM layer.
Side note:
requests/second (more is better) | |
No JIT | 63.56 |
Function JIT | 66.32 |
tracing JIT | 69.45 |
Here we see the results: enabling the JIT only has a slight improvement. In fact, running the benchmarks over and over, the results differ slightly every time: I’ve even seen cases where a JIT enabled run performs worse than the non JIT’ed version. Before drawing final conclusions, let’s bump the memory buffer limit. We’ll give the JIT a little more room to breathe with 500MB of memory instead of 100MB.
requests/second (more is better) | |
No JIT | 71.69 |
Function JIT | 72.82 |
Tracing JIT | 70.11 |
As you can see: a case of the JIT performing worse. Like I said at the beginning of this post: I want to measure the relative impact the JIT has on real-life web projects. It’s clear from these tests that sometimes there might be benefits, but it’s in no way as noticeable as the fractal example we started out with. I admit I’m not really surprised by that. Like I wrote before: there’s very little hot code to be optimised in real-life applications, we’re only rarely doing fractal-like computations.
So am I saying there’s no need for the JIT? Not quite, I think the JIT can open up new areas for PHP: areas where complex computations do benefit from JIT’ed code. I’m thinking about machine learning, AI, stuff like that. The JIT might give opportunities to the PHP community that didn’t exist yet, but it’s unclear to say anything with certainty at this point.
So, that concludes my JIT testing. As expected: the JIT probably won’t have a significant impact on web applications, at least not right now.
I won’t discuss my thoughts on whether the JIT itself is a good addition or not in this post, let’s have those discussions together over here!
Понимаем 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.
Достаточно прямолинейно, как вы можете заметить. Но здесь есть и узкое место: какой смысл лексировать и парсить код каждый раз, когда вы его выполняете, если ваш php-код может даже не меняется так часто?
В конце концов, нас интересуют только опкоды, верно? Правильно! Вот зачем существует расширение Opcache.
Расширение Opcache
Расширение Opcache поставляется с PHP, и, как правило, нет особых причин его деактивировать. Если вы используете PHP, вам, вероятно, следует включить Opcache.
Что он делает, так это добавляет слой оперативного общего кэша для опкодов. Его задача состоит в том, чтобы извлекать опкоды, недавно сгенерированные из нашего AST, и кэшировать их, чтобы при дальнейших выполнениях можно было легко пропустить фазы лексирования и синтаксического анализа.
Вот схема того же процесса с учетом расширения Opcache:
Поток интерпретации 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
В этой статье мы рассмотрим результаты нескольких бенчмарков, начиная с PHP 5 и вплоть до экспериментальной JIT-ветки (сейчас в разработке). На момент написания не было известно, появится ли до PHP 8 ещё какая-то основная версия, например PHP 7.2. Но логично предположить, что возможности экспериментальной ветки как минимум будут включены в PHP 8.
C момента своего появления в 1994-м язык PHP радикально изменился. Первые релизы представляли собой просто внешние CGI-программы, которые создавались во многом как личный проект Расмуса Лердорфа. С третьей версии PHP был серьёзно переработан, возникла группа разработчиков языка.
Благодаря расширяемости PHP 3 функциональность языка стремительно разрасталась. Появлялись базовые и дополнительные расширения, которые привносили новые функции в разные сферы: работу с сетью, парсинг, кеширование и поддержку баз данных.
Развивался и сам язык, в него внесли много улучшений. Появилась поддержка объектно ориентированных конструкций, таких как классы, интерфейсы, трейты, замыкания и т. д.
Но многим разработчикам этого было мало. С ростом популярности языка росли и требования к нему со стороны сообщества, в основном связанные с производительностью, масштабируемостью и более экономным потреблением памяти.
Почти 20 лет создатели языка прилагали огромные усилия, чтобы удовлетворять всевозможные требования. Хотя с появлением PHP 3 производительность существенно возросла, сколько-то серьёзные результаты язык смог продемонстрировать только с PHP 4, когда появился движок Zend.
В 2000-м были внедрены новые in-memory компилятор и модель исполнения (executor model). Это позволило вновь сильно поднять производительность PHP, нередко в 5—10 раз. В результате его начали всерьёз рассматривать как инструмент для создания веб-приложений и сайтов. И сегодня PHP достиг высот, которых никто не ожидал от этого языка, когда он появился.
Но взрывной рост популярности PHP лишь привёл к росту требований о повышении производительности. К счастью, у движка Zend прекрасный потенциал для модернизации.
Хотя PHP 5 не стал заметным шагом вперёд и в некоторых случаях был даже медленнее PHP 4, группа разработчиков Zend постоянно оптимизировала движок от релиза к релизу, в результате PHP 5.6 оказался быстрее в 1,5—3 раза.
Но главный рывок произошёл с выходом PHP 7 в декабре 2015-го. Через год была анонсирована версия 7.1, тоже получившая ряд улучшений.
Компилятор PHP JIT и ожидания по улучшению производительности PHP 8
В настоящее время разрабатывается очень многообещающая версия Zend. Она будет основана на версии из релиза 7.1, а когда именно выйдет, пока не объявлено. Так что сейчас это экспериментальная JIT-ветка.
Одна из главных интриг связана с Just-In-Time (JIT) компиляцией. Это методика преобразования кода в другой формат (нативный машинный код) прямо перед выполнением. Цель JIT — повысить скорость работы программ. Посмотрим, смогут ли разработчики сдержать обещание.
Бенчмарк обработки PHP-скриптов
Для этой статьи использовались бенчмарки, измерявшие производительность обработки скриптов на чисто процессорных задачах, т. е. без операций ввода-вывода: обращений к файлам, подключений к сети или базе данных.
Применялись следующие бенчмарки:
Бенчмарки прогонялись на последних второстепенных релизах основных версий PHP:
Те же бенчмарки прогонялись и на всех промежуточных релизах, например между 5.3.0 и 5.3.29. Результаты красноречивы: релизы не демонстрировали заметных улучшений производительности. Улучшения отмечались только при переходах между основными версиями, например с PHP 5.4 на PHP 5.5 или с PHP 5.6 на PHP 7.
Это означает, что те же скрипты будут выполняться примерно с одной скоростью и на PHP 5.4.0, и на PHP 5.4.45.
Подробности о настройке хостовой системы, о выполнении конкретных бенчмарков и об интерпретировании результатов можно почитать тут.
Сравнение результатов процессорных бенчмарков
По каждому бенчмарку приведены три значения:
Результаты прогона бенчмарков вы можете увидеть в таблице ниже.
(1) Бенчмарк не может выполняться на версиях до 5.3, потому что он использует свойства, которые ещё не были реализованы.
(2) Результаты в этой колонке немного смещены, потому что бенчмарку для работы нужен как минимум PHP 5.3. Их можно взять просто для справки, раз нельзя сравнить с PHP 5.0.
(3) Это модифицированная версия скрипта mandelbrot.php, который выполнялся слишком быстро в версии 7.1.0 в экспериментальной ветке, так что не получалось точно измерить скорость. Поэтому мы внутри скрипта выполняли сто вычислений, а не одно.
Конечно, чисто процессорные бенчмарки не позволяют оценить все аспекты производительности PHP. Данные могут быть нерепрезентативны. Тем не менее на основе этих данных можно сделать выводы:
Сопоставление производительности разных версий PHP
PHP 5 гораздо производительнее, чем PHP 4. Движок Zend, лежащий в основе интерпретатора, был полностью переработан (Zend Engine 2), что открыло дорогу дальнейшим улучшениям. Здесь мы не будем освещать все различия между PHP 4 и PHP 5, вкратце пройдёмся лишь по вещам, внедрённым после PHP 5.0.
Ниже перечислены только те изменения, которые затронули ядро PHP. Более подробный список нововведений и изменений: PHP 5 и PHP 7.
PHP 5.1
PHP 5.2
PHP 5.3
PHP 5.4
PHP 5.5
PHP 5.6
PHP 7 vs. PHP 5.6
Большинство из этих улучшений относятся к движку Zend:
PHP 7.1, улучшения производительности
Свойства PHP 8 или PHP 7.2, экспериментальная JIT-ветка
Как измерялась производительность
Настройка системы
С сделал выделенную систему с такими характеристиками:
Хотя система поставлялась с компилятором Gnu C 4.7.2, пришлось поставить более свежую версию: экспериментальная JIT-ветка должна компилироваться с помощью Gnu C 4.8 и выше.
Компилирование исходного кода
Перед сборкой полных дистрибутивов был запущен скрипт configure со следующими параметрами:
Конечно, я компилировал более старые версии, некоторые параметры требовалось отключить или заменить другими. Также не все расширения были доступны или могли быть скомпилированы.
Запуск бенчмарков
Каждый бенчмарк запускался с помощью PHP CLI (Command-Line Interface) через специальный скрипт, который делал следующее:
1) С помощью функции microtime() на лету модифицировал скрипт, чтобы изнутри измерять время его выполнения. После модифицирования скрипт выглядел так:
Это делалось для того, чтобы обеспечить стабильность измерений скрипта изнутри, без изменения его поведения.
2) Далее шли два сухих прогона, чтобы исполняемые PHP-файлы и содержимое скрипта бенчмарка оказались в кеше ОС.
3) Скрипт выполнялся пять раз, сохранялись минимальное, максимальное и среднее время выполнения. В этой статье представлены только средние значения — «время выполнения скрипта».
Использовались такие настройки в php.ini:
Интерпретирование результатов
Значение real — это время от вызова команды до её прерывания (пока не происходит возврата к командной строке).
Значение user — время, потраченное на выполнение пользовательского кода (в данном случае — исполняемого PHP-файла).
Значение sys — время, потраченное на выполнение кода ОС (kernel). Это значение должно быть минимальным, но может оказаться сильно больше представленного, если ваш код обращается, например, к медленным устройствам. Также на величину значения способна повлиять высокая загруженность ОС.
Тот же скрипт был выполнен на высоконагруженной ОС при параллельном компилировании тремя разными PHP-версиями:
Как видите, уровень нагрузки сильно влияет на время выполнения (возможно, и на системное время). Поэтому я добавил в бенчмарк ещё одно значение — overhead операционной системы. Это разница между полным временем выполнения ( elapsed time ) и суммой пользовательского и системного времени.
Я удостоверился, чтобы во время прогона бенчмарков это значение в течение 99 % времени было меньше 100 миллисекунд, даже когда выполнение скриптов занимало десятки секунд.
Спасибо Дмитрию Стогову и всей команде разработки PHP
Эта статья писалась при активной помощи Дмитрия Стогова. Он прояснил ряд моментов и рецензировал представленную здесь информацию.
Дмитрий был разработчиком расширения Turck MMCache, которое со времён PHP 4 может использоваться для кеширования PHP-опкодов в совместно используемой памяти. После этого Дмитрий начал работать над Zend, чем и занимается по сей день.
Также он когда-то инициировал создание PHPNG — того, что позднее превратилось в PHP 7. В работе над этой и последующими версиями с Дмитрием сотрудничали Никита Попов и Синьчэнь Хуэй (Xinchen Hui).
В создание PHP 5 внесли большой вклад Энди Гутманс, Зеев Сураски и Стас Малышев. Многих других разработчиков я не стану здесь перечислять, чтобы не загромождать статью.
Специальное благодарственное видео для всех, кто помогал развивать PHP
В 2016-м исполнился 21 год со дня появления PHP — 8 июня 1995 г.
Чтобы отдать должное всем, кто так или иначе внёс свой вклад в развитие PHP, Питер Кокот с помощью Gource создал анимационное видео. В нём рассказывается о развитии ключевых модулей PHP в течение всей жизни языка.
Питер Кокот хорошо известен в PHP-сообществе. Он основал в Facebook PHP Group, крупнейшую группу, посвящённую отдельному языку программирования. В ней состоят более 140 тыс. участников и 22 модератора. Создатель PHP Расмус Лердорф сказал: «В мире PHP ничего не происходит без движения сообщества». Надеюсь, эти слова будут вдохновлять вас.
Если вы не можете помочь развитию PHP с помощью написания кода на С, то можете выкладывать свои PHP-разработки на GitHub, PHP Classes, Packagist — куда угодно. Чем больше мест, где мы будем делиться друг с другом наработками, тем лучше.
Заключение
Цель статьи — дать представление о производительности разных версий PHP, начиная с 5.0 и заканчивая свежайшей экспериментальной версией. Тестирование выполнялось с помощью известных бенчмарков. Также в статье приведён список улучшений, повысивших производительность различных версий PHP.