Memoize PhpMethodReflection::getName() and return the right operand on a TypeCombinatorCache memo hit - #6489
Merged
Conversation
…ss members PhpMethodReflection::getName() memoizes its result (964K calls per self-analysis run recomputed strtolower() each time). The vendored BetterReflection fixes are shipped as turbo-ext/poc/better-reflection-reflection-memo.patch (vendor/ is git-ignored): ReflectionFunctionAbstract::getName() memo, getAttributesByName()/filterAttributesByName() short-circuit on the attribute-less case, and the cached-members checks hoisted in front of AlreadyVisitedClasses::createEmpty() in ReflectionClass. Measured together: -1.6% user CPU on the self-analysis A/B, byte-identical output. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
A memo hit handed back the result object of the call that populated the entry. When that result was one of its operands - TypeCombinator returns an operand for a union() of equal types or a remove() of nothing - the new call got the old call's operand instead of its own, and identity tests on the result flipped: ArrayType::setExistingOffsetValueType() reads `union(...) === $this->itemType` as "nothing was written". Such an entry now records the operand's position in the result pointer's alignment bits (the slot stays 24 bytes) and a hit returns the operand at that position of the call at hand. One object passed at two positions leaves undecided which one a structurally equal call should return, so that call is not memoized. The divergence is latent on this branch - the analysis output of src/Type and src/Analyser is identical with the extension on or off either way - because the callers that test the identity are still PHP and hit the operand through their own call. The smoke test covers it directly: both memo-hit checks fail without the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN
staabm
reviewed
Sep 20, 2026
| @@ -0,0 +1,21 @@ | |||
| # Proof-of-concept patches for vendored packages | |||
Contributor
There was a problem hiding this comment.
I think the poc folder was not meant to be committed?
Member
Author
There was a problem hiding this comment.
Yeah probably not, will fix 😊
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent changes to the reflection and type-combinator hot paths.
PhpMethodReflection::getName()memo964K calls per self-analysis run recomputed
strtolower()on the same name. The result is memoized in a?string $namefield.The matching vendored-package fixes ship as
turbo-ext/poc/better-reflection-reflection-memo.patch(a newturbo-ext/poc/directory —vendor/is git-ignored, so a PHP fix in a vendored package cannot be committed):ReflectionFunctionAbstract::getName()memo,getAttributesByName()/filterAttributesByName()short-circuit on the attribute-less case, and the cached-member checks hoisted in front ofAlreadyVisitedClasses::createEmpty(). Measured together: −1.6% user CPU on the self-analysis A/B, byte-identical output. They want a separate PR againstondrejmirtes/better-reflection; the patch file is here so the measurement can be reproduced and reviewed.TypeCombinatorCachememo-hit identityA memo hit handed back the result object of the call that populated the entry. When that result was one of its operands —
TypeCombinatorreturns an operand for aunion()of equal types or aremove()of nothing — the new call got the old call's operand instead of its own, and identity tests on the result flipped.ArrayType::setExistingOffsetValueType()readsunion(...) === $this->itemTypeas "nothing was written".Such an entry now records the operand's position in the result pointer's alignment bits (the slot stays 24 bytes) and a hit returns the operand at that position of the call at hand. One object passed at two positions leaves undecided which one a structurally equal call should return, so that call is not memoized.
The divergence is latent today: the analysis output of
src/Typeandsrc/Analyseris identical with the extension on or off either way, because the callers that test the identity are PHP and reach the operand through their own call. The four new smoke checks cover it directly at the unit level.Verification
turbo-ext/tests/smoke.php: fails before the fix (TCC: a memo hit returns the operand of the call at hand,TCC: a memo hit on removing nothing returns the operand of the call at hand), passes after.signature-parity.php(79 methods),side-by-side.php(4 classes paired): OK.make phpstan: no errors.make lint,make cs: clean.make testswith the extension loaded and active (version gate verified viabin/phpstan diagnose): 22016 tests, 97656 assertions, OK (65 skipped).🤖 Generated with Claude Code
https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN