Repository navigation
Free and look up less: native fast paths, shared scope services, reasonless result sharing - #6768
Merged
Merged
Conversation
pt_object_type_new() without a subtracted type or a handed-in class reflection hands back one ObjectType per class name: the value is the same, and the instance's caches (ancestors, parent, interfaces, the lazily resolved class reflection, the method cache) are computed once instead of per instance. The reflection belongs to the container, so the table is dropped with ObjectType::resetCaches(), which ContainerFactory calls for every new container, and at request shutdown. 652k ObjectType creations per single-process self-analysis were the largest group of type objects in the allocation census. Measured −0.92% instructions (sd 0.06%, n=4), user CPU −0.71% (noisy), identical output, peak memory unchanged. Leaf types (MixedType, NullType, ...) are not interned: that was measured at ~0 before and removed (#6355). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
TypeTraverser built a fresh [$this, 'mapInternal'] / [$this, 'traverseInternal'] array for every traversed type, and each native Type::traverse() resolved it by name through zend_fcall_info_init(). The traversals in progress now keep their two callables on a small stack, released when map() returns, and the traverse op fills the fci/fcc of such an array directly. A callable kept or called after its map() returned is built and resolved as before. Instructions retired on self-analysis: -1.01% (3 ABBA pairs), identical analysis output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
bin/phpstan disables the garbage collector, but the engine still records each array and object whose refcount drops to non-zero in the root buffer, which then only grows, and every free of a buffered value searches it to remove the entry (gc_remove_compressed once past 512K roots). Activation protects the buffer when the collector is off, so nothing is recorded. -0.58% instructions, -10MB peak memory on the single-process benchmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
Every printed, keyed or attribute-read node asked the by-name table, which hashes and compares the class name; a direct-mapped table by class entry in front of it answers them with one load. -0.59% instructions (median) on the single-process benchmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
map() created an instance per traversal (1.6M per self-analysis subset) and freed it when the traversal ended. An instance nothing outlived the traversal with is now reset to the constructor's state and handed to the next map(). -0.20% instructions on the single-process benchmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
Each transformStaticType() created a holder object and a Closure over one of its methods (a copy of the function included). A native closure is one object carrying the body and the captured values. -0.26% instructions on the single-process benchmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
The RecursionGuard and TemplateTypeMap::map() callbacks were a holder object plus a Closure over its __invoke(); a native closure replaces both, and the by-reference capture carries the offsetSet() value type back. -0.13% instructions on the single-process benchmark (within noise; it removes the holder's invocation path). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
A self-analysis created 1.4M results, nearly all without reasons, and and()/or()/extremeIdentity()/maxMin() merged three empty arrays per call into yet another one. createFromTrinaryLogic() returns the createYes() / createNo() / createMaybe() instance for a result, the call sites that spelled new IsSuperTypeOfResult($result, []) use it, and the combinators skip the merging when no operand has reasons and return a shared instance. -0.48% instructions, -0.60% cycles on the single-process benchmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
Every method call on a static receiver and every called-on prototype mapped each signature type through a TypeTraverser callback that changes StaticTypes only. Whether a type contains one is now asked once per type object - by the same traversal, answered from the type's native header afterwards - and a type without one is returned as is. -1.18% instructions, -0.71% cycles on the single-process benchmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
checkReturnType() asked VerbosityLevel::getRecommendedLevelByType() - a traversal of both types - for every return statement, and used the answer only in the messages of the errors it reports. -0.29% instructions on the single-process benchmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
resolveToBounds() and resolveToDefaults() traverse a type to replace its TemplateTypes and change nothing else. The cached contains-a-StaticType answer becomes a general one - whether a traversal reaches an instance of a class, remembered per type object under its own fact bit - and a type without a TemplateType is returned as is. -0.59% instructions on the single-process benchmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
Every MutatingScope carried the thirteen services, the expression result storage stack and the node callback in a property each - the same values in every scope of a factory, copied into each of the ~900K scopes of a self-analysis subset and released from each again. They now live in a ScopeServices object the factory builds once, and a scope holds one reference to it (37 property slots down to 23). -0.88% instructions, ~-3% cycles on the single-process benchmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X
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.
Twelve changes that make the analysis itself do less work: freeing objects, by-name lookups and type traversals. They came out of profiling the turbo-codegen branch but stand without it. Each touches hand-written turbo code or plain PHP only.
Full parallel self-analysis vs 2.3.x (turbo on, ShipMonk DCD off in both legs, 5 alternating pairs): user CPU −4.37% (51.47 → 49.22 s, faster 5/5), wall −2.81% (8.34 → 8.11 s, faster 5/5), main-process peak memory −1.9%. Identical output (no errors in either leg).
Native only
StaticType. Whether a type contains one is computed once per type object and kept in its native header.ObjectTypeper class name for native code, dropped withObjectType::resetCaches().resolveToBounds()/resolveToDefaults()skip types containing noTemplateType, using the same per-type cached answer.bin/phpstan), stop the engine buffering possible cycle roots that nothing will ever collect.Closure.PHP and native
ScopeServices: the thirteen services, the expression result storage stack and the node callback every scope carried in a property each now live in one object the scope factory builds once. MutatingScope goes from 37 property slots to 23, which saves copying and releasing them for every scope. −0.88% instructions, ~−3% cycles.IsSuperTypeOfResult::createFromTrinaryLogic()(new,@api): returns the sharedcreateYes()/createNo()/createMaybe()instance. The ~40 sites that spellednew IsSuperTypeOfResult($result, [])use it.and(),or(),extremeIdentity(),maxMin(),lazyMaxMin(),negate()anddecorateReasons()return the shared instance when no operand has reasons, and skip merging empty arrays. −0.48% instructions.FunctionReturnTypeCheckasksVerbosityLevel::getRecommendedLevelByType(), which traverses both types, only when it reports an error, instead of for everyreturn. −0.29% instructions.* Instructions retired on a single-process analysis of
src/Analyser src/Rules src/Type, each measured on its own in alternating pairs on the turbo-codegen branch.Verified on this branch: walk trace identical (664,909 lines), the whole suite with and without the extension (22,539 tests),
make phpstan, phpcs, the shared core identical across PHP 8.3–8.6 NTS/ZTS, clang-tidy.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMNnG2Zo7wgvSZCzjdk17X