Uh oh!
There was an error while loading. Please reload this page.
Arm64: Improve support for HW_Flag_ReturnsPerElementMask - #128326
Conversation
When wrapping an intrinsic node that has an embedded mask with a ConditionalSelect, ensure that the constant node in op3 has a mask type when the intrinsic has the HW_Flag_ReturnsPerElementMask flag. Build out further support for ConditionalSelect_Predicates, and use this to wrap nodes with HW_Flag_ReturnsPerElementMask. Add GenTree::IsSelectZero and update various areas in HW intrinsic codegen to ensure this intrinsic assembles correctly. Use a tree visitor for assigning `TYP_MASK` to intrinsics that have `HW_Flag_ReturnsPerElementMask`. The current version of `impHWIntrinsic` does not process child nodes of the tree it returns for mask types, only the root node.
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
FYI if it's not known, assert in PR testing |
snickolls-arm
commented
Jun 30, 2026
I didn't see thanks, will take a look. |
There was a problem hiding this comment.
Pull request overview
This PR extends Arm64 SVE support for intrinsics that return per-element masks by introducing shared recognition of SVE conditional-select variants, adding a “select-zero” helper, updating containment/codegen to treat mask-zero similarly to vector-zero, and updating constant-folding / emitter behavior to match SVE predicate element semantics.
Changes:
- Adds
HWIntrinsicInfo::IsSveConditionalSelectandGenTree::IsSelectZero, and updates lowering/LSRA/codegen paths to use them. - Updates SVE mask creation intrinsics to be treated as returning per-element masks and adjusts import/constant construction accordingly.
- Adjusts SVE emitter options handling and updates/extends a JIT test to validate the new mask element representation.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tests/JIT/opt/SVE/ConditionalSelectConstants.cs | Updates expectations and adds a smoke check validating mask element values. |
| src/coreclr/jit/simd.h | Changes mask-to-vector constant evaluation semantics on Arm64 and adds helper to convert elementwise mask values to bitwise masks for folding. |
| src/coreclr/jit/lsraarm64.cpp | Treats both vector and predicate conditional-select forms uniformly when identifying embedded/contained selects. |
| src/coreclr/jit/lowerarmarch.cpp | Expands lowering/containment to include predicate conditional-select and uses IsSelectZero for zero-detection. |
| src/coreclr/jit/hwintrinsiclistarm64sve.h | Marks SVE CreateTrue/FalseMask intrinsics as returning per-element masks; updates containment flag for predicate select. |
| src/coreclr/jit/hwintrinsiccodegenarm64.cpp | Switches embedded-mask setup and containment checks from vector-zero to select-zero, enabling mask-zero handling. |
| src/coreclr/jit/hwintrinsicarm64.cpp | Imports SVE false masks via gtNewZeroConNode and updates true-mask constant folding to use mask constants. |
| src/coreclr/jit/hwintrinsic.h | Introduces IsSveConditionalSelect helper for consistent conditional-select recognition. |
| src/coreclr/jit/gentree.h | Adds GenTree::IsSelectZero helper for conditional-select zero detection across vectors/masks. |
| src/coreclr/jit/gentree.cpp | Extends gtNewZeroConNode to create mask-zero constants and updates folding for SVE mask representation. |
| src/coreclr/jit/emitarm64sve.cpp | Forces byte arrangement for predicate encodings where required and relaxes/adjusts scalable option assertions. |
Comments suppressed due to low confidence (1)
src/coreclr/jit/lowerarmarch.cpp:3636
TryContainingCselOpnow accepts bothNI_Sve_ConditionalSelectandNI_Sve_ConditionalSelect_Predicates, but the containment fast-path is still gated onchildNode->Op(3)->IsVectorZero(). For the predicate variant, op3 is expected to be a mask-zero (pfalse) rather than a vector-zero, so this will never trigger and makes the new broader check ineffective for mask selects.
if (!HWIntrinsicInfo::IsSveConditionalSelect(childNode->GetHWIntrinsicId()))
{
return false;
}
if (childNode->Op(2)->IsEmbMaskOp())
{
// Do not optimise if the conditional select node is added to embed the mask for its op2. Such conditional
// select nodes are optimised out while emitting the embedded variant of an instruction associated with op2.
assert(childNode->Op(2)->isContained());
return false;
}
bool canContain = false;
var_types simdBaseType = parentNode->GetSimdBaseType();
if (childNode->Op(3)->IsVectorZero())
{
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
tannergooding
commented
Jul 2, 2026
@snickolls-arm, as an FYI this is going to change a bit with #130049. It is effectively merging most of the tables for the xplat intrinsics into a new combined table and the import logic itself into a combined handler as well. The handling between That should make it a bit simpler to lightup for SVE, as you'll not need to extend or duplicate the tables again or add a bunch of |
Uh oh!
There was an error while loading. Please reload this page.
snickolls-arm
commented
Jul 2, 2026
Thanks for the heads up. This should reduce the diffs I have for implementing the |
There was a problem hiding this comment.
LGTM, but I'd really like for us to fix the SVE usages of ReturnsPerElementMask to be consistent with its usage on other targets/platforms.
CC. @dotnet/jit-contrib, @dhartglassMSFT for secondary review
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "bba2c056585e9a61b3e275825eb4f514c6e2b072",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "bbdd508a9371af9922e65ad073f9e372bc5c208e",
"last_reviewed_commit": "bba2c056585e9a61b3e275825eb4f514c6e2b072",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "bbdd508a9371af9922e65ad073f9e372bc5c208e",
"last_recorded_worker_run_id": "29684898666",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "bba2c056585e9a61b3e275825eb4f514c6e2b072",
"review_id": 4730673113
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: The problem is real and well-motivated. When wrapping an intrinsic with HW_Flag_ReturnsPerElementMask in a ConditionalSelect, the false-value constant (op3) previously used a vector-zero even for mask-typed results, and the mask Create*Mask intrinsics were not marked as returning per-element masks. This PR makes the type/flag invariant consistent (HW_Flag_ReturnsPerElementMask <==> TYP_MASK) and builds out ConditionalSelect_Predicates so mask-producing operations assemble correctly.
Approach: The approach is sound and matches maintainer direction (jakobbotsch requested exactly this consistency work). Introducing IsSveConditionalSelect/IsZeroForSelect helpers, treating mask-zero like vector-zero across lowering/LSRA/codegen, and forcing byte arrangement for the single predicate-select encoding are all reasonable. The main risk is a deliberate change to Arm64 mask->vector constant-folding semantics that must be applied consistently across all consumers.
Summary: EvaluateSimdCvtMaskToVector to emit {0, 1} instead of {0, AllBitsSet} on Arm64 is a cross-cutting semantic change. The author compensated in gtFoldExprHWIntrinsic via the new ConvertToBitWiseMask, but the parallel constant-folding reconstruction in valuenum.cpp appears to consume the same helper without that conversion. A JIT reviewer should confirm whether that VN path is also affected (it lies in unchanged code, so it is out of this review's strict change-scope but is directly coupled to a changed helper). See the inline comment on simd.h.
Detailed Findings
⚠️ Correctness — Arm64 mask->vector semantics change may leave a stale value-numbering consumer
EvaluateSimdCvtMaskToVector (and EvaluateSimdPatternToVector) now produce element value 1 rather than AllBitsSet on Arm64. The gtFoldExprHWIntrinsic ConditionalSelect folding path was updated to call ConvertToBitWiseMask before feeding the value into EvaluateBinarySimd(GT_AND, ...). However, valuenum.cpp (~line 9645, NI_Sve_ConditionalSelect branch under TARGET_ARM64) performs the same (y & x) | (z & ~x) reconstruction using EvaluateSimdCvtMaskToVector's result directly, without the new conversion. If reachable with constant operands, this would fold to an incorrect result (masking with 1 instead of AllBitsSet). This is coupled to a changed helper but lives in a file not in this PR's diff — flagged for human confirmation rather than as a strict in-scope blocker. Details in the inline comment on simd.h.
✅ Invariant enforcement — HW_Flag_ReturnsPerElementMask <==> TYP_MASK
The new assert in LowerHWIntrinsic correctly codifies the intended invariant and is placed early enough to catch inconsistencies before embedded-mask analysis depends on it. The hwintrinsiclistarm64sve.h flag additions on the CreateTrue/FalseMask* intrinsics are consistent with importing them as TYP_MASK constants via gtNewMskConNode/gtNewZeroConNode.
✅ Emitter byte-arrangement handling
Forcing INS_OPTS_SCALABLE_B for IF_SVE_DC_3A and the predicate sel (IF_SVE_CZ_4A) encodings is justified by the accompanying comment: these are bitwise predicate operations with a single encoding, so lane arrangement is irrelevant to the result. Relaxing the assert to insOptsScalable(opt) before normalizing to byte is reasonable.
✅ Helper consolidation
IsSveConditionalSelect and IsZeroForSelect are applied consistently across hwintrinsiccodegenarm64.cpp, lowerarmarch.cpp, and lsraarm64.cpp, replacing the previous == NI_Sve_ConditionalSelect / IsVectorZero() checks. This correctly extends the existing vector-select handling to the predicate variant.
⚠️ Test coverage — predicate-select paths only lightly exercised
The test update (ConditionalSelectConstants.cs) adds a useful Smoke() check and corrects expected values that shift because mask elements are now 1 rather than sign-extended AllBitsSet (e.g. -3 -> 3, 4080 -> 16). This validates the changed CreateTrueMask constant representation. However, there is no test that directly exercises ConditionalSelect_Predicates (mask-typed op3 = CreateFalseMask) end-to-end, nor the value-numbering constant-fold path noted above. Given this is an observable behavior change with a clear repro area, a targeted test for the predicate-select and the folded-VN path would strengthen confidence. Note the earlier PR-testing assert (maskBit == 0 firing in ConvertToBitWiseMask) was reported and addressed during development; confirm current CI is green before merge.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 159.6 AIC · ⌖ 11.5 AIC · ⊞ 10K
Uh oh!
There was an error while loading. Please reload this page.
Fix ARM64 ConvertMaskToVector codegen to produce AllBitsSet in active lanes, matching the binary mask representation assumed by JIT optimizations. Update SVE/SVE2 intrinsic tests to validate against this value, and add a focused ConditionalSelect regression test. Add comparison functions for floating-point type mask elements, as AllBitsSet falls within the NaN range. Simplify any touched HardwareIntrinsic test helpers using INumberBase<T> while in the area.
When wrapping an intrinsic node that has an embedded mask with a
ConditionalSelect, ensure that the constant node in op3 has a mask type when the intrinsic has theHW_Flag_ReturnsPerElementMaskflag.Build out further support for
ConditionalSelect_Predicates, and use this to wrap nodes withHW_Flag_ReturnsPerElementMask. AddGenTree::IsSelectZeroand update various areas in HW intrinsic codegen to ensure this intrinsic assembles correctly.