Mid📖Теория5 min

Производительность

OPcache, JIT-компиляция, realpath_cache, memory_limit, max_execution_time и оптимизация PHP 8.4

Настройки производительности

OPcache -- кеширование байткода

OPcache -- встроенное расширение PHP, которое кеширует скомпилированный байткод скриптов в разделяемой памяти. Без OPcache PHP каждый запрос читает файл, парсит его, компилирует в опкоды и выполняет. С OPcache этапы чтения, парсинга и компиляции пропускаются.

Базовая настройка

[opcache]
; Enable OPcache (required)
opcache.enable = 1

; Enable for CLI scripts (useful for long-running workers, tests)
opcache.enable_cli = 0

; Shared memory size in MB for storing compiled scripts
; Default: 128. Increase for large projects (Symfony/Laravel: 256+)
opcache.memory_consumption = 256

; Memory for interned strings (class names, function names, comments)
; Default: 8. Increase for projects with many classes
opcache.interned_strings_buffer = 32

; Maximum number of files that can be cached
; Default: 10000. Symfony app typically has 15000-30000 files
; Use prime number close to your actual file count
opcache.max_accelerated_files = 30000

; How often to check if files changed (seconds)
; 0 = check every request, 2 = default
; Production: disable entirely with validate_timestamps=0
opcache.revalidate_freq = 2

Ключевая production-директива

; Production: NEVER check if source files changed
; Requires PHP-FPM restart/reload after deployment
opcache.validate_timestamps = 0

; Development: check on every request
opcache.validate_timestamps = 1
opcache.revalidate_freq = 0

Критически важно: При validate_timestamps=0 PHP никогда не перечитывает исходные файлы. После деплоя нужен php-fpm reload или вызов opcache_reset(). Это даёт значительный прирост производительности -- нет stat() системных вызовов.


JIT-компиляция (PHP 8.0+)

JIT (Just-In-Time) компилирует PHP-байткод в машинный код процессора. Наибольший эффект на CPU-интенсивных задачах (математика, обработка данных). Для типичных веб-приложений (I/O-bound) эффект минимальный.

Конфигурация JIT

; Enable JIT (requires opcache.enable=1)
; Available modes in PHP 8.4:
opcache.jit = tracing

; Memory buffer for JIT compiled code (in MB)
; Separate from opcache.memory_consumption!
opcache.jit_buffer_size = 128M

; JIT debug: 0 = disabled, useful for troubleshooting
opcache.jit_debug = 0

Режимы JIT в PHP 8.4

В PHP 8.4 конфигурация JIT упрощена по сравнению с 8.0-8.3:

; PHP 8.4 simplified JIT modes:
opcache.jit = disable        ; JIT completely off
opcache.jit = tracing        ; Best for web apps (traces hot paths)
opcache.jit = function       ; Compiles entire functions

; PHP 8.0-8.3 legacy numeric format (still supported):
; opcache.jit = 1255          ; Tracing JIT
; opcache.jit = 1205          ; Function JIT
Режим Лучше для Описание
disable Отладка, совместимость JIT полностью выключен
tracing Веб-приложения Отслеживает горячие пути, оптимизирует их
function CLI, математика Компилирует функции целиком

Когда JIT помогает, а когда нет

<?php

// JIT helps: CPU-intensive computation
function fibonacci(int $n): int
{
    if ($n <= 1) return $n;
    return fibonacci($n - 1) + fibonacci($n - 2);
}
// With JIT: ~40-60% faster for pure computation

// JIT doesn't help much: I/O-bound web apps
function getUser(PDO $db, int $id): array
{
    // Bottleneck is database query, not PHP code
    $stmt = $db->prepare('SELECT * FROM users WHERE id = ?');
    $stmt->execute([$id]);
    return $stmt->fetch(PDO::FETCH_ASSOC);
}

PHP 8.4: Влияние property hooks на OPcache

<?php

// PHP 8.4 property hooks -- cached by OPcache like regular methods
class Product
{
    public string $slug {
        set(string $value) => strtolower(trim($value));
        get => $this->slug;
    }

    public float $priceWithTax {
        get => round($this->price * 1.2, 2);
    }

    public function __construct(
        public readonly float $price,
    ) {}
}

// Property hooks generate additional opcodes
// Increase opcache.memory_consumption if using extensively

Мониторинг OPcache

<?php

// Get OPcache status
$status = opcache_get_status();

// Key metrics
$memory = $status['memory_usage'];
echo "Used: " . round($memory['used_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Free: " . round($memory['free_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Wasted: " . round($memory['wasted_percentage'], 2) . "%\n";

$stats = $status['opcache_statistics'];
echo "Cached files: " . $stats['num_cached_scripts'] . "\n";
echo "Cache hits: " . $stats['hits'] . "\n";
echo "Cache misses: " . $stats['misses'] . "\n";
echo "Hit rate: " . round($stats['opcache_hit_rate'], 2) . "%\n";

// JIT status (PHP 8.0+)
$jit = $status['jit'];
echo "JIT enabled: " . ($jit['enabled'] ? 'yes' : 'no') . "\n";
echo "JIT buffer size: " . round($jit['buffer_size'] / 1024 / 1024, 2) . " MB\n";
echo "JIT buffer used: " . round($jit['buffer_used'] / 1024 / 1024, 2) . " MB\n";

Сброс кеша OPcache

<?php

// Full reset -- clears ALL cached scripts
opcache_reset();

// Invalidate single file
opcache_invalidate('/path/to/file.php', true);

// In deployment script:
// 1. Deploy new code
// 2. Call opcache_reset() via HTTP or CLI
// 3. Optionally preload with opcache_compile_file()

Preloading (PHP 7.4+)

Preloading загружает файлы в OPcache при старте PHP-FPM и хранит их постоянно в памяти.

; Path to preload script (runs once at FPM start)
opcache.preload = /var/www/app/config/preload.php

; User for preloading (required on Linux)
opcache.preload_user = www-data
<?php
// config/preload.php

// Preload framework core classes
require_once __DIR__ . '/../vendor/autoload.php';

// Symfony preload example
if (file_exists(__DIR__ . '/../var/cache/prod/App_KernelProdContainer.preload.php')) {
    require __DIR__ . '/../var/cache/prod/App_KernelProdContainer.preload.php';
}

Ограничение: Preloading работает только с PHP-FPM. Изменение предзагруженных файлов требует полного рестарта FPM (не reload).


realpath_cache -- кеш файловых путей

PHP кеширует результаты realpath() -- преобразования относительных путей в абсолютные и проверки существования файлов.

; Size of realpath cache per process
; Default: 4096K. Increase for projects with many files
realpath_cache_size = 4096K

; TTL in seconds
; Default: 120. Production: increase to 600+
realpath_cache_ttl = 600
<?php

// Check current realpath cache usage
$info = realpath_cache_size();
echo "Realpath cache used: " . round($info / 1024, 2) . " KB\n";

// View cached entries
$entries = realpath_cache_get();
echo "Cached paths: " . count($entries) . "\n";

Связь с OPcache: При opcache.validate_timestamps=0 realpath_cache менее критичен, так как OPcache не проверяет файлы. Но Composer autoload всё равно использует realpath().


memory_limit и max_execution_time

Расчёт memory_limit

; Web applications: 128M-256M for most cases
memory_limit = 256M

; API endpoints: 64M-128M (simple operations)
; CLI workers/queues: 256M-512M (data processing)
; Import/export: 512M-1G (large files)
; NEVER use -1 in production!
<?php

// Formula for PHP-FPM:
// memory_limit * pm.max_children <= Available_RAM

// Example: 256M * 50 children = 12.8 GB needed
// Leave RAM for OS, database, Redis, etc.

// Monitor actual usage
echo "Current: " . round(memory_get_usage(true) / 1024 / 1024, 2) . " MB\n";
echo "Peak: " . round(memory_get_peak_usage(true) / 1024 / 1024, 2) . " MB\n";

max_execution_time

; Web: 30 seconds default (good for most apps)
max_execution_time = 30

; CLI: 0 (unlimited) -- this is default for CLI SAPI
; max_execution_time counts ONLY CPU time
; Does NOT count: sleep(), database queries, file I/O, network waits
<?php

// set_time_limit() RESETS the timer from current moment
set_time_limit(30); // 30 seconds from NOW

// ini_set() sets from script START (does not reset)
ini_set('max_execution_time', '30');

// TRAP: set_time_limit() in a loop = infinite execution
while ($item = $queue->next()) {
    set_time_limit(30); // Timer resets each iteration!
    process($item);     // Script never times out
}

Полная production-конфигурация OPcache

[opcache]
; Core
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 30000

; Validation -- DISABLE for production
opcache.validate_timestamps = 0
opcache.revalidate_freq = 0

; Optimization
opcache.save_comments = 1
opcache.fast_shutdown = 1
opcache.optimization_level = 0x7FFEBFFF
opcache.huge_code_pages = 1

; JIT (PHP 8.4)
opcache.jit = tracing
opcache.jit_buffer_size = 128M

; Preloading
opcache.preload = /var/www/app/config/preload.php
opcache.preload_user = www-data

; Realpath cache
realpath_cache_size = 4096K
realpath_cache_ttl = 600

Проверь себя

🧪

Чем `set_time_limit(30)` отличается от `ini_set('max_execution_time', '30')`?

🧪

Для каких задач JIT-компиляция даёт наибольший прирост?

🧪

Что необходимо для работы preloading в PHP?

🧪

Считает ли `max_execution_time` время ожидания запроса к базе данных?

🧪

Что происходит при `opcache.validate_timestamps = 0`?