Skip to content

Cut memory and time of analysing large array literals - #6778

Merged
ondrejmirtes merged 3 commits into
2.3.xfrom
issue-15452
Oct 22, 2026
Merged

ondrejmirtes merged 3 commits into
2.3.xfrom
issue-15452

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Analysing a 20,000-entry string-keyed array literal took 139 MB and 1.46 s; 40,000 entries took 288 MB and 4.48 s. Two independent causes:

Memory — literal types were held behind a closure. ArrayHandler keeps every item's ExpressionResult alive until the array's own type is resolved (each item type is captured at its own evaluation point). Every literal's result carried a lazy typeCallback closure (~800 B) capturing an InitializerExprContext (~160 B) — about three times the ConstantStringType it eventually produced, which rules ask for anyway. A literal's type is lexical and literals are never tracked in a scope (isSpecifyExpressionTypeNoop() is true for Scalar), so ScalarHandler now resolves the type eagerly. Ported to turbo-ext/src/ScalarHandler.cpp.

Time — DuplicateKeysInLiteralArraysRule was quadratic. Each explicit key was looked up with array_search() in the list of seen keys; at 40,000 entries the rule alone took 3.3 s. Seen keys are now indexed by type and value, keeping the lookup as strict as the identity comparison.

Reporter's reproducer (https://github.com/alfredbez/phpstan-literal-array-memory-reproducer), turbo off:

Entries Before After
20,000 139 MB, 1.46 s 109 MB, 0.59 s
40,000 288 MB, 4.48 s 233 MB, 1.15 s

Most of what remains at 20,000 entries is the parsed AST of the 700 KB file (~56 MB) and one ExpressionResult per key and value.

tests/bench/data/bug-15452.php is a 5,000-entry fixture produced by the reporter's generator (~0.43 s → ~0.27 s, 28 → 24 MB).

Closes phpstan/phpstan#15452

🤖 Generated with Claude Code

https://claude.ai/code/session_017pydsEaJ8dAiZ22Nt1Yqf4

ondrejmirtes and others added 3 commits October 22, 2026 21:48
A literal's type is lexical - the same on every scope and in both
flavours - and literals are never tracked in a scope, so a lazy
typeCallback bought nothing. It cost a closure (~800 B) capturing an
InitializerExprContext (~160 B) for every literal, about three times the
ConstantStringType it eventually produced.

That adds up in large array literals: ArrayHandler keeps every item's
ExpressionResult alive until the array's own type is resolved, so a
20,000-entry string-keyed literal held ~38 MB of those closures and
contexts at the peak.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pydsEaJ8dAiZ22Nt1Yqf4
Every explicit key was searched for with array_search() in the list of
keys seen so far, making the rule quadratic in the number of items - a
40,000-entry literal spent 3.3 s in the rule alone. The seen keys are now
indexed by their type and value, which keeps the lookup as strict as the
identity comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017pydsEaJ8dAiZ22Nt1Yqf4
@ondrejmirtes
ondrejmirtes merged commit 4c1d0ec into 2.3.x Oct 22, 2026
273 of 274 checks passed
@ondrejmirtes
ondrejmirtes deleted the issue-15452 branch October 22, 2026 19:52
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.

High memory use when analysing large literal arrays

1 participant