Skip to content

Memoize PhpMethodReflection::getName() and return the right operand on a TypeCombinatorCache memo hit - #6489

Merged
ondrejmirtes merged 3 commits into
2.3.xfrom
memo-operand-identity-and-name-memo
Sep 20, 2026
Merged

ondrejmirtes merged 3 commits into
2.3.xfrom
memo-operand-identity-and-name-memo

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Sep 20, 2026

Copy link
Copy Markdown
Member

Two independent changes to the reflection and type-combinator hot paths.

PhpMethodReflection::getName() memo

964K calls per self-analysis run recomputed strtolower() on the same name. The result is memoized in a ?string $name field.

The matching vendored-package fixes ship as turbo-ext/poc/better-reflection-reflection-memo.patch (a new turbo-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 of AlreadyVisitedClasses::createEmpty(). Measured together: −1.6% user CPU on the self-analysis A/B, byte-identical output. They want a separate PR against ondrejmirtes/better-reflection; the patch file is here so the measurement can be reproduced and reviewed.

TypeCombinatorCache memo-hit identity

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 today: 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 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 tests with the extension loaded and active (version gate verified via bin/phpstan diagnose): 22016 tests, 97656 assertions, OK (65 skipped).

🤖 Generated with Claude Code

https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN

ondrejmirtes and others added 3 commits September 20, 2026 22:08
…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
@ondrejmirtes
ondrejmirtes merged commit c336a0c into 2.3.x Sep 20, 2026
829 of 893 checks passed
@ondrejmirtes
ondrejmirtes deleted the memo-operand-identity-and-name-memo branch September 20, 2026 20:24
Comment thread turbo-ext/poc/README.md
@@ -0,0 +1,21 @@
# Proof-of-concept patches for vendored packages

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the poc folder was not meant to be committed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah probably not, will fix 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants