Repository navigation
[NOMERGE] Key assertions and range facts by CSE VN equivalences - #136950
Draft
NinoFloris wants to merge 9 commits into
Draft
NinoFloris wants to merge 9 commits into
NinoFloris wants to merge 9 commits into
Conversation
|
Azure Pipelines: Successfully started running 5 pipeline(s). 10 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 |
NinoFloris
force-pushed
the
jit/cse-vn-equivalence
branch
3 times, most recently
from
November 4, 2026 04:05
14bc5ce to
b7eb6f8
Compare
CSE rewrites a use's conservative value number to its reaching definition's. Other nodes that carried the old value number, such as copies made before CSE, keep it, so facts created under one number are invisible under the other. Record the rewrite as a value number equivalence. The value number store keeps a union-find over rewritten numbers, empty for methods where no use changed. The representative is the number that says the most about the value: a constant, else a phi definition or a function, else the reaching definition's number; a constant never keys the map. Nothing consults it yet: the existing checked-bound transfer stays as it is, so this produces no code differences. A follow-up keys assertions by the representative.
About one method in nine records a rewrite, so the inline buckets of an embedded table would be carried by every value number store for nothing. Keep a pointer and allocate on the first record.
Most lookups are for numbers without a recorded equivalence, so a bit per number answers them without hashing, and the list behind it holds only the few pairs a method records, scanned in place. GrowableBitSet in bitvec.h is BitVec's shape without traits or a size up front, for a set whose index space is still being allocated while it fills.
Value numbers read out of a function application still name the operand from before CSE, so the peeled base of an address, the receiver of a method-table load, the operands of index arithmetic and the bound in the "i + k" and "bound - x" forms are canonicalized where they are extracted. Checked-bound membership moves to the representative when the equivalence is recorded, which replaces the special case in CSE. The representative is the side a consumer can reason about: a phi definition can be walked to its reaching values and a function can be decomposed, while an opaque load offers neither.
A number CSE rewrote may stand for a phi definition while naming an opaque load, so VNVisitReachingVNs and optVisitReachingAssertions canonicalize at entry rather than at each caller that passes a raw value number.
CSE rewrites a use's value number but not its parents', so a function over the old number stays distinct from the same function over the representative. Such a function is rebuilt over canonical arguments when its representative is asked for, and the result is remembered so that a function has one canonical form regardless of the order of lookups: a bit per number for the common case where the function is already canonical, and a list entry for a rebuilt number, which also inherits the checked-bound memberships of the original. Only value functions are rebuilt. Maps, phis, memory and exception sets are left alone. A rebuilt root can fold to a constant, which then becomes the representative of its class and never keys the map.
Value number based constant propagation folds a tree when the tree's own conservative number is a constant. The representative can be a constant when the tree's number is not: a load whose value CSE tied to a known one, or a function whose rebuilt form folded. Relops and range checks already see that through canonicalization, so a compare over such a tree folds and a load that only fed the compare dies with it, but a value used as data stayed. Asking for the representative folds it too, unless the constant takes more than one instruction to materialize: the value is already computed, often kept in a register by CSE, and a large immediate at each use costs more than it saves.
Treat address-exposed local reads as memory-load CSE candidates so repeated reads can share a captured value, enabling further bounds-check elimination. Existing memory dependence and availability checks govern reuse. Object references are included. On their own, capturing a receiver left cached method-table numbers referring to the earlier reload, so exact-type assertions stopped matching; the value number equivalences recorded by the rewrite carry those facts to the temp. Cover guarded span slicing and intervening writes and calls. The throw-elimination disasm check is scoped to ARM64: the Windows x86 and x64 JITs retain the unsigned Slice comparison after the signed guard, and the functional coverage runs on all targets without requiring that separate signedness optimization in their codegen.
A CSE definition that is the value of a store to a local produced two locals holding one value: the fresh temp, defined by the expression, and the original local, defined by a copy of the temp. The copy is one instruction at the definition and one more live register for as long as both are needed; on arm64 the register allocator cannot coalesce it when the two lifetimes diverge. When the candidate has a single definition site and that site is the value of a store to a local whose only definition node is that store, the local is the temp. The store stays as it is and becomes the completed CSE definition, each use reads the local under the store's SSA number, liveness is marked backwards from the use to the store with the same builder the fresh-temp path uses, and the use adopts the definition's value number as before. The entry value of a parameter or a must-init local has no definition node and cannot reach a use the store dominates, so it does not disqualify the local; a phi does. Shared-constant candidates and address-exposed, struct, or already-CSE locals keep a fresh temp.
NinoFloris
force-pushed
the
jit/cse-vn-equivalence
branch
from
November 5, 2026 00:30
5189f19 to
78c0a69
Compare
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.
Based on #136932, which records the equivalences and changes nothing. This stacks the consumers on it, as a draft to show what that PR unlocks. The last commit is the generalized form of what the experiment in #136739 needed to match exact-type assertions through CSE.
What it breaks down as, measured on my local libraries pmi collection (172,277 contexts, 1,393 changed methods):
ldr wzr/ldrb wzr)CORINFO_HELP_RNGCHKFAILcall)CHKCAST/ISINSTANCEOF)cmp/cmn/tst)ldr*)Footnotes
UnixDomainSocketEndPoint..ctor(ReadOnlySpan<byte>): two bounds checks and the loop's zero-trip test are gone, so the array length load that used to feed them is kept for its exception semantics only and becomesldr wzr, [x0, #0x08]. Same access, result discarded. The method is smaller. ↩Six loops whose bounds check disappeared and that were then rewritten to count down (
subpluscbnzinstead ofadd,cmp,blt). All six are smaller, between 12 and 24 bytes each. ↩ValueTask.Preserve(+20 bytes): assertion prop now substitutes the known-null_objinto the returned struct on the null path, and a multi-register struct return with a constant field goes through a stack temp. Pre-existing return shaping, reproduced on main withnew VT(null, v.Token, v.Flag).OptionsFactory.Create(+8 bytes): loop rewritten from a post-incremented pointer to an indexed form after its zero-trip test folded, costing one more callee-saved register.OdbcTransaction.Commitand.Rollback(-8 bytes each): aSafeHandlenull check folded, after which the handle lives in a stack slot instead of a callee-saved register. One reload more, one register pair less. ↩