Skip to content

Add fast path to TypeCombinator::union() for trivial 2-type cases#5325

Open
SanderMuller wants to merge 2 commits intophpstan:2.1.xfrom
SanderMuller:perf/typecombinator-union-fast-path
Open

Add fast path to TypeCombinator::union() for trivial 2-type cases#5325
SanderMuller wants to merge 2 commits intophpstan:2.1.xfrom
SanderMuller:perf/typecombinator-union-fast-path

Conversation

@SanderMuller
Copy link
Copy Markdown
Contributor

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 is X (never is the identity element)
  • union(X, MixedType) — result is MixedType (mixed absorbs everything)
  • union(X, X) — result is X (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 cheap instanceof or 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.

@staabm
Copy link
Copy Markdown
Contributor

staabm commented Mar 29, 2026

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?

@SanderMuller
Copy link
Copy Markdown
Contributor Author

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
https://github.com/phpstan/phpstan-src/actions/runs/23704938111/job/69055142260?pr=5325 ):

Benchmark PR Baseline Change
bug-10772 383ms 511ms -25.0%
bug-6948 35ms 39ms -8.8%
bug-6936 64ms 70ms -8.3%
bug-4308 2.8ms 3.1ms -8.3%
bug-1388 92ms 100ms -7.3%
bug-7903 1.859s 2.000s -7.0%
bug-5390 1.3ms 1.4ms -6.6%
bug-8503 218ms 233ms -6.3%
bug-7214 2.5ms 2.7ms -6.0%
bug-6442 3.0ms 3.2ms -5.0%
bug-4300 5.9ms 6.2ms -3.7%
bug-13685 17ms 17ms -3.7%
bug-7140 30ms 31ms -2.5%
bug-10538 1.103s 1.133s -2.6%
bug-7637 20ms 21ms -2.5%
bug-3686 12ms 12ms -2.4%
bug-11913 168ms 172ms -2.4%
bug-1447 22ms 22ms -2.2%
bug-7901 841ms 858ms -2.0%
bug-13310 51ms 52ms -2.0%
bug-12159 215ms 218ms -1.7%
bug-5231_2 15ms 15ms -1.5%
bug-13933 369ms 373ms -1.2%
bug-11283 908ms 919ms -1.2%
bug-12671 558ms 564ms -1.2%
bug-8146a 131ms 132ms -0.8%
bug-8215 812ms 819ms -0.8%
bug-13218 23ms 23ms -0.7%
bug-10979 817ms 822ms -0.6%
bug-13352 2.990s 3.008s -0.6%
bug-8146b 1.010s 1.015s -0.5%
bug-5081 2.598s 2.609s -0.4%
bug-11297 1.618s 1.625s -0.4%
bug-7581 345ms 346ms -0.4%
bug-6265 182ms 182ms -0.3%
bug-5231 16ms 16ms -0.2%
bug-14207 3.230s 3.235s -0.2%
bug-14207-and 914ms 912ms +0.3%
bug-8147 660ms 657ms +0.4%
bug-12800 1.351s 1.338s +1.0%
bug-14319 832ms 831ms +0.1%
union-fast-path 159ms new file

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

@SanderMuller
Copy link
Copy Markdown
Contributor Author

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 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.

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