Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
73f5d24
JIT: Remove floating-point CreateScalarUnsafe during xarch lowering
tannergooding Jul 9, 2026
4230fe9
JIT: stop builders manufacturing floating-point CreateScalarUnsafe
tannergooding Jul 9, 2026
ddaf8f8
JIT: remove intermediate CreateScalar when containing it during xarch…
tannergooding Jul 9, 2026
eec5293
JIT: remove dead contained-CreateScalarUnsafe support
tannergooding Jul 9, 2026
cbe7ff1
JIT: permit removed floating-point CreateScalarUnsafe operands in Get…
tannergooding Jul 9, 2026
2997855
JIT: remove ToVector256Unsafe/ToVector512Unsafe during xarch lowering
tannergooding Jul 9, 2026
fde7f87
JIT: remove GetLower/GetLower128 during xarch lowering
tannergooding Jul 9, 2026
802aa60
JIT: remove GetLower during arm64 lowering
tannergooding Jul 9, 2026
89a3cde
Only remove transparent SIMD reinterprets when consumed by a hwintrinsic
tannergooding Jul 10, 2026
e7d1bf3
Tighten store asserts and drop redundant null checks now that removal…
tannergooding Jul 10, 2026
ccf7e07
Inline the GetHWIntrinsicIdForBinOp operand asserts
tannergooding Jul 10, 2026
342eff9
Relax the AdvSimd.Insert targetReg != op3Reg assert to allow aliased …
tannergooding Jul 10, 2026
0de4361
Remove ToVector128Unsafe and floating-point CreateScalarUnsafe during…
tannergooding Jul 10, 2026
6f62054
Relax gtNewSimdBinOpNode operand-shape asserts to be HIR-only
tannergooding Jul 10, 2026
6a9851a
Relax GetHWIntrinsicIdForCmpOp operand-shape asserts to be HIR-only
tannergooding Jul 11, 2026
e75b094
Merge remote-tracking branch 'dotnet/main' into tannergooding-jit-fre…
tannergooding Jul 11, 2026
01465a4
Merge branch 'main' into tannergooding-jit-free-scalar-conversion-hwi…
tannergooding Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 59 additions & 34 deletions src/coreclr/jit/gentree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22169,13 +22169,6 @@ bool GenTree::isContainableHWIntrinsic() const
return node->GetSimdSize() == 16;
}

case NI_Vector_CreateScalar:
case NI_Vector_CreateScalarUnsafe:
{
// These HWIntrinsic operations are contained as part of scalar ops
return true;
}

case NI_X86Base_LoadAndDuplicateToVector128:
case NI_X86Base_MoveAndDuplicate:
case NI_AVX_BroadcastScalarToVector128:
Expand Down Expand Up @@ -22836,16 +22829,26 @@ GenTree* Compiler::gtNewSimdBinOpNode(
assert(varTypeIsArithmetic(simdBaseType));

assert(op1 != nullptr);
assert(op1->TypeIs(type, simdBaseType, genActualType(simdBaseType)) ||
(op1->TypeIs(TYP_SIMD12) && type == TYP_SIMD16));

assert(op2 != nullptr);

// The operand-shape asserts below validate that the HIR builder is producing the exact vector
// shape we expect. Once in LIR, lowering is free to feed a size-changing reinterpret operand
// directly -- e.g. an elided GetLower or ToVectorXXXUnsafe -- since the binop operates on the low
// simdSize bytes and containment validates the memory-operand size (operandSize >= expectedSize)
// before allowing a load. Only enforce the shape in HIR.
bool isLIR = (fgNodeThreading == NodeThreading::LIR);

if (!isLIR)
{
assert(op1->TypeIs(type, simdBaseType, genActualType(simdBaseType)) ||
(op1->TypeIs(TYP_SIMD12) && type == TYP_SIMD16));
}

if ((op == GT_LSH) || (op == GT_RSH) || (op == GT_RSZ))
{
assert(genActualType(op2) == TYP_INT);
}
else
else if (!isLIR)
{
assert((genActualType(op2) == genActualType(type)) || (genActualType(op2) == genActualType(simdBaseType)) ||
(op2->TypeIs(TYP_SIMD12) && (type == TYP_SIMD16)));
Expand Down Expand Up @@ -31743,8 +31746,20 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
assert(varTypeIsArithmetic(simdBaseType));
assert(varTypeIsSIMD(simdType));

// A floating-point CreateScalarUnsafe is removed during lowering, which can leave a scalar
// operand occupying the low element of a SIMD register. Such an operand is consumed here at
// full register width, so it is valid in addition to a full SIMD operand.
//
// Likewise, once in LIR, lowering may feed a size-changing SIMD reinterpret operand directly --
// e.g. an elided GetLower or ToVectorXXXUnsafe. It still occupies a full SIMD register and is
// consumed at the node's width, so treat any SIMD-typed operand as a full-vector operand rather
// than requiring an exact size match. In HIR the operand size must still be exact.
auto isFullVectorOp = [=](GenTree* op) -> bool {
return op->TypeIs(simdType) || ((comp->fgNodeThreading == NodeThreading::LIR) && varTypeIsSIMD(op));
};

assert(op1 != nullptr);
assert(op1->TypeIs(simdType));
assert(isFullVectorOp(op1) || varTypeIsFloating(op1));
assert(op2 != nullptr);
Comment thread
tannergooding marked this conversation as resolved.

#if defined(TARGET_XARCH)
Expand All @@ -31770,7 +31785,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
{
case GT_ADD:
{
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2) || varTypeIsFloating(op2));

#if defined(TARGET_XARCH)
if (simdSize == 64)
Expand Down Expand Up @@ -31817,7 +31832,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
case GT_AND:
{
assert(!isScalar);
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2) || varTypeIsFloating(op2));

#if defined(TARGET_XARCH)
if (simdSize == 64)
Expand Down Expand Up @@ -31853,7 +31868,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
case GT_AND_NOT:
{
assert(!isScalar);
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2) || varTypeIsFloating(op2));

if (comp->fgNodeThreading != NodeThreading::LIR)
{
Expand Down Expand Up @@ -31904,7 +31919,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
#else
assert(varTypeIsFloating(simdBaseType));
#endif
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2) || varTypeIsFloating(op2));

#if defined(TARGET_XARCH)
if (varTypeIsFloating(simdBaseType))
Expand Down Expand Up @@ -31942,7 +31957,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
case GT_LSH:
{
assert(!isScalar);
assert(op2->TypeIs(simdType) || varTypeIsInt(op2));
assert(isFullVectorOp(op2) || varTypeIsInt(op2));
assert(varTypeIsIntegral(simdBaseType));

#if defined(TARGET_XARCH)
Expand Down Expand Up @@ -31998,7 +32013,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
case GT_MUL:
{
#if defined(TARGET_XARCH)
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2) || varTypeIsFloating(op2));

if (simdSize == 64)
{
Expand Down Expand Up @@ -32045,11 +32060,11 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
}
else if (simdBaseType == TYP_DOUBLE)
{
id = op2->TypeIs(simdType) ? NI_AdvSimd_Arm64_Multiply : NI_AdvSimd_Arm64_MultiplyByScalar;
id = isFullVectorOp(op2) ? NI_AdvSimd_Arm64_Multiply : NI_AdvSimd_Arm64_MultiplyByScalar;
}
else if (!varTypeIsLong(simdBaseType))
{
id = op2->TypeIs(simdType) ? NI_AdvSimd_Multiply : NI_AdvSimd_MultiplyByScalar;
id = isFullVectorOp(op2) ? NI_AdvSimd_Multiply : NI_AdvSimd_MultiplyByScalar;
}
#elif defined(TARGET_WASM)
if (!varTypeIsByte(simdBaseType))
Expand All @@ -32065,7 +32080,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
case GT_OR:
{
assert(!isScalar);
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2) || varTypeIsFloating(op2));

#if defined(TARGET_XARCH)
if (simdSize == 64)
Expand Down Expand Up @@ -32101,7 +32116,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
case GT_ROL:
{
assert(!isScalar);
assert(op2->TypeIs(simdType) || varTypeIsInt(op2));
assert(isFullVectorOp(op2) || varTypeIsInt(op2));
assert(varTypeIsIntegral(simdBaseType));

#if defined(TARGET_XARCH)
Expand All @@ -32116,7 +32131,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
case GT_ROR:
{
assert(!isScalar);
assert(op2->TypeIs(simdType) || varTypeIsInt(op2));
assert(isFullVectorOp(op2) || varTypeIsInt(op2));
assert(varTypeIsIntegral(simdBaseType));

#if defined(TARGET_XARCH)
Expand All @@ -32131,7 +32146,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
case GT_RSH:
{
assert(!isScalar);
assert(op2->TypeIs(simdType) || varTypeIsInt(op2));
assert(isFullVectorOp(op2) || varTypeIsInt(op2));
assert(varTypeIsIntegral(simdBaseType));

#if defined(TARGET_XARCH)
Expand Down Expand Up @@ -32190,7 +32205,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
case GT_RSZ:
{
assert(!isScalar);
assert(op2->TypeIs(simdType) || varTypeIsInt(op2));
assert(isFullVectorOp(op2) || varTypeIsInt(op2));
assert(varTypeIsIntegral(simdBaseType));

#if defined(TARGET_XARCH)
Expand Down Expand Up @@ -32245,7 +32260,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,

case GT_SUB:
{
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2) || varTypeIsFloating(op2));

#if defined(TARGET_XARCH)
if (simdSize == 64)
Expand Down Expand Up @@ -32292,7 +32307,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForBinOp(Compiler* comp,
case GT_XOR:
{
assert(!isScalar);
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2) || varTypeIsFloating(op2));

#if defined(TARGET_XARCH)
if (simdSize == 64)
Expand Down Expand Up @@ -32367,8 +32382,18 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForCmpOp(Compiler* comp,
assert(varTypeIsArithmetic(simdBaseType));
assert(varTypeIsSIMD(simdType));

#ifdef DEBUG
// Once in LIR, lowering may feed a size-changing SIMD reinterpret operand directly -- e.g. an
// elided GetLower or ToVectorXXXUnsafe. It still occupies a full SIMD register and is consumed
// at the node's width, so treat any SIMD-typed operand as a full-vector operand rather than
// requiring an exact size match. In HIR the operand size must still be exact.
auto isFullVectorOp = [=](GenTree* op) -> bool {
return op->TypeIs(simdType) || ((comp->fgNodeThreading == NodeThreading::LIR) && varTypeIsSIMD(op));
};
#endif // DEBUG

assert(op1 != nullptr);
assert(op1->TypeIs(simdType));
assert(isFullVectorOp(op1));
assert(op2 != nullptr);

#if defined(TARGET_XARCH)
Expand Down Expand Up @@ -32420,7 +32445,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForCmpOp(Compiler* comp,
{
case GT_EQ:
{
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2));

#if defined(TARGET_XARCH)
if (varTypeIsMask(type))
Expand Down Expand Up @@ -32462,7 +32487,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForCmpOp(Compiler* comp,

case GT_GE:
{
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2));

#if defined(TARGET_XARCH)
if (varTypeIsMask(type))
Expand Down Expand Up @@ -32509,7 +32534,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForCmpOp(Compiler* comp,

case GT_GT:
{
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2));

#if defined(TARGET_XARCH)
if (varTypeIsMask(type))
Expand Down Expand Up @@ -32570,7 +32595,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForCmpOp(Compiler* comp,

case GT_LE:
{
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2));

#if defined(TARGET_XARCH)
if (varTypeIsMask(type))
Expand Down Expand Up @@ -32617,7 +32642,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForCmpOp(Compiler* comp,

case GT_LT:
{
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2));

// !GE

Expand Down Expand Up @@ -32680,7 +32705,7 @@ NamedIntrinsic GenTreeHWIntrinsic::GetHWIntrinsicIdForCmpOp(Compiler* comp,

case GT_NE:
{
assert(op2->TypeIs(simdType));
assert(isFullVectorOp(op2));

#if defined(TARGET_XARCH)
if (varTypeIsMask(type))
Expand Down
8 changes: 7 additions & 1 deletion src/coreclr/jit/hwintrinsiccodegenarm64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1426,7 +1426,13 @@ void CodeGen::genHWIntrinsic(GenTreeHWIntrinsic* node)
// fmov (scalar) zeros the upper bits and is not safe to use
assert(!intrin.op3->isContainedFltOrDblImmed());

assert(targetReg != op3Reg);
// The mov above copies op1 into targetReg and the ins below then reads op3. That is
// only unsafe when targetReg == op3Reg but targetReg != op1Reg, as the mov would then
// clobber op3 before it is read. LSRA marks op3 delayFree, so a distinct op3 can never
// share the def register; targetReg == op3Reg is only reachable when op3 aliases op1
// (e.g. Vector.Create(x, ..., x, ...) after a floating-point CreateScalarUnsafe is
// elided into a bare scalar), in which case the mov is skipped and op3 is preserved.
assert((targetReg != op3Reg) || (targetReg == op1Reg));

HWIntrinsicImmOpHelper helper(this, intrin.op2, node);

Expand Down
7 changes: 3 additions & 4 deletions src/coreclr/jit/hwintrinsiccodegenxarch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2274,10 +2274,9 @@ void CodeGen::genBaseIntrinsic(GenTreeHWIntrinsic* node, insOpts instOptions)
case NI_Vector_AsVector3:
case NI_Vector_ToScalar:
{
// genOperandDesc looks through a contained CreateScalar/CreateScalarUnsafe to the operand it
// wraps, which may itself live in a register (e.g. Vector128.CreateScalarUnsafe(x).ToScalar()).
// We therefore use the descriptor's containment - not op1 directly - to decide instruction
// selection: only a true memory operand can be read with a plain integer load.
// op1 may be a contained memory operand or live in a register. We use the descriptor's
// containment - not op1 directly - to decide instruction selection: only a true memory
// operand can be read with a plain integer load.
OperandDesc op1Desc = genOperandDesc(ins, op1);

if (op1Desc.IsContained())
Expand Down
27 changes: 5 additions & 22 deletions src/coreclr/jit/instr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1136,18 +1136,15 @@ CodeGen::OperandDesc CodeGen::genOperandDesc(instruction ins, GenTree* op)
{
assert(simdBaseType == TYP_DOUBLE);
}
// If broadcast node is contained, should mean that we have some forms like
// Broadcast -> CreateScalarUnsafe -> Scalar.
// If so, directly emit scalar.
// In the code below, we specially handle the `Broadcast -> CNS_INT/CNS_LNG` form and
// handle other cases recursively.
// A contained broadcast wraps its scalar operand directly (any CreateScalar/Unsafe
// wrapper was removed during lowering). We specially handle the
// `Broadcast -> CNS_INT/CNS_LNG` form below and handle other cases recursively.
GenTree* hwintrinsicChild = hwintrinsic->Op(1);
assert(hwintrinsicChild->isContained());
if (hwintrinsicChild->IsIntegralConst())
{
// a special case is when the operand of CreateScalarUnsafe is an integer type,
// CreateScalarUnsafe node will be folded, so we directly match a pattern of
// broadcast -> LCL_VAR(TYP_(U)INT/LONG)
// A broadcast over an integral constant is materialized as a data constant
// that we can read directly from memory.
INT64 scalarValue = hwintrinsicChild->AsIntConCommon()->IntegralValue();
UNATIVE_OFFSET cnum = emit->emitDataConst(&scalarValue, genTypeSize(simdBaseType),
genTypeSize(simdBaseType), simdBaseType);
Expand All @@ -1162,20 +1159,6 @@ CodeGen::OperandDesc CodeGen::genOperandDesc(instruction ins, GenTree* op)
break;
}

case NI_Vector_CreateScalar:
case NI_Vector_CreateScalarUnsafe:
{
// The hwintrinsic should be contained and its
// op1 should be either contained or spilled. This
// allows us to transparently "look through" the
// CreateScalar/Unsafe and treat it directly like
// a load from memory.

assert(hwintrinsic->isContained());
op = hwintrinsic->Op(1);
return genOperandDesc(ins, op);
}

default:
{
assert(hwintrinsic->OperIsMemoryLoad());
Expand Down
Loading
Loading