OPcache, JIT и почему PHP не такой уж медленный

Вопрос, где важно не перепутать кэш опкодов, кэш данных и JIT-компиляцию.

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

Вопрос 1: «Что такое опкоды и зачем OPcache?»

Что проверяет интервьюер

Понимаете ли вы, что PHP не «читает файл построчно», а работает в два этапа: компиляция исходника в промежуточное представление (опкоды) и исполнение этих опкодов виртуальной машиной Zend. В модели shared-nothing без кэша первый этап повторялся бы для каждого файла на каждый запрос — а это лексический разбор, парсинг и генерация кода для сотен файлов фреймворка.

# посмотреть опкоды файла, не выполняя его
php -d opcache.enable_cli=1 \
    -d opcache.opt_debug_level=0x10000 \
    script.php

OPcache кладёт результат компиляции в разделяемую память один раз, и все воркеры FPM его переиспользуют. Реальный эффект на типичном фреймворковом приложении — ускорение в 2–3 раза и заметное падение нагрузки на CPU. Это первое, что проверяют на «медленном» проде.

Минимальная боевая конфигурация

; php.ini
opcache.enable=1
opcache.memory_consumption=256      ; МБ под опкоды
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000 ; больше, чем файлов в проекте
opcache.validate_timestamps=0       ; на проде: не проверять mtime файлов
opcache.revalidate_freq=0
opcache.jit_buffer_size=64M
opcache.jit=tracing

Ключевая строка — validate_timestamps=0. С ней PHP перестаёт при каждом обращении проверять время изменения файла, но за это приходится платить: после деплоя кэш надо сбросить самому — перезапуском FPM или вызовом opcache_reset(). На локальной машине значение оставляют равным 1, иначе правки не будут видны.

Вопрос 2: «Что такое preloading?»

Preloading (PHP 7.4) идёт дальше кэша опкодов: указанный скрипт выполняется один раз при старте мастер-процесса и загружает классы в память навсегда — до перезапуска FPM. Такие классы доступны всем запросам без единого обращения к автозагрузчику и файловой системе.

<?php
// preload.php, указанный в opcache.preload
$dir = new RecursiveDirectoryIterator(__DIR__ . '/vendor');
foreach (new RecursiveIteratorIterator($dir) as $file) {
    if ($file->getExtension() === 'php') {
        opcache_compile_file($file->getPathname());
    }
}

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

Вопрос 3: «Ускорит ли JIT моё веб-приложение?»

Почти наверняка нет — и это правильный ответ. JIT (PHP 8.0) компилирует горячие опкоды в машинный код прямо во время выполнения. Он даёт кратный прирост на вычислительных задачах: обработке изображений, математике, длинных циклах. Типичное веб-приложение почти всё время ждёт базу, Redis и внешние API, поэтому на нём JIT добавляет считанные проценты, а иногда и ничего.

ТехнологияЧто кэширует/делаетГде выигрыш
OPcacheскомпилированные опкоды файловвсегда, обязателен на проде
Preloadingклассы в памяти мастер-процессаединицы процентов
JITмашинный код горячих участковматематика, циклы, CLI
Кэш данных (Redis)результаты запросов и вычисленийобычно самый большой

Сильный кандидат добавит: прежде чем спорить о JIT, надо посмотреть в профилировщик (Xdebug, Blackfire, Tideways) и в лог медленных запросов базы. Как правило, 80% времени лежит в N+1-запросах и в отсутствующих индексах, а не в самом PHP.

Типичные ошибки кандидатов

  • Считают, что OPcache кэширует результаты работы скрипта или ответы страниц. Он кэширует только опкоды.
  • Забывают сбросить кэш после деплоя при validate_timestamps=0 и получают «старый код на проде» — очень частая авария.
  • Путают OPcache (общий для воркеров кэш опкодов) с APCu (кэш пользовательских данных в разделяемой памяти).
  • Обещают «включим JIT и всё полетит» без профилирования.
  • Не могут назвать opcache_get_status() и не знают, как посмотреть заполнение кэша и число промахов.

Как ответить кратко

«PHP компилирует исходник в опкоды и исполняет их на Zend VM. OPcache хранит опкоды в разделяемой памяти, чтобы не компилировать код на каждый запрос — это обязательная настройка прода, ускорение в разы; на проде ставят validate_timestamps=0 и сбрасывают кэш при деплое. Preloading держит классы в памяти мастер-процесса, JIT компилирует горячие участки в машинный код и заметен в основном на вычислительных задачах, а не в вебе, где узкое место — база».

Проверьте себя
1. Что именно хранит OPcache?
AHTML-ответы страниц
BСкомпилированные опкоды PHP-файлов в разделяемой памяти
CРезультаты запросов к базе данных
DПользовательские переменные между запросами
2. Что нужно сделать после деплоя, если на проде выставлено opcache.validate_timestamps=0?
AНичего, PHP сам заметит изменения файлов
BОчистить сессии пользователей
CСбросить кэш опкодов: перезапустить FPM или вызвать opcache_reset()
DПерезапустить nginx
3. На каких задачах JIT в PHP 8 даёт наибольший выигрыш?
AНа типичных веб-запросах с обращениями к базе
BНа отдаче статических файлов
CНа парсинге больших JSON, потому что JIT ускоряет ввод-вывод
DНа вычислительных задачах: математике, длинных циклах, обработке изображений