Conversation
- GT_CNS_INT: Compare icon handle flags and field sequences, not just the integer value. Two constants with the same value but different handle types or field sequences are not equivalent. - GT_ALLOCOBJ: Add missing case in the unary ExOp switch. Previously hit the default assert in debug builds. - GT_ARR_ELEM: Compare gtArrElemSize. Resolves existing TODO comment. - GT_SELECT: Add missing case in the special-node switch. Compare all three operands (condition, true value, false value). Co-authored-by: Copilot <[email protected]>
- GT_INDEX_ADDR: Compare gtElemType, gtLenOffset, and gtElemOffset in addition to gtElemSize. - GT_FTN_ADDR: Add leaf case comparing gtFptrMethod. Previously returned false for equal function address nodes. - GT_PHI_ARG: Add leaf case comparing local number, predecessor block, and SSA number. Previously returned false for equal phi arg nodes. Co-authored-by: Copilot <[email protected]>
- GT_JCC/GT_SETCC: Add leaf cases comparing gtCondition. - GT_PHYSREG: Add leaf case comparing gtSrcReg. - GT_JCMP/GT_JTEST/GT_SELECTCC: Compare gtCondition for binary nodes that carry a condition code but are not ExOp. - GT_SELECT_INCCC/GT_SELECT_INVCC/GT_SELECT_NEGCC (ARM64): Compare gtCondition for ARM64-specific conditional select variants. - GT_CCMP (ARM64/AMD64): Compare gtCondition and gtFlagsVal. Co-authored-by: Copilot <[email protected]>
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
|
@EgorBo PTAL Just a few diffs, but it looks like the old behavior can lead to improperly optimized multi-guess GDVs in NAOT. |
There was a problem hiding this comment.
Pull request overview
This PR tightens GenTree::Compare in CoreCLR JIT to treat more node-specific fields as part of structural equality, improving correctness for optimizations that rely on tree equivalence.
Changes:
- Extend constant and leaf-node comparisons to include additional distinguishing fields (e.g., handle flags, field sequences, conditions, physregs).
- Add missing comparisons for several operators (
GT_PHI_ARG,GT_FTN_ADDR,GT_JCC/GT_SETCC,GT_PHYSREG,GT_ALLOCOBJ,GT_INDEX_ADDR,GT_ARR_ELEM,GT_SELECT). - Add condition-code comparisons for CC-carrying binops (
GT_JCMP,GT_JTEST,GT_SELECTCC, target-specific selects,GT_CCMP).
- GT_CNS_INT: add gtCompileTimeHandle comparison - GT_FTN_ADDR: add gtFptrDelegateTarget and gtEntryPoint (R2R) comparisons - GT_ALLOCOBJ: add gtHelperHasSideEffects and gtEntryPoint (R2R) comparisons Co-authored-by: Copilot <[email protected]>
- GT_CNS_INT: add gtCompileTimeHandle comparison - GT_FTN_ADDR: add gtFptrDelegateTarget and gtEntryPoint (R2R) comparisons - GT_ALLOCOBJ: add gtHelperHasSideEffects and gtEntryPoint (R2R) comparisons Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
…nupGenTreeCompare
| #ifdef FEATURE_READYTORUN | ||
| if (op1->AsFptrVal()->gtEntryPoint.addr != op2->AsFptrVal()->gtEntryPoint.addr) | ||
| { | ||
| break; | ||
| } | ||
| #endif |
There was a problem hiding this comment.
For FEATURE_READYTORUN comparisons of GT_FTN_ADDR, this only checks gtEntryPoint.addr. CORINFO_CONST_LOOKUP semantics also depend on accessType (and the union value may be interpreted differently based on it). To avoid treating different entry point forms as equal, include accessType in the comparison (and compare the relevant union member accordingly).
| #ifdef FEATURE_READYTORUN | ||
| if (op1->AsAllocObj()->gtEntryPoint.addr != op2->AsAllocObj()->gtEntryPoint.addr) | ||
| { | ||
| return false; | ||
| } | ||
| #endif |
There was a problem hiding this comment.
For FEATURE_READYTORUN comparisons of GT_ALLOCOBJ, this only checks gtEntryPoint.addr. CORINFO_CONST_LOOKUP equality should also consider accessType (and the union contents as interpreted by it), otherwise nodes with different entry point access modes can be considered identical.
| if ((op1->AsIndexAddr()->gtElemSize != op2->AsIndexAddr()->gtElemSize) || | ||
| (op1->AsIndexAddr()->gtElemType != op2->AsIndexAddr()->gtElemType) || | ||
| (op1->AsIndexAddr()->gtStructElemClass != op2->AsIndexAddr()->gtStructElemClass) || | ||
| (op1->AsIndexAddr()->gtLenOffset != op2->AsIndexAddr()->gtLenOffset) || | ||
| (op1->AsIndexAddr()->gtElemOffset != op2->AsIndexAddr()->gtElemOffset)) |
There was a problem hiding this comment.
The GT_INDEX_ADDR ExOp comparison now checks several structural fields, but it still doesn't account for GT_INDEX_ADDR-specific semantic flags like GTF_INX_RNGCHK and GTF_INX_ADDR_NONNULL. These flags affect whether a bounds check / null-faulting behavior is required, so Compare should ensure the relevant bits match to avoid considering semantically different nodes identical.
|
Any idea what produced the massive diffs? |
src/coreclr/jit/gentree.cpp
Outdated
| if ((op1->AsIntCon()->gtIconVal == op2->AsIntCon()->gtIconVal) && | ||
| (op1->GetIconHandleFlag() == op2->GetIconHandleFlag()) && | ||
| (op1->AsIntCon()->gtFieldSeq == op2->AsIntCon()->gtFieldSeq) && | ||
| (op1->AsIntCon()->gtCompileTimeHandle == op2->AsIntCon()->gtCompileTimeHandle)) |
There was a problem hiding this comment.
I suspect e.g. assert-prop never propagates compile-time handle for constants
There was a problem hiding this comment.
I think we should add tests for the actual issues this fixes. And remove the comparisons that we don't have failing tests for.
For the field sequence check, I think it can be something like:
if (obj.GetType() == typeof(ClassOne) {
Unsafe.As<ClassOne>(obj).Field = 1;
} else {
Unsafe.As<ClassTwo>(obj).Field = 2; // Same offset as ClassOne::Field
}being tail-merged (the field sequence affects semantics of ADD(x, field seq).
I don't think gtCompileTimeHandle does or should affect semantics.
I can't think of any cases where GetIconHandleFlag would differentiate two different handles with the same value. If we want to support such cases, we will need to rationalize what that 'means' first.
There was a problem hiding this comment.
Thanks. Copilot (with a little help from me) found a case where field seqs matter.
src/coreclr/jit/gentree.cpp
Outdated
| if ((op1->AsIntCon()->gtIconVal == op2->AsIntCon()->gtIconVal) && | ||
| (op1->GetIconHandleFlag() == op2->GetIconHandleFlag()) && | ||
| (op1->AsIntCon()->gtFieldSeq == op2->AsIntCon()->gtFieldSeq) && | ||
| (op1->AsIntCon()->gtCompileTimeHandle == op2->AsIntCon()->gtCompileTimeHandle)) |
There was a problem hiding this comment.
I think we should add tests for the actual issues this fixes. And remove the comparisons that we don't have failing tests for.
For the field sequence check, I think it can be something like:
if (obj.GetType() == typeof(ClassOne) {
Unsafe.As<ClassOne>(obj).Field = 1;
} else {
Unsafe.As<ClassTwo>(obj).Field = 2; // Same offset as ClassOne::Field
}being tail-merged (the field sequence affects semantics of ADD(x, field seq).
I don't think gtCompileTimeHandle does or should affect semantics.
I can't think of any cases where GetIconHandleFlag would differentiate two different handles with the same value. If we want to support such cases, we will need to rationalize what that 'means' first.
src/coreclr/jit/gentree.cpp
Outdated
|
|
||
| if (kind & GTK_BINOP) | ||
| { | ||
| if (op1->OperIs(GT_JCMP, GT_JTEST, GT_SELECTCC)) |
There was a problem hiding this comment.
These operators look like they're missing GTK_EXOP.
There was a problem hiding this comment.
Added missing cases and fixed up the compare logic for them.
src/coreclr/jit/gentree.cpp
Outdated
| (op1->AsAllocObj()->gtAllocObjClsHnd != op2->AsAllocObj()->gtAllocObjClsHnd) || | ||
| (op1->AsAllocObj()->gtHelperHasSideEffects != op2->AsAllocObj()->gtHelperHasSideEffects)) |
There was a problem hiding this comment.
| (op1->AsAllocObj()->gtAllocObjClsHnd != op2->AsAllocObj()->gtAllocObjClsHnd) || | |
| (op1->AsAllocObj()->gtHelperHasSideEffects != op2->AsAllocObj()->gtHelperHasSideEffects)) | |
| (op1->AsAllocObj()->gtAllocObjClsHnd != op2->AsAllocObj()->gtAllocObjClsHnd)) |
gtHelperHasSideEffects is a pure function of gtAllocObjClsHnd.
Diffs were pretty minimal at first but the gaggle of copilot reviewers added more checks. Let me claw some of these back. |
Exercises a scenario where tail merge incorrectly merges stores with different field sequences (ClassOne.Field vs ClassTwo.Field via Unsafe.As at the same offset). After inlining, redundant branch opts uses the liberal VN to fold branches, exposing incorrect alias analysis from the wrong field sequence. Without the gtFieldSeq check in GenTree::Compare, the test returns 42 instead of the expected 999. Co-authored-by: Copilot <[email protected]>
GT_JCMP, GT_JTEST, GT_SELECTCC, GT_CCMP, GT_SELECT_INCCC, GT_SELECT_INVCC, and GT_SELECT_NEGCC all extend GenTreeOp with extra fields (gtCondition, gtFlagsVal) but were not marked GTK_EXOP. This meant their extra field comparisons in GenTree::Compare were outside the IsExOp switch, inconsistent with other extended operators. Move the condition/flags comparisons into the ExOp switch block. Co-authored-by: Copilot <[email protected]>
gtCompileTimeHandle does not affect runtime semantics. SPMI diffs confirm removing the check enables ~5500 additional optimizations across all collections with no regressions, saving ~306KB total. Co-authored-by: Copilot <[email protected]>
This was the source of the extra diffs. Removing that there are just a handful of diffs: one case where we can now fold some function pointer checks, and a couple where the new field seq check blocks a tail merge. In those cases the field offsets ended up the same so the new code is less efficient. I suppose we could relax the field seq check once we're past the point where it could impact VN based optimizations, but these opportunities seem rare. |
gtHelperHasSideEffects is a pure function of gtAllocObjClsHnd (both are derived from getNewHelper(classHandle)). Since gtAllocObjClsHnd is already compared, the gtHelperHasSideEffects check is redundant. Co-authored-by: Copilot <[email protected]>
| case GT_ALLOCOBJ: | ||
| if ((op1->AsAllocObj()->gtNewHelper != op2->AsAllocObj()->gtNewHelper) || | ||
| (op1->AsAllocObj()->gtAllocObjClsHnd != op2->AsAllocObj()->gtAllocObjClsHnd)) | ||
| { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
GT_ALLOCOBJ extra-field comparison doesn’t include gtHelperHasSideEffects, which is later used to set GTF_CALL_M_ALLOC_SIDE_EFFECTS when the node is morphed into a helper call. Include gtHelperHasSideEffects in the comparison so nodes with different side-effect behavior are not treated as identical.
SingleAccretion
left a comment
There was a problem hiding this comment.
LGTM modulo comments (the test doesn't look right).
| Unsafe.As<ClassTwo>(obj).Field = 999; | ||
| return Unsafe.As<ClassOne>(obj).Field; |
There was a problem hiding this comment.
This is UB.
Also, the comment at the top doesn't make sense to me.
// After inlining, redundant branch opts (which uses the liberal VN) can fold
// the type check using the wrong field-based alias information, producing
// incorrect results.
What type check?
There was a problem hiding this comment.
Trying to do all this via copilot, so...
Maybe if we add a second type check at the use we can expose this without UB?
| <RequiresProcessIsolation>true</RequiresProcessIsolation> | ||
| <Optimize>True</Optimize> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <Compile Include="$(MSBuildProjectName).cs" /> | ||
| <CLRTestEnvironmentVariable Include="DOTNET_TieredCompilation" Value="0" /> |
There was a problem hiding this comment.
| <RequiresProcessIsolation>true</RequiresProcessIsolation> | |
| <Optimize>True</Optimize> | |
| </PropertyGroup> | |
| <ItemGroup> | |
| <Compile Include="$(MSBuildProjectName).cs" /> | |
| <CLRTestEnvironmentVariable Include="DOTNET_TieredCompilation" Value="0" /> | |
| <Optimize>True</Optimize> | |
| </PropertyGroup> | |
| <ItemGroup> | |
| <Compile Include="$(MSBuildProjectName).cs" /> |
There was a problem hiding this comment.
The whole csproj should be deleted and the test added into one of the merged csprojs instead.
Exercises a scenario where tail merge incorrectly merges stores with different field sequences (ClassOne.Field vs ClassTwo.Field via Unsafe.As at the same offset). After inlining, redundant branch opts uses the liberal VN to fold branches, exposing incorrect alias analysis from the wrong field sequence. Without the gtFieldSeq check in GenTree::Compare, the test returns 42 instead of the expected 999. Co-authored-by: Copilot <[email protected]>
…nupGenTreeCompare
Add some missing checks