Add fast path to TypeCombinator::union() for trivial 2-type cases#5325
Add fast path to TypeCombinator::union() for trivial 2-type cases#5325SanderMuller wants to merge 2 commits intophpstan:2.1.xfrom
Conversation
|
I tried similar things in the past. in the end I did not propose the patch, because - while it seems useful in microbenchmarks - I was not able to prove on real world code-bases that it makes a meaningful difference. did you run this change on your projects and did it make a difference? |
I haven't run it yet on my own projects. This is the result from the benchmarks in the CI of this PR (https://github.com/phpstan/phpstan-src/actions/runs/23704938111/job/69055142257?pr=5325 and
No benchmark regressed beyond noise. Consistent small improvements across the board, with bug-10772 showing a notable -25%. I will see if I can run this change on 2 of the bigger projects I work on |
I saw a 2.6% and 2.3% speed up on my 2 bigger projects. So I think it will mainly benefit specific setups like we see in the benchmarks, some bugs receiving 5-25% performance improvement while others show noise level changes. |
Problem
TypeCombinator::union()is one of the most called methods in PHPStan. Profiling shows 30k-66k calls per file on heavy benchmarks. Every call goes through the full normalization pipeline: flattening nested unions, classifying scalars/arrays/enums, sorting integer ranges, O(n^2) deduplication, and array processing.Many of these calls are trivial 2-type cases where the result is immediately known:
union(NeverType, X)— result isX(never is the identity element)union(X, MixedType)— result isMixedType(mixed absorbs everything)union(X, X)— result isX(same object passed twice)These patterns are common in type narrowing, scope merging, and conditional type resolution.
Solution
Add a fast path before the main normalization loop that handles exactly these three cases when
$typesCount === 2. Each check is a cheapinstanceofor identity comparison.The checks mirror logic that already exists deeper in the method (never removal at line 199, mixed short-circuit at line 191, dedup at line 350) but avoids all the setup work.
Impact
Benchmarked on the 5 slowest files from
tests/bench/data/, measured in isolation (without other optimizations):9,363ms → 7,893ms (-15.7%)
All files benefit roughly equally since
union()is called pervasively.