Skip to content

perf: Iterate through nullsafe expression chains#6084

Merged
staabm merged 1 commit into
phpstan:2.2.xfrom
realFlowControl:optimize/nullsafe-short-circuiting-loop
Jul 22, 2026
Merged

perf: Iterate through nullsafe expression chains#6084
staabm merged 1 commit into
phpstan:2.2.xfrom
realFlowControl:optimize/nullsafe-short-circuiting-loop

Conversation

@realFlowControl

@realFlowControl realFlowControl commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

While profiling PHPStan on the Sylius project, I found several optimization opportunities. This PR addresses the first one.

It replaces recursive calls in NullsafeShortCircuitingHelper::getType() with a loop while preserving the same expression-chain handling and return behavior. This helper is a CPU hotspot during expression type resolution. Iteration avoids repeated PHP method calls and stack frames.

In five uncached Sylius analysis runs per variant, covering 2,366 files with eight workers, median elapsed time fell from 28.35 s to 27.22 s (-4.0%), while median aggregate CPU time (user + sys) fell from 172.90 s to 170.65 s (-1.3%). In the combined candidate profile, CPU time attributed to this helper fell from 5.72 s to 2.70 s (-52.8%).

PHPBench showed -2.5% locally as well 🎉

Benchmarks were run on an Apple M4 Max with PHP 8.5.7

@realFlowControl
realFlowControl marked this pull request as ready for review July 22, 2026 14:12
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

@staabm

staabm commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Hey Florian,

nice to see you contributing :-).

could you give us a small series of steps how this improvement can be reproduced?

Thanks
Markus

PS: I just rebased the PR, as it was missing ~150 commits of 2.2.x

@staabm staabm changed the title pref: Iterate through nullsafe expression chains perf: Iterate through nullsafe expression chains Jul 22, 2026
Replace recursive calls in NullsafeShortCircuitingHelper::getType() with a loop while preserving the same expression-chain cases and return behavior.

This helper is a flat CPU hotspot under expression type resolution. Iteration avoids repeated PHP method calls and stack frames for chained property, method, array, and static accesses.

Benchmark: uncached Sylius analysis of 2,366 files with eight workers, five runs per variant. Median elapsed time fell from 28.35 s to 27.22 s (-4.0%) and aggregate user+sys CPU from 172.90 s to 170.65 s (-1.3%). In the combined profile, helper CPU fell from 5.72 s to 2.70 s (-52.8%).
@staabm
staabm force-pushed the optimize/nullsafe-short-circuiting-loop branch from 5097016 to ef44511 Compare July 22, 2026 14:29
@staabm
staabm requested a review from VincentLanglet July 22, 2026 14:45
@realFlowControl

Copy link
Copy Markdown
Contributor Author

could you give us a small series of steps how this improvement can be reproduced?

So it boils down to me cloning https://github.com/sylius/sylius (because it is a huge code base that uses PHPStan), running PHPStan with /usr/bin/time -p in front multiple times, apply the patch and do it again.

@staabm

staabm commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

PS: you could try latest 2.2.x-dev version on your project. it should be waaay faster than the latest stable release
(just to make sure you are not wasting time to speedup/investigate things which no longer are a bottleneck ;-))

@realFlowControl

realFlowControl commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

I benchmarked the 2.2.x branch with this PR patch applied vs. without 🎉

@staabm
staabm merged commit 336d682 into phpstan:2.2.x Jul 22, 2026
1091 of 1096 checks passed
@staabm

staabm commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

thank you @realFlowControl

ondrejmirtes added a commit that referenced this pull request Jul 22, 2026
Every property fetch / method call resolves its type by walking down to
the chain root to detect a nullsafe operator (NullsafeShortCircuitingHelper),
costing O(N²) walk steps per chain of depth N — with or without an actual
nullsafe operator in the chain. Deep loop-wrapped plain chains make that
walk dominate: 3.71s -> 3.14s wall (-15%), -18% user CPU from the
recursion-to-loop rewrite. The real-world counterpart is Symfony
TreeBuilder fluent chains (300+ calls in one statement) in Sylius bundle
Configuration classes, which dropped up to 23% per file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016szvNF5RXhACdfMQNc6DVL
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.

4 participants