Repository navigation
Conversation
Author
|
@dotnet-policy-service agree |
glyh
marked this pull request as draft
October 20, 2026 08:54
|
Azure Pipelines: Successfully started running 5 pipeline(s). 11 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
optAssertionVNIsSubtype walked PHI definitions recursively, passing budget - 1 to every incoming edge, so `budget` bounded depth rather than work. With no memo the cost is O(fan-in ^ depth) in a loop-free PHI DAG, and optVisitReachingAssertions allocates two BitVecs per call out of the compilation arena. Nine 8-way switches merging into one object and then a single `if (o is A a)` took 10.9 s and 629 MB, measured before this branch was rebased onto its current base. Only the PHI walk is memoized. The body moves to optAssertionVNIsSubtypeWorker; optAssertionVNIsSubtype creates the memo and calls it, and the recursion goes through the worker so every frame shares the one table. The memo lives on the top-level frame, so it dies with the query: no generation stamp, no Compiler state. The key is (objVN, castToVN). The walk reads each edge's own assertion set rather than the caller's -- optVisitReachingAssertions documents that the caller has already checked assertions for the current block -- so that pair is the walk's whole input. The direct-assertion scan, which does depend on the caller's set, stays above the memo and is never cached; hoisting the lookup above it would make the key unsound. Only "proved" is stored. A `false` may mean "budget exhausted", which is not a property of the key: the same key can be false at depth 3 and true at depth 9, so caching it would serve a failure where a proof existed. A `true` is a real derivation and stays valid at any depth and on any path, cyclic or not, which is also why no depth is in the key and why cyclic PHIs need no in-progress marking. A JitHashTable, keyed through a small Equals/GetHashCode pair like BasicBlockLocalPair and AssertionKeyFuncs, rather than a hand-rolled table. It is allocated on the first proof, so a query answered from the value's own type never pays for it. On the reproducer AssertionProp goes from 8,892 bytes without the memo to 9,404 with it. The regression test is in the same commit and covers two different checks: the nine-switch shape, timed on the call that JITs it and failing past 2000 ms -- 100 in ~145 ms against this JIT, 102 at 10,688 ms against the parent commit's JIT built in the same tree -- and two loop-carrying shapes that guard termination when a PHI cycle makes the walk revisit itself and carry a null edge and a non-A edge into the cast, so an unsound proof shows up as an exception or a wrong value. AggressiveOptimization is load-bearing: under default tiering the first call compiles as Tier0, assertion propagation does not run, and both JITs pass in milliseconds. Fixes dotnet#133857
glyh
force-pushed
the
jit-memoize-subtype-phi-walk
branch
from
October 20, 2026 09:10
1e1a61e to
9c3ebb0
Compare
glyh
marked this pull request as ready for review
October 20, 2026 09:11
This branch has not been deployed
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.
DISCLAIMER: This is AI generated, then iterated and reviewed by me.
The subtype walk in
optAssertionVNIsSubtypepassesbudget - 1down each PHI edge, sobudgetlimits depth and nothing limits work. The same merge gets re-proven by every path that reaches it, which is exponential in the number of merges, andoptVisitReachingAssertionsallocates twoBitVecs per call so the arena climbs alongside. Nine 8-way switches into oneif (o is A a)is enough to hit it (#133857).The memo wraps the PHI walk, and only the walk. The recursive body moved to
optAssertionVNIsSubtypeWorker;optAssertionVNIsSubtypecreates the memo and calls it, and the recursion goes through the worker so every frame shares the one table. It lives on the top-level frame, so it dies with the query — no generation stamp, no compiler state.Watch out for
false. Afalsefrom this walk means either "no proof" or "ran out of budget", and the second isn't a property of the key — the same key can come back false at depth 3 and true at depth 9, so storing it would permanently discard a proof. Atrueis a real derivation and stays valid at any depth and on any path, cyclic or not, which is also why cyclic PHIs need no in-progress marking.optVisitReachingAssertions: "We assume that the caller already checked assertions for the current block"), so(value, claim)is the walk's whole input. Hoisting the lookup above the check would serve a proof derived from a different set.truefound with 9 levels left answers a frame with 3. That is safe because the budget bounds search effort, not the validity of a derivation, and it can only fold casts that are genuinely provable.Numbers
Whole-process wall time on the reproducer: 100 in ~145 ms with this JIT, 102 at 10,688 ms against the parent commit's JIT built in the same tree, which trips the test's own 2000 ms bound.
Memory:
AssertionPropgoes 8,892 → 9,404 bytes on that repro, so the memo costs 512. My first cut hard-coded a 2048-entry array, which was 49,152 bytes for a method that needs 73 entries.Not in this PR
optAssertionVNIsNonNullhas the same shape and no memo. That's #134462; the attempt there (#134916) is closed rather than merged, and doesn't overlap this.