Skip to content

turbo: One canonical instance per type (3% faster) - #6261

Open
AJenbo wants to merge 2 commits into
phpstan:2.2.xfrom
AJenbo:faster-turbo
Open

turbo: One canonical instance per type (3% faster)#6261
AJenbo wants to merge 2 commits into
phpstan:2.2.xfrom
AJenbo:faster-turbo

Conversation

@AJenbo

@AJenbo AJenbo commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

This is ported over from PHPantom and how it handles types internally. The benefits aren't as big as I had hoped since part of it was already being performed in the majority of cases but it still provided a very real 3% improvement to turbo's performance.

It reduces life time type instances by 60% but since they are short lived there's no real memory benefit to this.

Entries are borrowed exactly like the memo's: a weak map whose dtor releases the slot, so a canonical instance is retained only as long as some live scope holds it anyway. The open-addressing table is now a SlotTable shared by the memo and the intern table.

Interning is only sound because doUnion() now returns the same type for equal array operands as for identical ones; before that fix, making the operands identical changed inference at tests/PHPStan/Rules/Variables/data/bug-8113.php:47 and analysis output was no longer identical with the extension on and off.

@AJenbo AJenbo changed the title turbo: One canonical instance per type turbo: One canonical instance per type (3% faster) Aug 24, 2026
@AJenbo
AJenbo marked this pull request as draft August 24, 2026 21:49
This is ported over from PHPantom and how it handles types internally.
The benefits aren't as big as I had hoped since part of it was already
being performed in the majority of cases, but it still provided a very
real 3% improvement to turbo's performance.

Keep one canonical instance per distinct type value, keyed by the same
128-bit structural hash the memo already computes. Operations that reach
the same value by different routes then hand back the same object, so the
identity checks already on the hot paths — pt_types_identical_or_equal()
on scope merges, `$a === $b` in TypeCombinator::doUnion() — decide
comparisons that used to recurse into two graphs.

Entries are borrowed exactly like the memo's: a weak map whose dtor
releases the slot, so a canonical instance is retained only as long as
some live scope holds it anyway. The open-addressing table is now a
SlotTable shared by the memo and the intern table.

Worth -3.1% user CPU on serial self-analysis (81.8 -> 79.2 s, three
interleaved rotated rounds). Instrumented over src/Type + src/Analyser:
793k TypeCombinator calls, 184k memo misses, and 110,874 of those misses
(60%) recompute a value that already exists — it reduces the lifetime
type instance count by 60%, though since they're short-lived there's no
real memory benefit to this; only 73k distinct values are ever canonical.
Every substitution was verified equals()-true and describe()-identical.
Peak RSS is unchanged — the deduplicated results are short-lived, and
peak RSS is set by reflection and parser state instead.

Interning is only sound because doUnion() now returns the same type for
equal array operands as for identical ones; before that fix, making the
operands identical changed inference at
tests/PHPStan/Rules/Variables/data/bug-8113.php:47 and analysis output
was no longer identical with the extension on and off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AJenbo
AJenbo marked this pull request as ready for review August 24, 2026 21:59
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

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