[php-src] PR #24370: Skip the parent-chain walk in instanceof for a final or enum RHS
| From: | GromNaN | Date: | Fri, 16 Oct 2026 08:54:30 +0000 |
| Subject: | [php-src] PR #24370: Skip the parent-chain walk in instanceof for a final or enum RHS | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39499@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24370
Author: GromNaN
$x instanceof FinalClass walks the whole parent chain of $x before it can
return false. Because a final class cannot be extended, and an enum is always final, such a right
hand side can never be an ancestor of the left hand side, so the walk can stop immediately.
The check is added in instanceof_function_slow(), in the branch that walks the parent
chain. The interface branch is left untouched, since interfaces can never be final. The mask tests
ZEND_ACC_FINAL|ZEND_ACC_ENUM because internal enums created by
zend_register_internal_enum() set only ZEND_ACC_ENUM, for example
RoundingMode and SortDirection.
The inline helper instanceof_function() was not used for the check, because
ZEND_ACC_FINAL is defined in zend_compile.h, which
zend_operators.h does not include, and adding it there would create an include cycle
through zend_globals.h. Putting the check in the ZEND_INSTANCEOF opcode
handler was also tried, but it slows the shared handler on the non final paths by about 6 to 8
percent.
Benchmarks, PHP 8.7.0-dev on macOS arm64, opcache CLI on, JIT off, inline loops, 30M iterations per
case, median of 12 paired runs:
| case | baseline | patched | ratio |
| --- | --- | --- | --- |
| final hit | 104.4 Mops/s | 103.6 Mops/s | 0.997x |
| final miss, depth 1 | 96.5 | 100.1 | 1.023x |
| final miss, depth 3 | 92.7 | 99.9 | 1.078x |
| final miss, depth 6 | 87.1 | 100.7 | 1.147x |
| enum miss | 97.9 | 99.8 | 1.027x |
| interface miss | 96.9 | 95.3 | 0.998x |
| plain hit | 103.3 | 103.2 | 1.003x |
| plain miss | 97.2 | 95.9 | 0.983x |
The final misses become flat at about 100 Mops/s whatever the depth, close to the hit cost. The
noise floor, measured with the same protocol on the baseline against itself, is about 2 percent. The
depth 1 case only gains about 2 percent, because at depth 1 the only saving is a single pointer
load. The win grows with the depth of the left hand side hierarchy, which is the realistic case in
frameworks. Under JIT tracing the same direction holds, but that mode is much noisier and has no
dedicated ZEND_INSTANCEOF codegen.
The new test Zend/tests/instanceof_final.phpt covers a final class hit and miss, a
final class that implements an interface, a userland enum, an internal enum, a readonly final class,
non object left operands, an undefined right operand, a variable right operand, and
self, parent and static. It produces the same output on the
patched and unpatched binaries. The whole Zend/tests suite passes with 0 failures.