JIT в PHP 8: почему он не ускорил ваш веб под нагрузкой
JIT в PHP 8 обещал прирост, но на типичном веб-приложении включение opcache.jit часто не даёт ничего или замедляет. Разбираем, где механизм реально работает, а где вы платите за прогрев впустую.

Вы включили opcache.jit=tracing на проде, перезапустили PHP-FPM и не увидели разницы в p95 — а иногда получили лишние миллисекунды на холодных воркерах. Это ожидаемо. JIT в PHP 8 разгоняет счётные циклы, а не запросы к базе и сериализацию JSON, из которых состоит средний бэкенд. Разберём, что именно происходит под капотом, почему на веб-нагрузке выигрыша почти нет и в каких сценариях JIT всё-таки окупается.
Что JIT делает и чего не делает
До PHP 8 движок работал так: исходник компилируется в опкоды, OPcache кеширует их в разделяемой памяти, а Zend VM интерпретирует эти опкоды на каждом запросе. JIT добавляет ещё один слой: часть опкодов транслируется в машинный код прямо во время выполнения и исполняется напрямую, минуя интерпретатор.
Ключевой момент, который ломает ожидания: JIT ускоряет исполнение опкодов. Если ваше приложение проводит 80% времени вне PHP-кода — в ожидании ответа PostgreSQL, в системных вызовах, внутри расширений на C (json_encode, preg_match, hash) — то ускорять JIT почти нечего. Он трогает только тот тонкий слой, где VM гоняет байткод вашей бизнес-логики.
JIT не делает PHP быстрее. Он делает быстрее исполнение опкодов. На типичном запросе, где логика ждёт I/O, это разные вещи.
Два режима: function и tracing
Режим задаётся четырёхзначным числом в opcache.jit. Практически используют два именованных значения:
- tracing (
opcache.jit=tracing, оно же 1254) — JIT наблюдает за исполнением, находит горячие участки и трассирует их. Режим по умолчанию для реального прироста; именно его рекомендует документация PHP для большинства случаев. - function (
opcache.jit=function, оно же 1205) — компилирует функции целиком без анализа горячих путей. Проще, но на реальных приложениях обычно проигрывает tracing.
Точную семантику каждого разряда числа смотрите в разделе про JIT официальной документации — там разобраны CPU-оптимизации, регистр-аллокация и триггеры компиляции. Наизусть их держать смысла нет.
Обязательное условие: JIT работает только при включённом OPcache
JIT — часть OPcache, а не отдельное расширение. Без opcache.enable=1 строка opcache.jit просто игнорируется. Вторая частая причина, по которой «JIT не включился» — исчерпанный буфер: если opcache.jit_buffer_size=0, JIT выключен независимо от режима.
Почему на вебе прироста нет
Возьмём разложение времени типичного запроса к API-эндпоинту, отдающему список из базы. Условно на 50 мс ответа приходится:
- ожидание сети и ответа БД — большая часть;
- сериализация результата (
json_encode) — исполняется в C-коде расширения, JIT сюда не лезет; - роутинг, DI-контейнер, гидрация объектов — вот это PHP-опкоды, которые JIT потенциально ускорит;
- autoload и чтение конфигов — при прогретом OPcache почти бесплатно.
Слой, который JIT реально трогает, — это единицы процентов от общего времени. Ускорьте его хоть вдвое, на p95 запроса это отразится в пределах шума. Отсюда правило: JIT окупается там, где PHP считает, а не ждёт.
Где JIT реально ускоряет
- математика с плотными циклами: обработка изображений на чистом PHP, физические расчёты, генерация фракталов, числодробилки;
- парсеры и интерпретаторы, написанные на PHP;
- алгоритмы на больших массивах в памяти без обращения к I/O;
- долгоживущие процессы (воркеры очередей, демоны), где стоимость прогрева JIT амортизируется на тысячах итераций.
Именно на бенчмарке Mandelbrot и подобных синтетических тестах JIT показывает кратный прирост — и именно эти цифры кочуют по презентациям, создавая ложное ожидание для веб-приложений.
Цена прогрева под нагрузкой
Под нагрузкой у JIT есть неочевидная стоимость. Трассировка и компиляция происходят в рамках рабочего процесса. При модели PHP-FPM каждый воркер прогревает свой JIT заново после рестарта или после того, как pm.max_requests убил и пересоздал процесс. На пике трафика вы платите за прогрев именно тогда, когда ресурсов меньше всего.
Второй эффект — потребление буфера. opcache.jit_buffer_size выделяется поверх opcache.memory_consumption. Если буфер мал, JIT перестаёт компилировать новые трассы молча; если велик — вы отъедаете память у самого OPcache и рискуете вытеснением закешированных опкодов.
Что и как мерить, а не гадать
Синтетический бенчмарк вам не поможет — он не воспроизводит ваш профиль I/O. Мерьте на своём трафике:
- Снимите базовую линию: p50/p95/p99 времени ответа и CPU на воркерах без JIT (
opcache.jit=off) под реальной или воспроизведённой нагрузкой. Инструмент нагрузки —wrk,k6или зеркалирование продового трафика. - Профилируйте, куда уходит время. Если профайлер (например, XHProf или коммерческие APM) показывает, что доминируют вызовы БД и C-функции — JIT вам не поможет, и дальше можно не идти.
- Включите
opcache.jit=tracingс явнымopcache.jit_buffer_size(начните с 64–128M), прогрейте, снимите те же метрики на той же нагрузке. - Сравните не средние, а перцентили и стабильность CPU. Прирост в пределах 2–3% на p95 — это шум, ради которого не стоит усложнять конфиг.
Числа привести здесь честно нельзя: они зависят от вашего профиля запросов, железа и версии PHP. Смысл в методике — сравнивайте один и тот же трафик до и после на одной машине.
Конфигурация: минимум для проверки
Проверено на PHP 8.2 и 8.3, CLI и PHP-FPM. Фрагмент для php.ini (или отдельного файла в conf.d):
# OPcache обязателен — без него JIT не работает
opcache.enable=1
opcache.enable_cli=1
# Память под кеш опкодов
opcache.memory_consumption=256
# Буфер под скомпилированный машинный код JIT.
# 0 означает JIT выключен, даже если задан режим.
opcache.jit_buffer_size=128M
# Режим трассировки — рекомендуемый для большинства случаев
opcache.jit=tracing
Проверить, что JIT действительно активен, можно через CLI:
# Показывает секцию opcache, включая строки про jit
php --ri opcache
Ищите в выводе ненулевой jit_buffer_size и активный режим. Если буфер нулевой или режим off — JIT не работает, разбирайтесь с этими двумя параметрами прежде, чем делать замеры.
Точные имена директив — из документации
Набор INI-директив JIT (opcache.jit, opcache.jit_buffer_size, opcache.jit_debug и прочие) сверяйте с разделом OPcache в официальной документации для вашей минорной версии: часть флагов отладки и поведение по умолчанию менялись между релизами 8.x.
Где подход ломается
- Отладка усложняется. При включённом JIT сегфолты и странное поведение расширений отлаживать тяжелее: стек может не совпадать с ожидаемым. При диагностике проблем JIT временно отключают, чтобы исключить его из уравнения.
- Совместимость с расширениями. Некоторые профайлеры и отладчики (например, инструменты пошаговой отладки) плохо дружат с JIT-скомпилированным кодом. Проверяйте связку до включения на проде.
- Preloading и JIT. Взаимодействие
opcache.preloadи JIT имеет тонкости; если используете preloading, тестируйте комбинацию отдельно. - Память. На хостах с жёстким лимитом RAM буфер JIT поверх OPcache может подтолкнуть воркеры к OOM. Считайте:
memory_consumption+jit_buffer_sizeна каждый пул.
Что делать дальше
Прежде чем трогать JIT, снимите профиль. Если запросы упираются в базу и I/O — оптимизируйте запросы, добавьте индексы, кешируйте результаты; JIT здесь бесполезен. Если у вас есть счётный воркер или CPU-bound задача — вынесите её в отдельный процесс, включите tracing, замерьте на реальной нагрузке и оставьте JIT только там, где перцентили действительно улучшились. Инвалидация OPcache и стратегия рестарта воркеров под деплоем — отдельная тема, здесь мы её не разворачивали.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: PHP Manual — OPcache configuration (JIT), PHP RFC: JIT
Частые вопросы
У меня PHP 8.1, а не 8.3 — выводы те же?
Включил tracing, а прироста нет и CPU не упал — что не так?
Как понять, что JIT вообще активировался?
Стоит ли включать JIT на боевом веб-приложении по умолчанию?
JIT конфликтует с моим профайлером или отладчиком?
Какой размер jit_buffer_size выбрать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.