0

PHP 8.0 — JIT compiler: what it is and when it helps

Advanced5 min read·php-09-001
interviewperformance

Concept

PHP 8.0 introduced the JIT (Just-In-Time) compiler as part of OPcache. It compiles hot bytecode instructions directly to native machine code at runtime, bypassing the PHP VM's opcode interpreter for those paths.

How PHP normally executes: Source → Lexer/Parser → AST → Opcodes → OPcache (opcodes cached) → Zend VM interprets opcodes → CPU. The Zend VM is a dispatch loop in C that reads each opcode and calls the corresponding handler.

What JIT adds: For frequently executed code paths ("hot" paths), JIT compiles opcodes to native x86-64 (or ARM64) machine code. The next time that path runs, the CPU executes it directly — no VM dispatch overhead. JIT uses a call graph (call-based tracing, not method JIT) to find hot functions.

Types of JIT in PHP 8: opcache.jit = tracing compiles traces across function boundaries (most aggressive). opcache.jit = function compiles function-by-function (more conservative). opcache.jit = disable or 0 turns it off.

When JIT helps vs doesn't help: JIT most benefits CPU-bound code — mathematical computations, image processing, data serialization. It barely helps I/O-bound PHP web apps — if your request spends 80% of time waiting for the database, JIT doesn't speed up the database wait. Real-world benchmarks: web frameworks see 5-15% improvement; pure computation benchmarks see 2-5× improvement.

Configuration: Requires opcache.enable=1, opcache.jit_buffer_size=128M (or larger), opcache.jit=tracing. In Laravel Octane (long-running process), JIT warm-up is amortized across many requests.

Code Example

php
<?php
declare(strict_types=1);

// CPU-intensive benchmark — JIT shines here
function mandelbrot(float $cReal, float $cImag, int $maxIter = 100): int
{
    $zReal = $zImag = 0.0;
    for ($i = 0; $i < $maxIter; $i++) {
        $zReal2 = $zReal * $zReal;
        $zImag2 = $zImag * $zImag;
        if ($zReal2 + $zImag2 > 4.0) return $i;
        $zImag  = 2.0 * $zReal * $zImag + $cImag;
        $zReal  = $zReal2 - $zImag2 + $cReal;
    }
    return $maxIter;
}

$start = hrtime(true);
$sum   = 0;
for ($x = -2.0; $x <= 1.0; $x += 0.01) {
    for ($y = -1.5; $y <= 1.5; $y += 0.01) {
        $sum += mandelbrot($x, $y);
    }
}
$ms = (hrtime(true) - $start) / 1_000_000;
echo "Mandelbrot: {$ms}ms (sum={$sum})\n";
// Without JIT: ~1200ms
// With JIT (tracing): ~300ms — 4× faster for pure computation

// Checking JIT status
if (function_exists('opcache_get_status')) {
    $status = opcache_get_status();
    if ($status && isset($status['jit'])) {
        var_dump($status['jit']['enabled']);    // bool
        var_dump($status['jit']['buffer_free']); // free JIT buffer
    }
}

// I/O-bound code — JIT barely helps
function ioHeavy(): void
{
    // JIT can't speed up waiting for database/network/filesystem
    $result = file_get_contents('https://example.com'); // I/O wait dominates
    $data   = json_decode($result); // this part might benefit slightly
}

// php.ini settings for JIT:
// opcache.enable=1
// opcache.enable_cli=1  (for CLI scripts)
// opcache.jit=tracing   (or 1255 — a numeric form: tracing mode)
// opcache.jit_buffer_size=128M