PHP GR8: повысит ли JIT производительность PHP 8
https://habr.com/ru/company/badoo/blog/448622/PHP — один из основных языков разработки в Badoo. В наших дата-центрах тысячи процессорных ядер заняты выполнением миллионов строк кода на PHP. Мы внимательно следим за новинками и активно ищем пути улучшения производительности, так как на наших объёмах даже небольшая оптимизация приводит к существенной экономии ресурсов. Одна из главных новостей в области производительности PHP — появление JIT в восьмой версии языка. Это, безусловно, не могло остаться без нашего внимания, и мы перевели статью о том, что есть JIT, как он будет реализован в PHP, зачем его решили делать и что от него ждать.
Если вы не вышли из пещеры или не прибыли из прошлого (в этом случае добро пожаловать), то уже знаете, что в PHP 8 будет JIT: на днях тихо-мирно завершилось голосование, и подавляющее большинство участников высказались за внедрение, так что всё решено.
Можно в порыве радости даже изобразить несколько безумных движений как на фото (это, к слову, называется «детройтский JIT»:
А теперь присядьте и прочтите эту развенчивающую мифы статью. Я хочу прояснить недопонимание, связанное с тем, что собой представляет JIT и чем он полезен, и рассказать о том, как он работает (но не слишком подробно, чтобы вы не заскучали).
Поскольку я не знаю, кто будет читать статью, пойду от простых вопросов к сложным. Если вы уже знаете ответ на вопрос в заголовке, можете смело пропускать соответствующую главу.
Что такое JIT?
PHP реализован на базе виртуальной машины (мы называем её Zend VM). Язык компилирует исходный PHP-код в инструкции, которые понимает виртуальная машина (это называется стадией компиляции). Полученные на стадии компиляции инструкции виртуальной машины мы называем опкодами (opcodes). На стадии исполнения (runtime) Zend VM исполняет опкоды, выполняя тем самым требуемую работу.
Эта схема прекрасно работает. Кроме того, инструменты вроде APC (раньше) и OpCachе (сегодня) кешируют результаты выполнения стадии компиляции, так что эта стадия выполняется лишь в случае необходимости.
Если коротко, то JIT — это стратегия компиляции just in time (в нужный момент), при которой код сначала переводится в промежуточное представление, которое затем в ходе исполнения превращается в машинный код, зависящий от архитектуры.
В PHP это означает, что JIT будет рассматривать полученные на стадии компиляции инструкции для виртуальной машины как промежуточное представление и выдавать машинный код, который будет выполняться уже не Zend VM, а непосредственно процессором.
Для чего PHP нужен JIT?
Незадолго до появления PHP 7.0 основным направлением работы команды PHP стала производительность языка. Большинство основных изменений в PHP 7.0 содержались в патче PHPNG, который значительно улучшил то, как PHP использует память и процессор. С тех пор каждому из нас приходится поглядывать за производительностью языка.
После выхода PHP 7.0 улучшения производительности продолжились: оптимизирована хеш-таблица (основная структура данных в PHP), внедрены специализация определённых опкодов в Zend VM и специализация определённых последовательностей в компиляторе, постоянно улучшается Optimizer (компонент OpCache) и реализовано ещё множество других изменений.
Суровая правда заключается в том, что в результате всех этих оптимизаций мы быстро приближаемся к пределу возможностей улучшения производительности.
Обратите внимание: под «пределом возможностей улучшения» я имею в виду тот факт, что компромиссы, на которые придётся пойти ради дальнейших улучшений, больше не выглядят привлекательными. Когда речь идёт об оптимизации производительности, мы всегда говорим о компромиссах. Нередко ради производительности нам приходится жертвовать простотой. Каждому хотелось бы думать, что самый простой код является и самым быстрым, но в современном мире программирования на С это не так. Самым быстрым чаще всего оказывается код, который подготовлен к использованию преимуществ внутреннего устройства архитектуры или встроенных в платформу/компилятор конструкций. Простота сама по себе не гарантирует лучшей производительности.
Поэтому на данном этапе оптимальным способом выжать из PHP ещё больше производительности выглядит внедрение JIT.
JIT ускорит работу моего сайта?
По всей вероятности, незначительно.
Возможно, это не тот ответ, который вы ожидали. Дело в том, что в общем случае PHP-приложения ограничены по вводу-выводу (I/O bound), а JIT лучше всего работает с кодом, который ограничен по процессору (CPU bound).
Что означает «ограничен по вводу-выводу и по процессору»?
Для описания характеристик общей производительности какого-то кода или приложения мы используем термины «ограничен по вводу-выводу» и «ограничен по процессору».
Самое простое определение:
- ограниченный по вводу-выводу код будет работать значительно быстрее, если мы найдём способ улучшить (уменьшить, оптимизировать) выполняемые операции ввода-вывода;
- ограниченный по процессору код будет работать значительно быстрее, если мы найдём способ улучшить (уменьшить, оптимизировать) выполняемые процессором инструкции или волшебным образом увеличим тактовую частоту процессора.
Код и приложение могут быть ограничены по вводу-выводу, по процессору или по тому и другому.
В целом PHP-приложения склонны быть ограничены по вводу-выводу: основным их узким местом зачастую оказываются операции ввода-вывода — подключение, чтение и запись в базу данных, кеши, файлы, сокеты и т. д.
Как выглядит ограниченный по процессору PHP-код?
Возможно, некоторые PHP-программисты плохо знакомы с ограниченным по процессору кодом из-за самой природы большинства PHP-приложений: обычно они выполняют роль связующего звена с базой данных или с кешем, поднимают и выдают небольшие количества HTML/JSON/XML-ответов.
Вы можете посмотреть на свою кодовую базу и найти много кода, который не имеет ничего общего с вводом-выводом, кода, который вызывает функции, никак не связанные с вводом-выводом. И вас может смутить, что это не делает ваше приложение ограниченным по процессору, хотя в его коде больше строк, не работающих с вводом-выводом, чем работающих.
Дело в том, что PHP — один из самых быстрых интерпретируемых языков. Не существует заметной разницы между вызовом функции, не задействующей ввод-вывод, в Zend VM и в машинном коде. Конечно, какая-то разница есть, но и машинный код, и Zend VM используют соглашение о вызовах (calling convention), поэтому не имеет значения, вызываете вы какую-то_функцию_уровня_С()
в опкодах или в машинном коде, — это не окажет заметного влияния на производительность всего приложения, которое совершает вызов.
Примечание: если говорить упрощённо, то соглашение о вызовах — это последовательность инструкций, исполняемых до входа в другую функцию. В обоих случаях соглашение о вызовах передаёт аргументы в стек.
Вы спросите: «А что насчёт циклов, хвостовых вызовов (tail calls) и прочего»? PHP достаточно сообразителен — и при включённом компоненте Optimizer из OpCache ваш код будет волшебным образом преобразован в более эффективную версию написанного вами.
Здесь нужно отметить, что JIT не изменит соглашения о вызовах Zend VM. Сделано так, потому что PHP должен уметь в любой момент переключаться между режимами JIT и VM (поэтому решили сохранить текущие соглашения). В результате любые вызовы, которые вы видите повсюду, с использованием JIT будут работать ненамного быстрее.
Если хотите увидеть, как выглядит ограниченный по процессору PHP-код, загляните сюда: https://github.com/php/php-src/blob/master/Zend/bench.php. Это крайний пример, но он показывает, что всё великолепие JIT раскрывается в математике.
Пришлось пойти на такой экстремальный компромисс, чтобы ускорить математические вычисления в PHP?
Нет. Мы пошли на это ради расширения спектра применения языка (и расширения значительного).
Не хотим хвастаться, но PHP доминирует в вебе. Если вы занимаетесь веб-разработкой и не рассматриваете использование PHP в своём следующем проекте, то вы что-то делаете неправильно (по мнению очень предвзятого разработчика PHP).
На первый взгляд может показаться, что ускорение математических вычислений в PHP имеет очень узкое применение. Однако это открывает нам дорогу, например, к машинному обучению, 3D-рендерингу, 2D-рендерингу (GUI) и анализу данных.
Почему это нельзя реализовать в PHP 7.4?
Выше я назвал JIT экстремальным компромиссом, и я действительно так считаю: это одна из самых сложных стратегий компилирования среди всех существующих, если не самая сложная. Внедрение JIT — это значительное повышение сложности.
Если вы спросите Дмитрия, автора JIT, сделал ли он PHP сложным, он ответит: «Нет, я ненавижу сложность» (это цитата).
По сути, «сложное» означает «то, что мы не понимаем». И сегодня мало кто из разработчиков языка действительно понимает имеющуюся реализацию JIT.
Работа над PHP 7.4 идёт быстрыми темпами, и внедрение JIT в эту версию приведёт к тому, что лишь единицы смогут отлаживать, исправлять и улучшать язык. Это неприемлемо для тех, кто голосовал против JIT в PHP 7.4.
До релиза PHP 8 многие из нас будут разбираться в реализации JIT. Есть фичи, которые мы хотим реализовать, и инструменты, которые хотим переписать для восьмой версии, поэтому вникнуть в JIT нам необходимо в первую очередь. Нам нужно это время, и мы очень благодарны, что большинство проголосовали за то, чтобы дать нам его.
Сложное не синоним ужасного. Сложное может быть прекрасным как звёздная туманность, и это как раз про JIT. Иными словами, даже когда у нас в команде человек 20 станут разбираться в JIT не хуже Дмитрия, это не изменит сложности самой природы JIT.
Разработка PHP замедлится?
Нет причин так думать. У нас достаточно времени, поэтому можно утверждать, что к моменту готовности PHP 8 среди нас будет достаточно тех, кто освоился с JIT настолько, чтобы работать не менее эффективно, чем сегодня, когда речь пойдёт об исправлении ошибок и развитии PHP.
Когда будете пытаться соотнести это с представлением об изначальной сложности JIT, помните, что большая часть времени, которое мы тратим на внедрение новых фич, уходит на их обсуждение. Чаще всего при работе над фичами и исправлении ошибок написание кода занимает минуты или часы, а обсуждения — недели или месяцы. В редких случаях код приходится писать часами или днями, но и тогда обсуждения всегда длятся дольше.
Это всё, что я хотел сказать.