Cheaper file cache, type memo bookkeeping and expression-key lookups - #6568
Merged
Merged
Conversation
ondrejmirtes
force-pushed
the
native-representation
branch
from
September 23, 2026 16:44
ae61523 to
d1b5149
Compare
…he object TypeCombinatorCache kept both per-object facts it needs in weak maps: the 128-bit structural hash of every hashed type (pt_type_hashes) and the list of memo keys a result object answers (pt_memo_results). Every entry is an EG(weakrefs) registration on the object, and every such object pays the engine's weak-reference notification when it dies. The shadowing classes the memo hashes structurally (PHPStan\Type\*, PHPStan\Php\*, TrinaryLogic) now get a create_object at activation that allocates a 32-byte header in front of the zend_object: the hash once computed, the memo key list and the memo generation the list belongs to. PHP subclasses inherit the allocator. The header's free_obj tombstones the object's memo slots, its clone_obj gives the clone a fresh header, and clear() bumps the generation so that lists from before a clear are ignored. Objects without the header keep using the weak maps. src/Analyser src/Rules src/Type single process, 8 interleaved pairs: 20.78 s -> 20.40 s user CPU (-1.82 %, 8/8, t = -14.7), peak RSS unchanged, output identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSGnoYacTfJv3taAhZkj5o
…inted key ExprPrinter caches an expression's printed key in the node's phpstan_cache_printer attribute. Every node constructed from another node's attributes - the BooleanAnd/BooleanOr and callable virtual nodes, the single-subject Isset_ nodes of a multi-variable isset(), the plain MethodCall read for an argument-less nullsafe call, the __invoke() call of an invokable callee, the clone-with property fetches, the foreach value assignment and the $this property fetches ClassStatementsGatherer records - copied that attribute along, and so printed as the node it was made from whenever that node had been printed first. A BooleanAndNode then was the original `$foo && $i++`, and what was tracked for it answered for the original expression: its keep-void type read mixed instead of bool. Each of these now drops the cached key from the copied attributes, the way ArgumentsNormalizer and the nullsafe and pipe handlers already did, in the PHP classes and in the native mirrors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSGnoYacTfJv3taAhZkj5o
…on can match ScopeOps::hasExpressionType() printed the expression's key before looking it up, and for a node that had never been printed that means a trip through the PHP pretty printer - ~60 % of the printer misses of an analysis, and most of those lookups answer No. A printed key determines the kind of node it was printed from: PHP syntax its node class, and every virtual node its own __phpstan marker (a short list prints like an array, so the two share a kind). The native hasExpressionType() now looks at the kinds of the tracked expressions first, and when none shares the node's, it answers No without printing. The PHP twin keeps printing; the answers are the same, only the printer's cache attribute of such a node is left unset. Exact since the previous commit stopped virtual nodes from inheriting another node's key. src/Analyser src/Rules src/Type single process, 8 interleaved pairs: 20.43 s -> 20.21 s user CPU (-1.08 %, 7/8), output identical, walk-trace identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSGnoYacTfJv3taAhZkj5o
FileCacheStorage wrote every entry as a PHP file returning a var_export()ed CacheItem and loaded it with include. Compiling such a file costs more than unserializing the same data, every object in it is hydrated through a userland __set_state() call, and the compiled code stays in memory for the rest of the process. Entries are now serialize() payloads in *.dat files - the separate extension keeps an older PHPStan sharing the directory from including a payload that is not PHP. clearUnusedFiles() (cache version v3-serialized) deletes the *.php entries of the former formats once. src/Analyser src/Rules src/Type single process, 8 interleaved pairs, warm cache: 20.27 s -> 19.69 s user CPU (-2.86 %, 8/8, t = -53), peak RSS 589 MB -> 526 MB (-11 %), output identical. IntermediaryNameScope::intern() stays: serialize() keeps objects shared within an entry but stores arrays by value, and skipping the interning measured +38 MB for no CPU gain. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSGnoYacTfJv3taAhZkj5o
ondrejmirtes
force-pushed
the
native-representation
branch
from
September 23, 2026 18:12
d1b5149 to
8e7f80c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three performance changes and one bug fix the second of them depends on.
Full self-analysis (
build/phpstan.neon, parallel, result cache cleared, disk caches warm), 6 interleaved pairs against 2.3.x: user CPU −3.4 % (6/6, t = −5.8), wall −3.2 %, output identical.Store file cache entries serialized instead of as
var_export()ed PHPFileCacheStoragewrote every entry as a PHP file returning avar_export()edCacheItemand loaded it withinclude. Compiling such a file costs more than unserializing the same data, every object in it is hydrated through a userland__set_state()call, and the compiled code stays in memory for the rest of the process. Entries are nowserialize()payloads in*.datfiles; the separate extension keeps an older PHPStan sharing the directory from including a payload that is not PHP.clearUnusedFiles()(cache versionv3-serialized) deletes the*.phpentries of the former formats once.Single process,
src/Analyser src/Rules src/Type, 8 pairs: −2.86 % user CPU, peak RSS 589 MB → 526 MB (−11 %). PHP only - applies without the extension as well.Keep a type's structural hash and memo keys in a header in front of the object (turbo)
TypeCombinatorCachekept the structural hash of every hashed type and the memo keys each result answers in weak maps - anEG(weakrefs)registration per object and a weak-reference notification when it dies. The shadowing classes it hashes structurally now allocate a 32-byte header in front of thezend_objectholding both; PHP subclasses inherit the allocator,free_objtombstones the object's memo slots andclone_objgives the clone a fresh header.Single process, 8 pairs: −1.82 % user CPU.
Do not let a node built from another node's attributes inherit its printed key (bug fix)
ExprPrintercaches an expression's printed key in the node'sphpstan_cache_printerattribute, and every node constructed from another node's attributes copied it along - theBooleanAnd/BooleanOrand callable virtual nodes, the single-subjectIsset_nodes of a multi-variableisset(), the plainMethodCallread for an argument-less nullsafe call, the__invoke()call of an invokable callee, the clone-with property fetches, the foreach value assignment and the$thisproperty fetchesClassStatementsGathererrecords. Such a node then printed as the node it was made from whenever that one had been printed first, and whatever was tracked for it answered for the original expression: the keep-void type of$foo && $i++;in a loop readmixedinstead ofbool. The cached key is now dropped from the copied attributes, in PHP and in the native mirrors.InheritedExpressionKeyRuleTestfails without the fix (with and without the extension).Answer
hasExpressionType()without printing a key no tracked expression can match (turbo)ScopeOps::hasExpressionType()printed the expression's key before looking it up - for a node never printed before that is a trip through the PHP pretty printer, most of the printer misses of an analysis, and most such lookups answer No. A printed key determines the kind of node it was printed from (PHP syntax its node class, every virtual node its own__phpstanmarker; a short list shares the array's kind), so when none of the tracked expressions shares the node's kind, the native implementation answers No without printing. Exact since the previous commit.Single process, 8 pairs: −1.08 % user CPU, walk-trace identical.
Verification
Before the rebase onto the current 2.3.x:
make testswith and without the extension (22,237 tests),make phpstan, side-by-side, signature parity, walk-trace identical,make lint-turbo.🤖 Generated with Claude Code
https://claude.ai/code/session_01QSGnoYacTfJv3taAhZkj5o