Repository navigation
Dispatch native Type calls directly and build the extension profile-guided - #6508
Merged
Merged
Conversation
| - name: "Compile phpstan_turbo with strict warnings" | ||
| # The training run of `make pgo` (bin/pgo-train.sh) runs bin/phpstan on | ||
| # the checkout, so the dependencies must be in place before the build. | ||
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 |
…uCache ClassReflectionAccess.cpp answers getName()/isGeneric()/getCacheKey()/ hasMethod()/hasFinalByKeywordOverride()/getNativeReflection()/isEnum() on an exact ClassReflection from the twin's private slots (offsets resolved once per class entry, forgotten at rinit) and MutatingScope::isInClass()/ getClassReflection() from a native ScopeContext's slot when the scope is exactly a MutatingScope; anything the slot cannot answer still calls the PHP method. Class-map keys classReflection, mutatingScope and the vendored reflectionEnum are appended (looked up without autoloading, like instanceof); the lruCache key is gone: PHPStan\Internal\LruCache is shadowed by LruCache.cpp (symtable keys, count/weight eviction with the floor, the int-overflow TypeError) with pt_lru_cache_* helpers and a smoke.php differential. reg.h gains a typed int property builder and mixedArg(). Self-analysis census: Type-kernel->PHP calls 12.43M -> 5.84M (ClassReflection 5.28M -> 0.58M, MutatingScope 1.99M -> 0.70M, LruCache 0.59M -> 0). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
NodeCallbackScope inherits isInClass() and getClassReflection() from MutatingScope unchanged, so the native slot read applies to it too; a subclass qualifies when both methods resolve to MutatingScope's own declarations, and the last approved class entry is remembered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
A native body calling another native object's equals()/isSuperTypeOf()/ traverse()/... went through the engine (pt_type_call: method lookup, zend_call_function frame, argument copies, the handler's zpp) — 34.2M such calls per self-analysis, each landing in a one-line delegation. - TypeOps.h/.cpp: a pt_type_op_id enum of 27 hot operations with their argument contracts, a per-class table of direct C++ entries (`zv::Val (*)(zend_object *self, zend_class_entry *scope, uint32_t argc, zval *argv)`), and pt_type_op()/pt_type_op_trinary(): the entry is called when the receiver is EXACTLY a registered native class that registered the op and the arguments satisfy the contract; anything else (a PHP subclass, a reference, a wrong zval kind) takes pt_type_call() as before. The ops of a class entry are found in a direct-mapped open-addressing table keyed by the zend_class_entry pointer (the engine leaves no CE field free for a runtime-linked user class), filled at activation in Shadow.cpp; a native subclass inherits the parent's entry (with the parent's scope) for every method it does not declare itself. - reg::Class::op()/traitOp() register the entries next to the cls.method()/cls.traitMethod() lines they mirror, an entry that only delegates to a handle member is generated from it (cls.op<PT_OP_X, &Handle::member>(): reg::detail::BoundOp derives the argv conversions from the member's parameter types and the result conversion from its return type; a member may ignore trailing op arguments but never take more than the op passes), any other is a one-line lambda; a type op read as a bool is pt_type_op_bool(); traitOp() binds to the immediately preceding traitMethod() and only when that call added the method, so the class-body/first-trait precedence is kept. - The callback holders (PHPStanTurbo\NativeCallback, ObjectTypeCallback) and TypeTraverser::mapInternal()/traverseInternal() are entered without a frame by pt_call_fci() and pt_type_call_callable() when a resolved callable is one of them (pt_direct_invoke); their PHP-visible __invoke() and methods stay. - pt_type_call() resolves the name to an op (length-bucketed memcmp, only for a receiver with an ops table), so the per-file by-name wrappers take the direct path too; 467 literal call sites use pt_type_op() outright; pt_call_type_equals()/pt_type_describe_precise() and ScopeOps' type queries likewise; the multi-statement JustNullable/ConstantScalar trait handlers share their bodies with the entries. Skipped on the direct path, and why that is safe: the execute_data frame (no observer/backtrace entry — pure queries), the handler's zpp (replaced by the op's argc/zval-kind pre-check with engine fallback; zpp checks no class either), the frame's argument/receiver addrefs (borrowed from the caller for the call — immutable Type/result objects), and the internal return-type check (never done at run time in release builds). Call census (self-analysis of src/Analyser, src/Rules, src/Type): native -> native calls through the engine 34.2M -> 7.2M; user CPU -2.3% over three interleaved pairs against the base extension; analysis output identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
The first round left 7.2M native -> native calls per self-analysis on
the engine path; the big items were name-delegating wrappers, the
callback holders and getters outside the op set.
- StaticType, ClosureType, the LateResolvableTypeTrait and
UnresolvedTemplateArgumentType forward to their inner type by name
(`$this->getStaticObjectType()->x()`, `$this->resolve()->x()`); each
gets a delegateOp() that enters the inner native type's direct entry,
and the classes register direct entries for every delegated op.
- The PHPStanTurbo\StaticTypeCallbacks holder (transform/map/guard/
toArgument) and IdentityCallback::identity() are recognised by
pt_direct_invoke() like the NativeCallback/ObjectTypeCallback
holders, so the TypeTraverser and RecursionGuard enter their C++
bodies without a frame.
- 24 ops added to pt_type_op_id for the large getters and member
queries: getEnumCaseObject, isComplete, isUnsealed, getKeyTypes/
getValueTypes/getOptionalKeys, getValue, getClassReflection,
getTypes, getSubtractedType, getTypeWithoutSubtractedType,
getItemType, getObjectClassReflections, getReferencedClasses,
hasOffsetValueType/getOffsetValueType, getAncestorWithClassName,
hasMethod, hasInstanceProperty, changeBaseClass,
getUnresolved{InstanceProperty,Method}Prototype, or, isTypeOnly —
registered next to the cls.method() lines they mirror (string
arguments are contract-checked as IS_STRING, class-typed ones with
an instanceof guard falling back to the engine).
- IsSuperTypeOfResult / AcceptsResult ::lazyMaxMin(), ::extremeIdentity()
and the variadic and()/or() get direct C++ entry points used by the
IntersectionType / UnionType / ConstantArrayType / IntegerRangeType
bodies that called them by name; TypeCombinator takes the
VerbosityLevel singletons instead of calling the factories by name;
the UnionType / IntersectionType describe() bodies read the level
through pt_verbosity_level_value_of().
Skipped on the new direct paths, and why that is safe: the same as the
first round — no frame (pure queries), the handler's zpp replaced by the
op contract with engine fallback, borrowed arguments on immutable
objects. The delegateOp() fallback for a resolved type without the
method reports pt_find_method()'s error text instead of the
delegateNamed() one; every Type implements the interface methods, so
that path is unreachable.
Call census (self-analysis of src/Analyser, src/Rules, src/Type): native
-> native calls through the engine 7.17M -> 2.03M; user CPU unchanged within noise over six interleaved
pairs against the base extension (paired mean +0.8s, sd 2.3s, t=0.9;
the removed frames are worth ~0.2s); analysis output identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
The object rule listed only support.h, zv.h and reg.h, so a change to TypeOps.h or TypeTraits.h left objects built against the old op-table layout in place; the linked extension then jumped into the wrong entry and spun at activation. Every object now depends on every header. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
ReflectionAccess.cpp answers ReflectionProviderStaticAccessor::getInstance() from the twin's static slot and MemoizingReflectionProvider::hasClass()/ getClass() from its $knownClasses/$unknownClasses/$classes memo slots (exact class entry, the method on a miss); every native provider call site goes through these readers. Six reflection value classes the kernel builds and calls per member lookup are shadowed natively: UnresolvableTypeHelper (a DI service; its TypeTraverser closure is a native callback holder, so the walk never leaves C++), NativeParameterReflection, and the four CalledOnType/Callback unresolved method/property prototype reflections — their transformMethodWithStaticType()/transformPropertyWithStaticType() share pt_prototype_resolved_method()/_property() in TypeTraits.cpp, the CalledOnType transformStaticType() closures run as native traverser callbacks. Class-map keys calledOnType*/callback*PrototypeReflection and nativeParameterReflection are gone (pt_*_new helpers replace them); memoizingReflectionProvider, unresolvableTypeResult, extendedDummyParameter, extendedFunctionVariant, resolved*/changedType* reflections are appended. Differential coverage under the real names in tests/type-family.php over a new PrototypeFixture. Self-analysis census: Type-kernel->PHP calls 5.70M -> 3.50M (MemoizingReflectionProvider 459K -> 46K, ReflectionProviderStaticAccessor 236K -> 0, UnresolvableTypeHelper 369K -> 0, CalledOnTypeUnresolvedMethodPrototypeReflection 430K -> 0, NativeParameterReflection 315K -> 0). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
`make pgo` runs the three stages locally or in CI: an instrumented build, a training run (bin/pgo-train.sh: PHPStan analysing its own analyser, rule and type sources with that build loaded through an ini scan directory, which survives the CLI's OPcache restart), and the final build using the recorded profile — clang's .profraw files merged by llvm-profdata, GCC's .gcda files read back from beside the objects. phar.yml's compile jobs install the Composer dependencies first and build with `make pgo`; the Windows job is unchanged. Measured on the current head (macOS, clang, 8 ABBA pairs of the same workload): -0.72 % user CPU, paired t -7.7, identical output, a 20 % smaller binary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
ondrejmirtes
force-pushed
the
turbo-type-kernel-dispatch
branch
from
September 22, 2026 07:24
bc41bdd to
cad5a85
Compare
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.
Removes the remaining crossings between the native Type kernel and PHP, and builds the distributed binaries profile-guided.
Native reads of PHP-owned state
ClassReflectionAccess.cppanswersgetName(),isGeneric(),getCacheKey(),hasMethod(),isEnum()and a few other getters from an exactClassReflection's private slots.MutatingScope::isInClass()andgetClassReflection()are answered from the nativeScopeContext; subclasses that inherit those two getters unchanged, such asNodeCallbackScope, qualify too. Anything a slot can't answer still calls the PHP method.ReflectionAccess.cppanswersReflectionProviderStaticAccessor::getInstance(), andMemoizingReflectionProvider::hasClass()/getClass()from its memo slots.LruCache,UnresolvableTypeHelper,NativeParameterReflectionand the four called-on-type/callback prototype reflections are shadowed natively.On a self-analysis run, this cuts Type-kernel→PHP calls from 12.4M to under 1M.
Direct native-to-native dispatch
A native body calling
equals(),isSuperTypeOf(),traverse()or a similar method on another native object used to go through the engine: method lookup, a call frame, argument copies and the handler's parameter parsing. That was 34M calls per self-analysis, each landing in a one-line delegation.TypeOps.hdefines about 50 hot operations with their argument contracts, and each native class registers direct C++ entries for them.pt_type_op()calls an entry only when the receiver is exactly a registered native class and the arguments satisfy the contract. A PHP subclass or an unexpected zval takes the engine path as before.StaticType,ClosureType, the late-resolvable trait) enter their inner type's entry directly. The native callback holders are called without a frame.turbo-ext/Makefilenow rebuilds every object when any header changes, because a stale op-table layout otherwise links silently.Profile-guided release builds
make pgoruns three stages: an instrumented build, a training run of PHPStan analysing its own sources with that build loaded, and a final build using the recorded profile. The phar workflow's compile jobs install the Composer dependencies and build withmake pgo; the Windows job is unchanged. Measured with the same workload in 8 ABBA pairs, it's 0.72% less user CPU (paired t −7.7) with identical output and a 20% smaller binary.Verification
On this commit, with the extension loaded and active:
side-by-side.php,signature-parity.phpandsmoke.phppass.make tests,make phpstan,make csandmake lintare clean.-Wall -Wextra -Werror) both pass.🤖 Generated with Claude Code
https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN