Skip to content

Settle the conditional targets before deriving the guards - #6428

Merged
ondrejmirtes merged 2 commits into
2.3.xfrom
conditional-guard-target-perf
Sep 12, 2026
Merged

ondrejmirtes merged 2 commits into
2.3.xfrom
conditional-guard-target-perf

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

createConditionalExpressions() derives a guard for every differing key, and since the guard type became the branch set difference (#6394) that costs a TypeCombinator::remove() of the two branch types — the most expensive step of a merge. On Slevomat that is 422,500 subtraction calls from this one function, 21.96s of CPU.

A guard is only ever consumed paired with a target: a different key in the first target loop, any key in the second. So the targets are settled first using only the cheap checks, the merge returns early when no key can be one, and the subtraction is skipped for a guard whose only possible target is its own key — which the target loop unsets from its own guard set anyway. The first target loop then reuses the settled set instead of repeating the same three checks.

Mirrored in turbo-ext/src/ScopeOps.cpp, with the version bump in the follow-up commit.

Measurements

Analysing Slevomat, turbo on, forward+reverse passes to cancel session drift:

user CPU wall
93c49a1b95 (before the set-difference guards) 583.13s 72.88s
0f5817a75d (after) 603.11s 75.12s
this branch 591.62s 73.86s

Output is byte-identical, on Slevomat and on ShipMonk.

The remainder above the pre-#6394 baseline is the feature's genuine cost — the extra conditional expressions the sharper guards create, and the subtractions that do produce a usable guard. A companion PR against 2.2.x (#6427) attacks the subtraction itself; the two are not additive, since this one already skips most of those calls.

Verification

  • tests/PHPStan/Analyser, tests/PHPStan/Type, tests/PHPStan/Rules green
  • tests/PHPStan/Analyser green again with the extension loaded, so both twins agree
  • turbo-ext/tests/smoke.php and turbo-ext/bin/side-by-side.php pass

🤖 Generated with Claude Code

https://claude.ai/code/session_01LDqrN1pcN3qWc7xzKfkJaF

@phpstan-bot

Copy link
Copy Markdown
Collaborator

You've opened the pull request against the latest branch 2.3.x. PHPStan 2.3 is not going to be released for months. If your code is relevant on 2.2.x and you want it to be released sooner, please rebase your pull request and change its target to 2.2.x.

ondrejmirtes and others added 2 commits September 12, 2026 17:14
createConditionalExpressions() derived a guard for every differing key, and
since the guard type became the branch set difference that costs a
TypeCombinator::remove() of the two branch types - the most expensive step of
a merge.

A guard is only ever consumed paired with a target: a different key in the
first target loop, any key in the second. So the targets are settled first
with the cheap checks, the merge returns early when no key can be one, and
the subtraction is skipped for a guard whose only possible target is its own
key, which the target loop unsets from its own guard set anyway. The first
target loop reuses the settled set instead of repeating the checks.

Analysing Slevomat: 603.1s -> 591.6s user CPU, with byte-identical output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LDqrN1pcN3qWc7xzKfkJaF
@ondrejmirtes
ondrejmirtes force-pushed the conditional-guard-target-perf branch from e2c6601 to bff769c Compare September 12, 2026 15:18
@ondrejmirtes
ondrejmirtes merged commit 05c338b into 2.3.x Sep 12, 2026
514 of 518 checks passed
@ondrejmirtes
ondrejmirtes deleted the conditional-guard-target-perf branch September 12, 2026 15:23
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