Repository navigation
Conversation
A final class cannot be extended, and an enum is always final, so a final or enum class entry can never be an ancestor of the left hand side. Return false immediately instead of walking the parent chain. Internal enums created by zend_register_internal_enum() only set ZEND_ACC_ENUM, so the check tests both ZEND_ACC_FINAL and ZEND_ACC_ENUM. The interface branch is left untouched, since interfaces can never be final.
61cdef3 to
0df488e
Compare
|
Closing this one. The change is correct and the test is solid, but the win does not survive a realistic look at PHP code, so it is not worth adding a branch to the common path. What was measuredPHP 8.7.0-dev, macOS arm64, opcache CLI on, JIT off, 30M iterations per case, median of 12 paired runs. Each comparison was also run in both orders, to cancel a one percent ordering effect found with a baseline against baseline control. Per operation, the patch saves:
Why it does not pay offThe gain grows with the depth of the left hand side hierarchy. The cost is fixed, one extra flag test before the walk, and it lands on every non final right hand side that reaches the slow function. Real projects use class hierarchies 0 to 2 levels deep, so the gain stays in the 0.37 to 0.6 ns range while the cost stays at 0.14 ns. The break-even point is about one final or enum right hand side for every three non final ones. The net effect is under a nanosecond per operation in either direction, and the sign depends on the mix. The cases that gain the most, a deep hierarchy with a final class on the right, are the least common, and interface right hand sides, which are the most common in frameworks, are unaffected. The test
It cannot catch dropping the The test stays in the branch and can be reused as is, if this is ever approached again from the opcode handler side, where the check could skip the call to |
$x instanceof FinalClasswalks the whole parent chain of$xbefore 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 testsZEND_ACC_FINAL|ZEND_ACC_ENUMbecause internal enums created byzend_register_internal_enum()set onlyZEND_ACC_ENUM, for exampleRoundingModeandSortDirection.The inline helper
instanceof_function()was not used for the check, becauseZEND_ACC_FINALis defined inzend_compile.h, whichzend_operators.hdoes not include, and adding it there would create an include cycle throughzend_globals.h. Putting the check in theZEND_INSTANCEOFopcode 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. The two binaries run back to back inside the same pair, and the reported number is the ratio of the medians.
The final misses become flat at about 100 Mops/s whatever the depth, close to the hit cost. The win grows with the depth of the left hand side hierarchy, which is the realistic case in frameworks. At depth 1 the only saving is a single pointer load, so the gain is about 2 percent.
Under JIT tracing the same direction holds, about 10 percent at depth 3 and 15 percent at depth 6, but that mode is much noisier and has no dedicated
ZEND_INSTANCEOFcodegen.Overhead on the non final paths
The new test runs before the walk, so it also runs for every non final right hand side that reaches
instanceof_function_slow(), both on a miss and on a hit found further up the chain. That is the common shape in general PHP code, so it was measured on its own.A control run of the baseline against itself showed that the second binary of a pair is consistently about 1 percent faster, on all four cases. To cancel that ordering effect, each comparison was run in both orders, 12 pairs in one order and 10 in the other, and the two results were combined.
The cost is about 1 to 1.5 percent, or 0.1 to 0.15 ns per operation. It has the same sign on all four cases and in both orders, so it is above the noise. The identity hit, the interface right hand side, and the final or enum right hand side do not pay it.
Trade-off
The gain applies to a final or enum right hand side on a left hand side deep enough for the walk to be visible, about 1.5 ns per operation at depth 6. The cost applies to every non final right hand side that reaches the slow function, about 0.15 ns per operation. In code where the right hand side is mostly non final, the patch is roughly neutral, and slightly negative at shallow depth. This is why the change stays small and does not touch the shared opcode handler.
Tests
The new test
Zend/tests/instanceof_final.phptcovers 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, andself,parentandstatic. It produces the same output on the patched and unpatched binaries. The wholeZend/testssuite passes with 0 failures.