A deep dive into the Zend Engine's bytecode execution explains how PHP compiles source code into opcodes and how the executor dispatches them. The Zend Engine ships five dispatch models (CALL, SWITCH, GOTO, HYBRID, TAILCALL), of which only CALL, HYBRID, and TAILCALL are actually used in production builds. The piece explains how each model interacts with CPU branch prediction, calling conventions, and register allocation, and traces how PHP 8.5 introduced TAILCALL to give Clang-built binaries the same performance as GCC-built HYBRID binaries. It also shows how the executor itself is generated from templates by a PHP script rather than hand-written.
Table of contents
From PHP code to opcodesWhat an executor has to doA detour through the CPUAn executor that is generatedCALL: one function call per instructionSWITCH: one very large switch statementGOTO: jumping straight into the next handlerHYBRID: separate functions plus computed gotoTAILCALL: handlers that jump to each otherWhich model does your PHP use?What each model is forWhat I took awayQuestions this post answers
What is the TAILCALL VM dispatch mode added in PHP 8.5?
TAILCALL is a new bytecode dispatch model added in PHP 8.5 that lets Clang-built PHP binaries match the performance of GCC's HYBRID mode without needing global register variables. It relies on guaranteed tail calls via the musttail attribute and the preserve_none calling convention, which keeps execute_data and opline pinned in registers across handler calls. Before it existed, Clang-built binaries were measured between 2.8% and 44% slower than GCC builds depending on the benchmark. Developers benchmarking PHP builds can track runtime internals like this on daily.dev.
Which PHP VM dispatch model does a Linux distribution's PHP package use, and which does macOS use?
Linux distribution packages typically use HYBRID because they are built with GCC, combining computed goto with global register variables pinned to specific CPU registers (%r14 and %r15 on x86-64). macOS and FreeBSD builds default to Clang, which lacks global register variable support, so they use TAILCALL instead, relying on guaranteed tail calls to achieve comparable performance since PHP 8.5. Anyone comparing PHP performance across platforms can follow these runtime details on daily.dev.
How can I check which Zend VM dispatch model my PHP build is using?
Since PHP 8.5, running `php -r 'echo ZEND_VM_KIND, PHP_EOL;'` prints the active dispatch model, such as ZEND_VM_KIND_HYBRID. The model is chosen at compile time by the C preprocessor based on build system probes for GCC global register variable support, the musttail attribute, and the preserve_none calling convention, falling back to CALL if none are available. Engineers debugging PHP performance differences can keep tabs on build-level details via daily.dev.