Zend Engine и opcache: что ревьюить в PHP-коде под нагрузкой
Как устройство компилятора PHP и кеша опкодов влияет на то, какие замечания в ревью действительно снижают нагрузку, а какие — микрооптимизация без эффекта.

На проде PHP 8.2 под FPM с 40 воркерами сервис держал 300 rps, а после релиза с десятком новых классов на автозагрузке время холодного ответа после деплоя выросло с 40 до 900 мс на первых запросах. Причина не в коде запроса, а в том, что opcache компилировал файлы заново, а вы не прогрели кеш. Это тот класс проблем, который не виден в юнит-тестах и почти не ловится линтером — но виден, если понимать, что происходит между «PHP прочитал файл» и «выполнил инструкцию».
Разберём, как устроен путь исполнения в Zend Engine, где здесь роль opcache и какие замечания в ревью и рефакторинг реально меняют поведение под нагрузкой, а какие — карго-культ вокруг микросекунд.
Что происходит между файлом и результатом
Когда PHP получает запрос на выполнение файла, он проходит несколько стадий. Понимание границ между ними — основа для того, чтобы отличать дорогие операции от дешёвых.
- Лексер и парсер — исходник превращается в AST (абстрактное синтаксическое дерево). Стоит процессорного времени, зависит от размера файла.
- Компилятор — AST превращается в набор опкодов (opcode array): низкоуровневых инструкций Zend VM вроде
ZEND_ADD,ZEND_INIT_FCALL,ZEND_DO_FCALL. - Zend VM — цикл-исполнитель, который проходит по массиву опкодов и выполняет их.
Ключевой момент: без opcache стадии 1 и 2 повторяются для каждого файла на каждый запрос. opcache сохраняет результат компиляции (опкоды) в разделяемой памяти и при следующем запросе отдаёт готовый opcode array, пропуская лексер, парсер и компилятор.
Opcache не ускоряет ваш код. Он убирает повторную компиляцию. Если у вас медленный SQL-запрос в цикле, opcache не спасёт — но если у вас 2000 файлов на автозагрузке, он экономит десятки миллисекунд на каждом запросе.
Для ревью это значит: замечания о том, «сколько раз выполняется код», важнее замечаний о том, «как красиво он написан», ровно там, где код на горячем пути.
opcode — что это на практике
Опкод — единица работы виртуальной машины Zend1. Одна строка PHP компилируется в один или несколько опкодов. Посмотреть, во что превращается ваш код, можно расширением VLD или через opcache_get_status() для статистики кеша. Заглядывать в опкоды в обычном ревью не нужно — но полезно один раз увидеть, что $a[$b][$c] и предварительно сохранённая ссылка на подмассив дают разное число инструкций.
Что реально ревьюить: горячий путь
Правило простое: оптимизировать имеет смысл то, что выполняется часто. Разделите код на три зоны и применяйте разную строгость.
| Зона | Пример | Что смотреть в ревью |
|---|---|---|
| Горячий путь | цикл по 10 000 строк, обработчик каждого запроса | число итераций, вызовы функций внутри цикла, лишние аллокации |
| Тёплый путь | бутстрап, инициализация контейнера | число подключаемых файлов, автозагрузка |
| Холодный путь | админка, крон раз в сутки | читаемость важнее скорости |
Вычисления в условии цикла
Классическое замечание, которое действительно снижает нагрузку. Разница — в том, что count() вызывается на каждой итерации.
// Плохо: count() выполняется на каждой итерации
for ($index = 0; $index < count($items); $index++) {
process($items[$index]);
}
// Лучше: длина вычислена один раз
$total = count($items);
for ($index = 0; $index < $total; $index++) {
process($items[$index]);
}
// Ещё лучше на массивах — foreach без индексной арифметики
foreach ($items as $item) {
process($item);
}На PHP 8.x foreach по массиву обычно даёт наименьшее число опкодов и не требует ручного контроля индекса. Замечание в ревью формулируйте конкретно: «count() в условии цикла — вынести за цикл или заменить на foreach», а не «оптимизируйте цикл».
Повторное разыменование
// Плохо: движок каждый раз идёт по цепочке ключей
for ($i = 0; $i < $total; $i++) {
$result[] = $config['database']['connections']['default']['host'];
}
// Лучше: разыменовали один раз
$host = $config['database']['connections']['default']['host'];
for ($i = 0; $i < $total; $i++) {
$result[] = $host;
}Здесь дело не в стиле, а в том, что каждое обращение к вложенному ключу — это отдельные опкоды выборки. Внутри цикла на тысячи итераций это заметно.
Чего НЕ стоит требовать в ревью
Часть популярных «оптимизаций» на современном PHP не даёт измеримого эффекта, а иногда вредит читаемости. Не ставьте такие замечания как блокирующие.
- Одинарные vs двойные кавычки. Разница между
'строка'и"строка"без интерполяции на PHP 8.x пренебрежимо мала. Требовать замену — трата времени ревьюера. - Микрооптимизация вне горячего пути. Оптимизировать код, который выполняется раз в сутки по крону, — минус к читаемости без выигрыша.
- Замена коротких функций ручными реализациями. Переписывать
in_array()на свой цикл «ради скорости» обычно даёт обратный эффект и ломает предсказуемость.
Прежде чем требовать оптимизацию, задайте вопрос автору PR: этот код на горячем пути? Если нет — читаемость выигрывает.
Рефакторинг под opcache: чего он касается
opcache кеширует результат компиляции, но есть код, который выполняется при каждом запросе, даже если файл в кеше. Это тело функций, которое вызывается, и код на верхнем уровне подключённых файлов. Опкоды закешированы — но VM всё равно их исполняет.
Автозагрузка и число файлов
Каждый подключаемый класс — это файл, который opcache должен держать в памяти. Если у вас 5000 классов, а opcache.max_accelerated_files настроен на 2000, часть файлов будет вытесняться и перекомпилироваться. Проверьте фактическое число:
// Число закешированных ключей и промахи
$status = opcache_get_status(true);
echo $status['opcache_statistics']['num_cached_keys'], PHP_EOL;
echo $status['opcache_statistics']['opcache_hit_rate'], PHP_EOL;Значение hit rate ниже примерно 99% на стабильном проде — сигнал, что файлов больше, чем помещается в кеш, или память кеша мала. Точные рекомендуемые значения параметров зависят от размера кодовой базы — сверяйтесь с разделом opcache в официальной документации PHP для вашей версии.
Прогрев после деплоя
Проблема из первого абзаца — про это. После атомарного деплоя с новым каталогом релиза opcache видит новые пути к файлам и компилирует их заново на первых запросах. Варианты:
- Прогреть кеш скриптом, который обходит основные точки входа сразу после деплоя, до переключения трафика.
- Использовать
opcache.preload(доступен с PHP 7.4) — файл, который загружается при старте FPM и держит указанные классы в памяти на всё время жизни процесса. Синтаксис и ограничения preload смотрите в документации: там есть нюансы с зависимостями классов.
В ревью PR, меняющего схему деплоя или структуру автозагрузки, вопрос «как прогревается opcache после релиза» — не придирка, а прямое влияние на latency первых запросов.
Где подход ломается
Знание опкодов помогает не всегда, и есть места, где интуиция подводит.
- JIT. С PHP 8.0 есть JIT-компилятор, который может компилировать опкоды в машинный код. Он даёт выигрыш на CPU-bound задачах (математика, обработка данных), но почти ничего — на типичном web-приложении, где узкое место — база и сеть. Не ждите от включения JIT ускорения запросов к БД.
- opcache и файлы, меняющиеся в рантайме. Если
opcache.validate_timestampsвыключен (как рекомендуют для прода), изменённый на диске файл НЕ подхватится без явного сброса кеша или перезапуска FPM. Забытый сброс после деплоя = старый код на проде. - Микрозамеры без нагрузки. Замеры на локальной машине в один поток не отражают поведение под конкуренцией за разделяемую память opcache. Мерьте на стенде, приближенном к проду.
Как мерить, а не гадать
Прежде чем принимать или отклонять «оптимизирующее» замечание, замерьте.
- Профилируйте горячий путь профайлером (Xdebug в режиме profile или сэмплирующий SPX) — он покажет, где реально уходит время, и часто это не то, на что вы смотрели в ревью.
- Смотрите
opcache_get_status()на проде: hit rate, число ключей, использование памяти и рестарты кеша (out of memory restarts). - Нагрузочный тест до и после рефакторинга на одинаковых данных и параллельности — иначе «стало быстрее» ничем не подтверждено.
Что делать дальше
Начните с малого: включите сбор статистики opcache на проде и посмотрите hit rate и число рестартов кеша за сутки. Если рестарты есть — это громче любого замечания про кавычки. Дальше настройте прогрев или preload под вашу схему деплоя и договоритесь в команде о правиле ревью: блокирующие замечания по производительности — только с указанием, что код на горячем пути, и по возможности с замером. Всё остальное — necessary evil читаемости, а не скорости.
IT-архитектор / SQL-оптимизатор: Поиск N+1, оптимизация запросов, code review, поиск багов.
Спросить за 20 ₽Источники: PHP Manual: OPcache Configuration, PHP Manual: opcache_get_status
Частые вопросы
У меня PHP 7.4, а не 8.x — применимо ли всё это?
Я включил opcache, но после деплоя всё равно старый код. Что не так?
Стоит ли включать JIT ради ускорения сайта?
Микрооптимизации внутри цикла — это преждевременная оптимизация?
Как понять, что opcache настроен неправильно?
Нужно ли смотреть опкоды в обычном код-ревью?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.