Repository navigation
Add microbenchmarks of internals and restore the performance they found regressed - #7066
Merged
Merged
Conversation
Each benchmark declares the complexity class of the code it counts: constant-key lookups in ConstantArrayType, unions and intersections in TypeCombinator, describing a UnionType, building an array shape, the binary search in Patcher, and the combinations of CombinationsHelper. A regression to a worse class fails without numbers to keep up to date. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d525-9c4c-73cf-8edc-000aa8e78378
mergeWith() returns the scope itself for no other scope or the same one, getVariableType() answers a certainly defined variable from its holder, and specifyExpressionType() checks for a scalar before the noop helper. Each returns what it returned before. The microbenchmarks reproduce the optimizations of #4640, #4663, #4726 and the other scope PRs of 2026.
They reproduce the optimizations of ArgumentsNormalizer, the expression printer, TypeUtils, specifying an offset of an array, and the unions and intersections of #6623. BenchContainer takes extra configuration files for benchmarks that need parameters.
Each level of a chain is resolved innermost first, and each marked every level below it again. When the level right below is already marked, only this one needs marking: asking about every level of a 64-deep chain goes from 4.87M to 263K instructions.
The nested nodes of a type belong to the declaration resolve() entered, so they are resolved through resolveType(). A nested enter and leave of the same declaration did nothing besides costing the calls.
…ection GenericParametersAcceptorResolver::resolve() infers predicate template types only for a callable parameter with asserts, which is rare, and removeFinalByKeywordOverrides() returns a constant scalar without traversing it. MemoizingReflectionProvider records a found class only when the context changed. The microbenchmarks reproduce the reflection performance fixes of 2026.
The union of a single constant array normalizes it once more, which costs more than computing it: range(1, 100000) goes from 3.68M to 494K instructions. Adds the microbenchmarks of the rule and return type extension performance fixes of 2026.
… not an object The intersection removes what is not an object, such as the null of a nullable type. For a type that is already an object it returned the same type and cost the most of the rule.
ConstantStringType and ConstantIntegerType describe themselves without the closures of VerbosityLevel::handle(), which made a union of 16 constant strings 28 % slower than on 2.2.5. TypeCombinator skips pairs of plain constant arrays compareTypesInUnion() leaves as they are, and stops asking a general scalar once one holds every value. UnionType, IntersectionType, ObjectType, HasMethodType and ConstantArrayType:: isCallable() return earlier or memoize. The C++ mirrors do the same. The microbenchmarks reproduce the type system performance fixes of 2026.
…::union() Most unions flatten one union of a few members, which is cheaper to scan than to index by object id, and most have no float range to sort. DependencyResolver tests for a class reflection first, the most common. The microbenchmarks reproduce the infrastructure performance fixes of 2026.
ScopeOps::createConditionalExpressions() keys each holder from guard and target parts described once each instead of once per pair, and returns early when no differing key is certain in both branches. getNodeKey() gets back the fast path for a variable that #6200 removed. The C++ mirror does the same. The microbenchmarks reproduce the remaining scope and TypeSpecifier performance fixes of 2026. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d525-9c4c-73cf-8edc-000aa8e78378
IsSuperTypeOfResult returns its shared instances without create(), ObjectType::isSuperTypeOf() skips the LRU touch of the description it touched last, UnionTypeHelper::sortTypes() asks for FloatRangeType after the enum branch, ConstantArrayType::isSuperTypeOf() keys its value pairs by nested object ids, and FiniteTypeSet::key() checks for a compound type before its exclusions. Each returns what it returned before. The microbenchmarks reproduce the remaining type system performance fixes of 2026. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d525-9c4c-73cf-8edc-000aa8e78378
Since c7c28d4 moved the cache behind the path routing, only the rich and the simple parser had one, so reflection parsed a php-8-stubs file again for each symbol it declares. The stub parser gets its own cache, which keeps its ASTs apart from those of the other parsers. Parsing the PDO stub again goes from 105M to 13K instructions. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d525-9c4c-73cf-8edc-000aa8e78378
The dead code detector is not loaded below PHP 8.2. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d525-9c4c-73cf-8edc-000aa8e78378
The downgrade does not rewrite match, throw expressions and ::class on objects. BenchEnum cannot be downgraded and is left out of lint, like the other enum files. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d525-9c4c-73cf-8edc-000aa8e78378
…rror The differential test compares the class constants of both implementations. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d525-9c4c-73cf-8edc-000aa8e78378
The microbenchmarks get their first numbers. The analysis benchmarks get numbers measured with Tally, which counts memory above the start of the analysis. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d525-9c4c-73cf-8edc-000aa8e78378
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.
Adds microbenchmarks of PHPStan internals to
tests/bench, run by Tally next toAnalysisBench, and fixes the regressions they found.Microbenchmarks. 357 cases in about 90 classes call internals directly:
TypeCombinator,ConstantArrayType,UnionType,MutatingScope,ScopeOps,TypeSpecifier,TypeNodeResolver, reflection, rules and return type extensions.BenchContainerboots the container in a#[Before]method, so only the call itself is counted. Some cases declare how they grow (#[Constant],#[Logarithmic],#[Linear],#[Linearithmic],#[Quadratic],#[Polynomial],#[Exponential],#[Complexity]). The others have#[Expected]/#[ExpectedFor]numbers accepted from this PR's CI run.Most cases reproduce a PR from a "Performance" section of the 2026 release notes. Each was run on the release before the fix, on every release since, and on 2.3.x. Where an older release counted fewer instructions and the code still exists, 2.3.x is brought back toward it.
Optimizations (instructions on 2.3.x → this branch, best older release in brackets):
PDO.phpagainrange(1, 100000)returns its one array withoutunion()compareTypesInUnion()leaves as they areConstantStringType/ConstantIntegerType::describe()withoutVerbosityLevel::handle()closures (#6160): union of 16 constant stringsConstantArrayType::isCallable()returns early:VerbosityLevelat 256NullsafeOperatorHelpermarks only the new level: every level of a 64-deep chainScopeOps::createConditionalExpressions()describes each part onceobjectfor an objectTypeNodeResolverenters a declaration once per PHPDoc type: 32 itemsMutatingScope::getVariableType(),mergeWith(),getNodeKey()fast pathsIsSuperTypeOfResultshared instances: negateEach optimization returns what the code returned before. Where a class has a turbo mirror, the C++ was changed to match and checked with the turbo-ext smoke, signature-parity and side-by-side checks.
The headers of the
AnalysisBenchfiles from #7059 are accepted from the same run, now that Tally counts memory above the start of the analysis.Amp thread: https://ampcode.com/threads/T-01a1d525-9c4c-73cf-8edc-000aa8e78378