diff --git a/src/coreclr/jit/codegenriscv64.cpp b/src/coreclr/jit/codegenriscv64.cpp index 94d3e57aaace04..771546c814d701 100644 --- a/src/coreclr/jit/codegenriscv64.cpp +++ b/src/coreclr/jit/codegenriscv64.cpp @@ -4744,6 +4744,18 @@ void CodeGen::genIntrinsic(GenTreeIntrinsic* treeNode) case NI_System_Math_MaxNumber: instr = is4 ? INS_fmax_s : INS_fmax_d; break; + case NI_System_Math_Min: + instr = INS_min; + break; + case NI_System_Math_MinUnsigned: + instr = INS_minu; + break; + case NI_System_Math_Max: + instr = INS_max; + break; + case NI_System_Math_MaxUnsigned: + instr = INS_maxu; + break; case NI_PRIMITIVE_LeadingZeroCount: instr = is4 ? INS_clzw : INS_clz; break; diff --git a/src/coreclr/jit/emitriscv64.cpp b/src/coreclr/jit/emitriscv64.cpp index 1b38e9688f5922..39f39b77591c90 100644 --- a/src/coreclr/jit/emitriscv64.cpp +++ b/src/coreclr/jit/emitriscv64.cpp @@ -826,7 +826,8 @@ void emitter::emitIns_R_R_R( if ((INS_add <= ins && ins <= INS_and) || (INS_mul <= ins && ins <= INS_remuw) || (INS_addw <= ins && ins <= INS_sraw) || (INS_fadd_s <= ins && ins <= INS_fmax_s) || (INS_fadd_d <= ins && ins <= INS_fmax_d) || (INS_feq_s <= ins && ins <= INS_fle_s) || - (INS_feq_d <= ins && ins <= INS_fle_d) || (INS_lr_w <= ins && ins <= INS_amomaxu_d)) + (INS_feq_d <= ins && ins <= INS_fle_d) || (INS_lr_w <= ins && ins <= INS_amomaxu_d) || + (INS_min <= ins && ins <= INS_maxu)) { #ifdef DEBUG switch (ins) @@ -913,6 +914,11 @@ void emitter::emitIns_R_R_R( case INS_amominu_d: case INS_amomaxu_w: case INS_amomaxu_d: + + case INS_min: + case INS_minu: + case INS_max: + case INS_maxu: break; default: NYI_RISCV64("illegal ins within emitIns_R_R_R!"); @@ -4081,6 +4087,15 @@ void emitter::emitDispInsName( return emitDispIllegalInstruction(code); } return; + case 0b0000101: + { + if ((opcode3 >> 2) != 1) // clmul[h] unsupported + return emitDispIllegalInstruction(code); + + static const char names[][5] = {"min ", "minu", "max ", "maxu"}; + printf("%s %s, %s, %s\n", names[opcode3 & 0b11], rd, rs1, rs2); + return; + } default: return emitDispIllegalInstruction(code); } diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index 5bf5fb170af2f1..417be90fcd1ec0 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -5404,10 +5404,12 @@ unsigned Compiler::gtSetEvalOrder(GenTree* tree) case NI_System_Math_MaxMagnitude: case NI_System_Math_MaxMagnitudeNumber: case NI_System_Math_MaxNumber: + case NI_System_Math_MaxUnsigned: case NI_System_Math_Min: case NI_System_Math_MinMagnitude: case NI_System_Math_MinMagnitudeNumber: case NI_System_Math_MinNumber: + case NI_System_Math_MinUnsigned: case NI_System_Math_Pow: case NI_System_Math_Round: case NI_System_Math_Sin: @@ -5759,10 +5761,12 @@ unsigned Compiler::gtSetEvalOrder(GenTree* tree) case NI_System_Math_MaxMagnitude: case NI_System_Math_MaxMagnitudeNumber: case NI_System_Math_MaxNumber: + case NI_System_Math_MaxUnsigned: case NI_System_Math_Min: case NI_System_Math_MinMagnitude: case NI_System_Math_MinMagnitudeNumber: case NI_System_Math_MinNumber: + case NI_System_Math_MinUnsigned: { level++; break; @@ -12758,6 +12762,9 @@ void Compiler::gtDispTree(GenTree* tree, case NI_System_Math_MaxNumber: printf(" maxNumber"); break; + case NI_System_Math_MaxUnsigned: + printf(" maxUnsigned"); + break; case NI_System_Math_Min: printf(" min"); break; @@ -12770,6 +12777,9 @@ void Compiler::gtDispTree(GenTree* tree, case NI_System_Math_MinNumber: printf(" minNumber"); break; + case NI_System_Math_MinUnsigned: + printf(" minUnsigned"); + break; case NI_System_Math_Pow: printf(" pow"); break; diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 208f6212cbd5c9..2dd1ebd574a93b 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -7948,6 +7948,8 @@ bool Compiler::IsTargetIntrinsic(NamedIntrinsic intrinsicName) case NI_System_Math_ReciprocalSqrtEstimate: return true; + case NI_System_Math_MinUnsigned: + case NI_System_Math_MaxUnsigned: case NI_PRIMITIVE_LeadingZeroCount: case NI_PRIMITIVE_TrailingZeroCount: case NI_PRIMITIVE_PopCount: @@ -8022,10 +8024,12 @@ bool Compiler::IsMathIntrinsic(NamedIntrinsic intrinsicName) case NI_System_Math_MaxMagnitude: case NI_System_Math_MaxMagnitudeNumber: case NI_System_Math_MaxNumber: + case NI_System_Math_MaxUnsigned: case NI_System_Math_Min: case NI_System_Math_MinMagnitude: case NI_System_Math_MinMagnitudeNumber: case NI_System_Math_MinNumber: + case NI_System_Math_MinUnsigned: case NI_System_Math_MultiplyAddEstimate: case NI_System_Math_Pow: case NI_System_Math_ReciprocalEstimate: @@ -9745,535 +9749,576 @@ GenTree* Compiler::impMinMaxIntrinsic(CORINFO_METHOD_HANDLE method, { var_types callType = JITtype2varType(callJitType); - assert(varTypeIsFloating(callType)); + assert(varTypeIsArithmetic(callType)); assert(sig->numArgs == 2); - GenTreeDblCon* cnsNode = nullptr; - GenTree* otherNode = nullptr; + if (varTypeIsFloating(callType)) + { + GenTreeDblCon* cnsNode = nullptr; + GenTree* otherNode = nullptr; - GenTree* op2 = impImplicitR4orR8Cast(impStackTop().val, callType); - GenTree* op1 = impImplicitR4orR8Cast(impStackTop(1).val, callType); + GenTree* op2 = impImplicitR4orR8Cast(impStackTop().val, callType); + GenTree* op1 = impImplicitR4orR8Cast(impStackTop(1).val, callType); #if defined(FEATURE_HW_INTRINSICS) && defined(TARGET_XARCH) - // If Avx10.2 is enabled, the min/max operations can be done using the - // new minmax instructions which is faster than using the combination - // of instructions for lower ISAs. We can use the minmax instructions - - if (compOpportunisticallyDependsOn(InstructionSet_AVX10v2)) - { - impPopStack(); - impPopStack(); - /** - * ctrlByte A control byte (imm8) that specifies the type of min/max operation and sign behavior: - * - Bits [1:0] (Op-select): Determines the operation performed: - * - 0b00: minimum - Returns x if x ≤ y, otherwise y; NaN handling applies. - * - 0b01: maximum - Returns x if x ≥ y, otherwise y; NaN handling applies. - * - 0b10: minimumMagnitude - Compares absolute values, returns the smaller magnitude. - * - 0b11: maximumMagnitude - Compares absolute values, returns the larger magnitude. - * - Bit [4] (min/max mode): Determines whether the instruction follows IEEE-compliant NaN handling: - * - 0: Standard min/max (propagates NaNs). - * - 1: Number-preferential min/max (ignores signaling NaNs). - * - Bits [3:2] (Sign control): Defines how the result’s sign is determined: - * - 0b00: Select sign from the first operand (src1). - * - 0b01: Select sign from the comparison result. - * - 0b10: Force result sign to 0 (positive). - * - 0b11: Force result sign to 1 (negative). - */ - uint8_t ctrlByte = 0x04; // Select sign from comparison result - ctrlByte |= isMax ? 0x01 : 0x00; - ctrlByte |= isMagnitude ? 0x02 : 0x00; - ctrlByte |= isNumber ? 0x10 : 0x00; - - GenTree* retNode = gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1, op2, gtNewIconNode(ctrlByte), - NI_AVX10v2_MinMaxScalar, callJitType, 16); - return gtNewSimdToScalarNode(genActualType(callType), retNode, callJitType, 16); - } + // If Avx10.2 is enabled, the min/max operations can be done using the + // new minmax instructions which is faster than using the combination + // of instructions for lower ISAs. We can use the minmax instructions + + if (compOpportunisticallyDependsOn(InstructionSet_AVX10v2)) + { + impPopStack(); + impPopStack(); + /** + * ctrlByte A control byte (imm8) that specifies the type of min/max operation and sign behavior: + * - Bits [1:0] (Op-select): Determines the operation performed: + * - 0b00: minimum - Returns x if x ≤ y, otherwise y; NaN handling applies. + * - 0b01: maximum - Returns x if x ≥ y, otherwise y; NaN handling applies. + * - 0b10: minimumMagnitude - Compares absolute values, returns the smaller magnitude. + * - 0b11: maximumMagnitude - Compares absolute values, returns the larger magnitude. + * - Bit [4] (min/max mode): Determines whether the instruction follows IEEE-compliant NaN + * handling: + * - 0: Standard min/max (propagates NaNs). + * - 1: Number-preferential min/max (ignores signaling NaNs). + * - Bits [3:2] (Sign control): Defines how the result’s sign is determined: + * - 0b00: Select sign from the first operand (src1). + * - 0b01: Select sign from the comparison result. + * - 0b10: Force result sign to 0 (positive). + * - 0b11: Force result sign to 1 (negative). + */ + uint8_t ctrlByte = 0x04; // Select sign from comparison result + ctrlByte |= isMax ? 0x01 : 0x00; + ctrlByte |= isMagnitude ? 0x02 : 0x00; + ctrlByte |= isNumber ? 0x10 : 0x00; + + GenTree* retNode = gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1, op2, gtNewIconNode(ctrlByte), + NI_AVX10v2_MinMaxScalar, callJitType, 16); + return gtNewSimdToScalarNode(genActualType(callType), retNode, callJitType, 16); + } #endif // FEATURE_HW_INTRINSICS && TARGET_XARCH - if (op2->IsCnsFltOrDbl()) - { - cnsNode = op2->AsDblCon(); - otherNode = op1; - } - else if (op1->IsCnsFltOrDbl()) - { - cnsNode = op1->AsDblCon(); - otherNode = op2; - } + if (op2->IsCnsFltOrDbl()) + { + cnsNode = op2->AsDblCon(); + otherNode = op1; + } + else if (op1->IsCnsFltOrDbl()) + { + cnsNode = op1->AsDblCon(); + otherNode = op2; + } - if (cnsNode != nullptr) - { - if (otherNode->IsCnsFltOrDbl()) + if (cnsNode != nullptr) { - // both are constant, we can fold this operation completely. Pop both peeked values + if (otherNode->IsCnsFltOrDbl()) + { + // both are constant, we can fold this operation completely. Pop both peeked values - double x = cnsNode->DconValue(); - double y = otherNode->AsDblCon()->DconValue(); - double z; + double x = cnsNode->DconValue(); + double y = otherNode->AsDblCon()->DconValue(); + double z; - if (isMax) - { - if (isMagnitude) + if (isMax) { - if (isNumber) + if (isMagnitude) + { + if (isNumber) + { + z = FloatingPointUtils::maximumMagnitudeNumber(x, y); + } + else + { + z = FloatingPointUtils::maximumMagnitude(x, y); + } + } + else if (isNumber) { - z = FloatingPointUtils::maximumMagnitudeNumber(x, y); + z = FloatingPointUtils::maximumNumber(x, y); } else { - z = FloatingPointUtils::maximumMagnitude(x, y); + z = FloatingPointUtils::maximum(x, y); } } - else if (isNumber) - { - z = FloatingPointUtils::maximumNumber(x, y); - } else { - z = FloatingPointUtils::maximum(x, y); - } - } - else - { - if (isMagnitude) - { - if (isNumber) + if (isMagnitude) + { + if (isNumber) + { + z = FloatingPointUtils::minimumMagnitudeNumber(x, y); + } + else + { + z = FloatingPointUtils::minimumMagnitude(x, y); + } + } + else if (isNumber) { - z = FloatingPointUtils::minimumMagnitudeNumber(x, y); + z = FloatingPointUtils::minimumNumber(x, y); } else { - z = FloatingPointUtils::minimumMagnitude(x, y); + z = FloatingPointUtils::minimum(x, y); } } - else if (isNumber) - { - z = FloatingPointUtils::minimumNumber(x, y); - } - else - { - z = FloatingPointUtils::minimum(x, y); - } - } - cnsNode->SetDconValue(z); - - impPopStack(); - impPopStack(); - - DEBUG_DESTROY_NODE(otherNode); - return cnsNode; - } + cnsNode->SetDconValue(z); - // only one is constant, we can fold in specialized scenarios - - if (cnsNode->IsFloatNaN()) - { - impSpillSideEffects(false, CHECK_SPILL_ALL DEBUGARG("spill side effects before propagating NaN")); - - impPopStack(); - impPopStack(); + impPopStack(); + impPopStack(); - if (isNumber) - { - DEBUG_DESTROY_NODE(cnsNode); - return otherNode; - } - else - { DEBUG_DESTROY_NODE(otherNode); return cnsNode; } - } -#if defined(FEATURE_HW_INTRINSICS) && defined(TARGET_XARCH) - if (!isMagnitude && compOpportunisticallyDependsOn(InstructionSet_SSE2)) - { - bool needsFixup = false; - bool canHandle = false; - bool isV512Supported = false; + // only one is constant, we can fold in specialized scenarios - if (isMax) + if (cnsNode->IsFloatNaN()) { - // maxsd, maxss return op2 if both inputs are 0 of either sign - // we require +0 to be greater than -0 we also require NaN to - // not be propagated for isNumber and to be propagated otherwise. - // - // This means for isNumber we want to do `max other, cns` and - // can only handle cns being -0 if Avx512F is supported. This is - // because if other was NaN, we want to return the non-NaN cns. - // But if cns was -0 and other was +0 we'd want to return +0 and - // so need to be able to fixup the result. - // - // For !isNumber we have the inverse and want `max cns, other` and - // can only handle cns being +0 if Avx512F is supported. This is - // because if other was NaN, we want to return other and if cns - // was +0 and other was -0 we'd want to return +0 and so need - // so need to be able to fixup the result. + impSpillSideEffects(false, CHECK_SPILL_ALL DEBUGARG("spill side effects before propagating NaN")); + + impPopStack(); + impPopStack(); if (isNumber) { - needsFixup = cnsNode->IsFloatNegativeZero(); + DEBUG_DESTROY_NODE(cnsNode); + return otherNode; } else { - needsFixup = cnsNode->IsFloatPositiveZero(); + DEBUG_DESTROY_NODE(otherNode); + return cnsNode; } + } + +#if defined(FEATURE_HW_INTRINSICS) && defined(TARGET_XARCH) + if (!isMagnitude && compOpportunisticallyDependsOn(InstructionSet_SSE2)) + { + bool needsFixup = false; + bool canHandle = false; + bool isV512Supported = false; - if (!needsFixup || compIsEvexOpportunisticallySupported(isV512Supported)) + if (isMax) { - // Given the checks, op1 can safely be the cns and op2 the other node + // maxsd, maxss return op2 if both inputs are 0 of either sign + // we require +0 to be greater than -0 we also require NaN to + // not be propagated for isNumber and to be propagated otherwise. + // + // This means for isNumber we want to do `max other, cns` and + // can only handle cns being -0 if Avx512F is supported. This is + // because if other was NaN, we want to return the non-NaN cns. + // But if cns was -0 and other was +0 we'd want to return +0 and + // so need to be able to fixup the result. + // + // For !isNumber we have the inverse and want `max cns, other` and + // can only handle cns being +0 if Avx512F is supported. This is + // because if other was NaN, we want to return other and if cns + // was +0 and other was -0 we'd want to return +0 and so need + // so need to be able to fixup the result. - intrinsicName = (callType == TYP_DOUBLE) ? NI_SSE2_MaxScalar : NI_SSE_MaxScalar; + if (isNumber) + { + needsFixup = cnsNode->IsFloatNegativeZero(); + } + else + { + needsFixup = cnsNode->IsFloatPositiveZero(); + } - // one is constant and we know its something we can handle, so pop both peeked values + if (!needsFixup || compIsEvexOpportunisticallySupported(isV512Supported)) + { + // Given the checks, op1 can safely be the cns and op2 the other node - op1 = cnsNode; - op2 = otherNode; + intrinsicName = (callType == TYP_DOUBLE) ? NI_SSE2_MaxScalar : NI_SSE_MaxScalar; - canHandle = true; - } - } - else - { - // minsd, minss return op2 if both inputs are 0 of either sign - // we require -0 to be lesser than +0, we also require NaN to - // not be propagated for isNumber and to be propagated otherwise. - // - // This means for isNumber we want to do `min other, cns` and - // can only handle cns being +0 if Avx512F is supported. This is - // because if other was NaN, we want to return the non-NaN cns. - // But if cns was +0 and other was -0 we'd want to return -0 and - // so need to be able to fixup the result. - // - // For !isNumber we have the inverse and want `min cns, other` and - // can only handle cns being -0 if Avx512F is supported. This is - // because if other was NaN, we want to return other and if cns - // was -0 and other was +0 we'd want to return -0 and so need - // so need to be able to fixup the result. + // one is constant and we know its something we can handle, so pop both peeked values - if (isNumber) - { - needsFixup = cnsNode->IsFloatPositiveZero(); + op1 = cnsNode; + op2 = otherNode; + + canHandle = true; + } } else { - needsFixup = cnsNode->IsFloatNegativeZero(); - } - - if (!needsFixup || compIsEvexOpportunisticallySupported(isV512Supported)) - { - // Given the checks, op1 can safely be the cns and op2 the other node - - intrinsicName = (callType == TYP_DOUBLE) ? NI_SSE2_MinScalar : NI_SSE_MinScalar; - - // one is constant and we know its something we can handle, so pop both peeked values + // minsd, minss return op2 if both inputs are 0 of either sign + // we require -0 to be lesser than +0, we also require NaN to + // not be propagated for isNumber and to be propagated otherwise. + // + // This means for isNumber we want to do `min other, cns` and + // can only handle cns being +0 if Avx512F is supported. This is + // because if other was NaN, we want to return the non-NaN cns. + // But if cns was +0 and other was -0 we'd want to return -0 and + // so need to be able to fixup the result. + // + // For !isNumber we have the inverse and want `min cns, other` and + // can only handle cns being -0 if Avx512F is supported. This is + // because if other was NaN, we want to return other and if cns + // was -0 and other was +0 we'd want to return -0 and so need + // so need to be able to fixup the result. - op1 = cnsNode; - op2 = otherNode; + if (isNumber) + { + needsFixup = cnsNode->IsFloatPositiveZero(); + } + else + { + needsFixup = cnsNode->IsFloatNegativeZero(); + } - canHandle = true; - } - } + if (!needsFixup || compIsEvexOpportunisticallySupported(isV512Supported)) + { + // Given the checks, op1 can safely be the cns and op2 the other node - if (canHandle) - { - assert(op1->IsCnsFltOrDbl() && !op2->IsCnsFltOrDbl()); + intrinsicName = (callType == TYP_DOUBLE) ? NI_SSE2_MinScalar : NI_SSE_MinScalar; - impPopStack(); - impPopStack(); + // one is constant and we know its something we can handle, so pop both peeked values - GenTreeVecCon* vecCon = gtNewVconNode(TYP_SIMD16); + op1 = cnsNode; + op2 = otherNode; - if (callJitType == CORINFO_TYPE_FLOAT) - { - vecCon->gtSimdVal.f32[0] = static_cast(op1->AsDblCon()->DconValue()); - } - else - { - vecCon->gtSimdVal.f64[0] = op1->AsDblCon()->DconValue(); + canHandle = true; + } } - op1 = vecCon; - op2 = gtNewSimdCreateScalarUnsafeNode(TYP_SIMD16, op2, callJitType, 16); - - GenTree* retNode = gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1, op2, intrinsicName, callJitType, 16); - - if (needsFixup) + if (canHandle) { - GenTree* op2Clone; - op2 = impCloneExpr(op2, &op2Clone, CHECK_SPILL_ALL, - nullptr DEBUGARG("Cloning non-constant for Math.Max/Min")); + assert(op1->IsCnsFltOrDbl() && !op2->IsCnsFltOrDbl()); - retNode->AsHWIntrinsic()->Op(2) = op2; - - GenTreeVecCon* tbl = gtNewVconNode(TYP_SIMD16); + impPopStack(); + impPopStack(); - // FixupScalar(left, right, table, control) computes the input type of right - // adjusts it based on the table and then returns - // - // In our case, left is going to be the result of the RangeScalar operation - // and right is going to be op1 or op2. In the case op1/op2 is QNaN or SNaN - // we want to preserve it instead. Otherwise we want to preserve the original - // result computed by RangeScalar. - // - // If both inputs are NaN, then we'll end up taking op1 by virtue of it being - // the latter fixup. + GenTreeVecCon* vecCon = gtNewVconNode(TYP_SIMD16); - if (isMax) + if (callJitType == CORINFO_TYPE_FLOAT) { - // QNAN: 0b0000: Preserve left - // SNAN: 0b0000 - // ZERO: 0b1000: +0 - // +ONE: 0b0000 - // -INF: 0b0000 - // +INF: 0b0000 - // -VAL: 0b0000 - // +VAL: 0b0000 - tbl->gtSimdVal.i32[0] = 0x0800; + vecCon->gtSimdVal.f32[0] = static_cast(op1->AsDblCon()->DconValue()); } else { - // QNAN: 0b0000: Preserve left - // SNAN: 0b0000 - // ZERO: 0b0111: -0 - // +ONE: 0b0000 - // -INF: 0b0000 - // +INF: 0b0000 - // -VAL: 0b0000 - // +VAL: 0b0000 - tbl->gtSimdVal.i32[0] = 0x0700; + vecCon->gtSimdVal.f64[0] = op1->AsDblCon()->DconValue(); } - NamedIntrinsic fixupScalarId = isV512Supported ? NI_AVX512F_FixupScalar : NI_AVX10v1_FixupScalar; + op1 = vecCon; + op2 = gtNewSimdCreateScalarUnsafeNode(TYP_SIMD16, op2, callJitType, 16); - retNode = gtNewSimdHWIntrinsicNode(TYP_SIMD16, retNode, op2Clone, tbl, gtNewIconNode(0), - fixupScalarId, callJitType, 16); - } + GenTree* retNode = gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1, op2, intrinsicName, callJitType, 16); - if (isNumber) - { - // Swap the operands so that the cnsNode is op1, this prevents - // the unknown value (which could be NaN) from being selected. + if (needsFixup) + { + GenTree* op2Clone; + op2 = impCloneExpr(op2, &op2Clone, CHECK_SPILL_ALL, + nullptr DEBUGARG("Cloning non-constant for Math.Max/Min")); - retNode->AsHWIntrinsic()->Op(1) = op2; - retNode->AsHWIntrinsic()->Op(2) = op1; - } + retNode->AsHWIntrinsic()->Op(2) = op2; + + GenTreeVecCon* tbl = gtNewVconNode(TYP_SIMD16); + + // FixupScalar(left, right, table, control) computes the input type of right + // adjusts it based on the table and then returns + // + // In our case, left is going to be the result of the RangeScalar operation + // and right is going to be op1 or op2. In the case op1/op2 is QNaN or SNaN + // we want to preserve it instead. Otherwise we want to preserve the original + // result computed by RangeScalar. + // + // If both inputs are NaN, then we'll end up taking op1 by virtue of it being + // the latter fixup. + + if (isMax) + { + // QNAN: 0b0000: Preserve left + // SNAN: 0b0000 + // ZERO: 0b1000: +0 + // +ONE: 0b0000 + // -INF: 0b0000 + // +INF: 0b0000 + // -VAL: 0b0000 + // +VAL: 0b0000 + tbl->gtSimdVal.i32[0] = 0x0800; + } + else + { + // QNAN: 0b0000: Preserve left + // SNAN: 0b0000 + // ZERO: 0b0111: -0 + // +ONE: 0b0000 + // -INF: 0b0000 + // +INF: 0b0000 + // -VAL: 0b0000 + // +VAL: 0b0000 + tbl->gtSimdVal.i32[0] = 0x0700; + } + + NamedIntrinsic fixupScalarId = + isV512Supported ? NI_AVX512F_FixupScalar : NI_AVX10v1_FixupScalar; + + retNode = gtNewSimdHWIntrinsicNode(TYP_SIMD16, retNode, op2Clone, tbl, gtNewIconNode(0), + fixupScalarId, callJitType, 16); + } - return gtNewSimdToScalarNode(genActualType(callType), retNode, callJitType, 16); + if (isNumber) + { + // Swap the operands so that the cnsNode is op1, this prevents + // the unknown value (which could be NaN) from being selected. + + retNode->AsHWIntrinsic()->Op(1) = op2; + retNode->AsHWIntrinsic()->Op(2) = op1; + } + + return gtNewSimdToScalarNode(genActualType(callType), retNode, callJitType, 16); + } } - } #endif // FEATURE_HW_INTRINSICS && TARGET_XARCH - } + } #if defined(FEATURE_HW_INTRINSICS) && defined(TARGET_XARCH) - bool isV512Supported = false; - if (compIsEvexOpportunisticallySupported(isV512Supported, InstructionSet_AVX512DQ)) - { - // We are constructing a chain of intrinsics similar to: - // var op1 = Vector128.CreateScalarUnsafe(x); - // var op2 = Vector128.CreateScalarUnsafe(y); - // - // var tmp = Avx512DQ.RangeScalar(op1, op2, imm8); - // var tbl = Vector128.CreateScalarUnsafe(0x00); - // - // tmp = Avx512F.FixupScalar(tmp, op2, tbl, 0x00); - // tmp = Avx512F.FixupScalar(tmp, op1, tbl, 0x00); - // - // return tmp.ToScalar(); + bool isV512Supported = false; + if (compIsEvexOpportunisticallySupported(isV512Supported, InstructionSet_AVX512DQ)) + { + // We are constructing a chain of intrinsics similar to: + // var op1 = Vector128.CreateScalarUnsafe(x); + // var op2 = Vector128.CreateScalarUnsafe(y); + // + // var tmp = Avx512DQ.RangeScalar(op1, op2, imm8); + // var tbl = Vector128.CreateScalarUnsafe(0x00); + // + // tmp = Avx512F.FixupScalar(tmp, op2, tbl, 0x00); + // tmp = Avx512F.FixupScalar(tmp, op1, tbl, 0x00); + // + // return tmp.ToScalar(); - // RangeScalar operates by default almost as MaxNumber or MinNumber - // but, it propagates sNaN and does not propagate qNaN. So we need - // an additional fixup to ensure we propagate qNaN as well. + // RangeScalar operates by default almost as MaxNumber or MinNumber + // but, it propagates sNaN and does not propagate qNaN. So we need + // an additional fixup to ensure we propagate qNaN as well. - uint8_t imm8; + uint8_t imm8; - if (isMax) - { - if (isMagnitude) + if (isMax) { - // 0b01_11: Sign(CompareResult), Max-Abs Value - imm8 = 0x07; + if (isMagnitude) + { + // 0b01_11: Sign(CompareResult), Max-Abs Value + imm8 = 0x07; + } + else + { + // 0b01_01: Sign(CompareResult), Max Value + imm8 = 0x05; + } + } + else if (isMagnitude) + { + // 0b01_10: Sign(CompareResult), Min-Abs Value + imm8 = 0x06; } else { - // 0b01_01: Sign(CompareResult), Max Value - imm8 = 0x05; + // 0b01_00: Sign(CompareResult), Min Value + imm8 = 0x04; } - } - else if (isMagnitude) - { - // 0b01_10: Sign(CompareResult), Min-Abs Value - imm8 = 0x06; - } - else - { - // 0b01_00: Sign(CompareResult), Min Value - imm8 = 0x04; - } - - GenTree* op3 = gtNewIconNode(imm8); - GenTree* op2 = gtNewSimdCreateScalarUnsafeNode(TYP_SIMD16, impPopStack().val, callJitType, 16); - GenTree* op1 = gtNewSimdCreateScalarUnsafeNode(TYP_SIMD16, impPopStack().val, callJitType, 16); - GenTree* op2Clone; - op2 = impCloneExpr(op2, &op2Clone, CHECK_SPILL_ALL, nullptr DEBUGARG("Cloning op2 for Math.Max/Min")); + GenTree* op3 = gtNewIconNode(imm8); + GenTree* op2 = gtNewSimdCreateScalarUnsafeNode(TYP_SIMD16, impPopStack().val, callJitType, 16); + GenTree* op1 = gtNewSimdCreateScalarUnsafeNode(TYP_SIMD16, impPopStack().val, callJitType, 16); - GenTree* op1Clone; - op1 = impCloneExpr(op1, &op1Clone, CHECK_SPILL_ALL, nullptr DEBUGARG("Cloning op1 for Math.Max/Min")); + GenTree* op2Clone; + op2 = impCloneExpr(op2, &op2Clone, CHECK_SPILL_ALL, nullptr DEBUGARG("Cloning op2 for Math.Max/Min")); - GenTree* tmp = - !isV512Supported - ? gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1, op2, op3, NI_AVX10v1_RangeScalar, callJitType, 16) - : gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1, op2, op3, NI_AVX512DQ_RangeScalar, callJitType, 16); + GenTree* op1Clone; + op1 = impCloneExpr(op1, &op1Clone, CHECK_SPILL_ALL, nullptr DEBUGARG("Cloning op1 for Math.Max/Min")); - // FixupScalar(left, right, table, control) computes the input type of right - // adjusts it based on the table and then returns - // - // In our case, left is going to be the result of the RangeScalar operation, - // which is either sNaN or a normal value, and right is going to be op1 or op2. + GenTree* tmp = + !isV512Supported + ? gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1, op2, op3, NI_AVX10v1_RangeScalar, callJitType, 16) + : gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1, op2, op3, NI_AVX512DQ_RangeScalar, callJitType, 16); - GenTree* tbl1 = gtNewVconNode(TYP_SIMD16); - GenTree* tbl2; + // FixupScalar(left, right, table, control) computes the input type of right + // adjusts it based on the table and then returns + // + // In our case, left is going to be the result of the RangeScalar operation, + // which is either sNaN or a normal value, and right is going to be op1 or op2. - // We currently have (commutative) - // * snan, snan = snan - // * snan, qnan = snan - // * snan, norm = snan - // * qnan, qnan = qnan - // * qnan, norm = norm - // * norm, norm = norm + GenTree* tbl1 = gtNewVconNode(TYP_SIMD16); + GenTree* tbl2; - NamedIntrinsic fixupHwIntrinsicID = !isV512Supported ? NI_AVX10v1_FixupScalar : NI_AVX512F_FixupScalar; - if (isNumber) - { - // We need to fixup the case of: + // We currently have (commutative) + // * snan, snan = snan + // * snan, qnan = snan // * snan, norm = snan - // - // Instead, it should be: - // * snan, norm = norm + // * qnan, qnan = qnan + // * qnan, norm = norm + // * norm, norm = norm - // First look at op1 and op2 using op2 as the classification - // - // If op2 is norm, we take op2 (norm) - // If op2 is nan, we take op1 ( nan or norm) - // - // Thus, if one input was norm the fixup is now norm - - // QNAN: 0b0000: Preserve left - // SNAN: 0b0000 - // ZERO: 0b0001: Preserve right - // +ONE: 0b0001 - // -INF: 0b0001 - // +INF: 0b0001 - // -VAL: 0b0001 - // +VAL: 0b0001 - tbl1->AsVecCon()->gtSimdVal.i32[0] = 0x11111100; - - // Next look at result and fixup using result as the classification - // - // If result is norm, we take the result (norm) - // If result is nan, we take the fixup ( nan or norm) - // - // Thus if either input was snan, we now have norm as expected - // Otherwise, the result was already correct + NamedIntrinsic fixupHwIntrinsicID = !isV512Supported ? NI_AVX10v1_FixupScalar : NI_AVX512F_FixupScalar; + if (isNumber) + { + // We need to fixup the case of: + // * snan, norm = snan + // + // Instead, it should be: + // * snan, norm = norm - tbl1 = impCloneExpr(tbl1, &tbl2, CHECK_SPILL_ALL, nullptr DEBUGARG("Cloning tbl for Math.Max/Min")); + // First look at op1 and op2 using op2 as the classification + // + // If op2 is norm, we take op2 (norm) + // If op2 is nan, we take op1 ( nan or norm) + // + // Thus, if one input was norm the fixup is now norm + + // QNAN: 0b0000: Preserve left + // SNAN: 0b0000 + // ZERO: 0b0001: Preserve right + // +ONE: 0b0001 + // -INF: 0b0001 + // +INF: 0b0001 + // -VAL: 0b0001 + // +VAL: 0b0001 + tbl1->AsVecCon()->gtSimdVal.i32[0] = 0x11111100; + + // Next look at result and fixup using result as the classification + // + // If result is norm, we take the result (norm) + // If result is nan, we take the fixup ( nan or norm) + // + // Thus if either input was snan, we now have norm as expected + // Otherwise, the result was already correct - op1Clone = gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1Clone, op2Clone, tbl1, gtNewIconNode(0), - fixupHwIntrinsicID, callJitType, 16); + tbl1 = impCloneExpr(tbl1, &tbl2, CHECK_SPILL_ALL, nullptr DEBUGARG("Cloning tbl for Math.Max/Min")); - tmp = gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1Clone, tmp, tbl2, gtNewIconNode(0), fixupHwIntrinsicID, - callJitType, 16); - } - else - { - // We need to fixup the case of: - // * qnan, norm = norm - // - // Instead, it should be: - // * qnan, norm = qnan + op1Clone = gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1Clone, op2Clone, tbl1, gtNewIconNode(0), + fixupHwIntrinsicID, callJitType, 16); - // First look at op1 and op2 using op2 as the classification - // - // If op2 is norm, we take op1 ( nan or norm) - // If op2 is snan, we take op1 ( nan or norm) - // If op2 is qnan, we take op2 (qnan) - // - // Thus, if either input was qnan the fixup is now qnan - - // QNAN: 0b0001: Preserve right - // SNAN: 0b0000: Preserve left - // ZERO: 0b0000 - // +ONE: 0b0000 - // -INF: 0b0000 - // +INF: 0b0000 - // -VAL: 0b0000 - // +VAL: 0b0000 - tbl1->AsVecCon()->gtSimdVal.i32[0] = 0x00000001; - - // Next look at result and fixup using fixup as the classification - // - // If fixup is norm, we take the result (norm) - // If fixup is sNaN, we take the result (sNaN) - // If fixup is qNaN, we take the fixup (qNaN) - // - // Thus if the fixup was qnan, we now have qnan as expected - // Otherwise, the result was already correct + tmp = gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1Clone, tmp, tbl2, gtNewIconNode(0), fixupHwIntrinsicID, + callJitType, 16); + } + else + { + // We need to fixup the case of: + // * qnan, norm = norm + // + // Instead, it should be: + // * qnan, norm = qnan - tbl1 = impCloneExpr(tbl1, &tbl2, CHECK_SPILL_ALL, nullptr DEBUGARG("Cloning tbl for Math.Max/Min")); + // First look at op1 and op2 using op2 as the classification + // + // If op2 is norm, we take op1 ( nan or norm) + // If op2 is snan, we take op1 ( nan or norm) + // If op2 is qnan, we take op2 (qnan) + // + // Thus, if either input was qnan the fixup is now qnan + + // QNAN: 0b0001: Preserve right + // SNAN: 0b0000: Preserve left + // ZERO: 0b0000 + // +ONE: 0b0000 + // -INF: 0b0000 + // +INF: 0b0000 + // -VAL: 0b0000 + // +VAL: 0b0000 + tbl1->AsVecCon()->gtSimdVal.i32[0] = 0x00000001; + + // Next look at result and fixup using fixup as the classification + // + // If fixup is norm, we take the result (norm) + // If fixup is sNaN, we take the result (sNaN) + // If fixup is qNaN, we take the fixup (qNaN) + // + // Thus if the fixup was qnan, we now have qnan as expected + // Otherwise, the result was already correct - op1Clone = gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1Clone, op2Clone, tbl1, gtNewIconNode(0), - fixupHwIntrinsicID, callJitType, 16); + tbl1 = impCloneExpr(tbl1, &tbl2, CHECK_SPILL_ALL, nullptr DEBUGARG("Cloning tbl for Math.Max/Min")); - tmp = gtNewSimdHWIntrinsicNode(TYP_SIMD16, tmp, op1Clone, tbl2, gtNewIconNode(0), fixupHwIntrinsicID, - callJitType, 16); - } + op1Clone = gtNewSimdHWIntrinsicNode(TYP_SIMD16, op1Clone, op2Clone, tbl1, gtNewIconNode(0), + fixupHwIntrinsicID, callJitType, 16); - return gtNewSimdToScalarNode(genActualType(callType), tmp, callJitType, 16); - } + tmp = gtNewSimdHWIntrinsicNode(TYP_SIMD16, tmp, op1Clone, tbl2, gtNewIconNode(0), fixupHwIntrinsicID, + callJitType, 16); + } + + return gtNewSimdToScalarNode(genActualType(callType), tmp, callJitType, 16); + } #endif // FEATURE_HW_INTRINSICS && TARGET_XARCH #ifdef TARGET_RISCV64 - GenTree *op1Clone = nullptr, *op2Clone = nullptr; + GenTree *op1Clone = nullptr, *op2Clone = nullptr; - op2 = impPopStack().val; - if (!isNumber) - { - op2 = impCloneExpr(op2, &op2Clone, CHECK_SPILL_ALL, nullptr DEBUGARG("Clone op2 for Math.Min/Max non-Number")); - } + op2 = impPopStack().val; + if (!isNumber) + { + op2 = impCloneExpr(op2, &op2Clone, CHECK_SPILL_ALL, + nullptr DEBUGARG("Clone op2 for Math.Min/Max non-Number")); + } - op1 = impPopStack().val; - if (!isNumber) - { - op1 = impCloneExpr(op1, &op1Clone, CHECK_SPILL_ALL, nullptr DEBUGARG("Clone op1 for Math.Min/Max non-Number")); - } + op1 = impPopStack().val; + if (!isNumber) + { + op1 = impCloneExpr(op1, &op1Clone, CHECK_SPILL_ALL, + nullptr DEBUGARG("Clone op1 for Math.Min/Max non-Number")); + } - static const CORINFO_CONST_LOOKUP nullEntry = {IAT_VALUE}; - if (isMagnitude) - { - op1 = new (this, GT_INTRINSIC) GenTreeIntrinsic(callType, op1, NI_System_Math_Abs, nullptr R2RARG(nullEntry)); - op2 = new (this, GT_INTRINSIC) GenTreeIntrinsic(callType, op2, NI_System_Math_Abs, nullptr R2RARG(nullEntry)); - } - NamedIntrinsic name = isMax ? NI_System_Math_MaxNumber : NI_System_Math_MinNumber; - GenTree* minMax = new (this, GT_INTRINSIC) GenTreeIntrinsic(callType, op1, op2, name, nullptr R2RARG(nullEntry)); + static const CORINFO_CONST_LOOKUP nullEntry = {IAT_VALUE}; + if (isMagnitude) + { + op1 = + new (this, GT_INTRINSIC) GenTreeIntrinsic(callType, op1, NI_System_Math_Abs, nullptr R2RARG(nullEntry)); + op2 = + new (this, GT_INTRINSIC) GenTreeIntrinsic(callType, op2, NI_System_Math_Abs, nullptr R2RARG(nullEntry)); + } + NamedIntrinsic name = isMax ? NI_System_Math_MaxNumber : NI_System_Math_MinNumber; + GenTree* minMax = + new (this, GT_INTRINSIC) GenTreeIntrinsic(callType, op1, op2, name, nullptr R2RARG(nullEntry)); - if (!isNumber) - { - GenTreeOp* isOp1Number = gtNewOperNode(GT_EQ, TYP_INT, op1Clone, gtCloneExpr(op1Clone)); - GenTreeOp* isOp2Number = gtNewOperNode(GT_EQ, TYP_INT, op2Clone, gtCloneExpr(op2Clone)); - GenTreeOp* isOkForMinMax = gtNewOperNode(GT_EQ, TYP_INT, isOp1Number, isOp2Number); + if (!isNumber) + { + GenTreeOp* isOp1Number = gtNewOperNode(GT_EQ, TYP_INT, op1Clone, gtCloneExpr(op1Clone)); + GenTreeOp* isOp2Number = gtNewOperNode(GT_EQ, TYP_INT, op2Clone, gtCloneExpr(op2Clone)); + GenTreeOp* isOkForMinMax = gtNewOperNode(GT_EQ, TYP_INT, isOp1Number, isOp2Number); - GenTreeOp* nanPropagator = gtNewOperNode(GT_ADD, callType, gtCloneExpr(op1Clone), gtCloneExpr(op2Clone)); + GenTreeOp* nanPropagator = gtNewOperNode(GT_ADD, callType, gtCloneExpr(op1Clone), gtCloneExpr(op2Clone)); - GenTreeQmark* qmark = gtNewQmarkNode(callType, isOkForMinMax, gtNewColonNode(callType, minMax, nanPropagator)); - // QMARK has to be a root node - unsigned tmp = lvaGrabTemp(true DEBUGARG("Temp for Qmark in Math.Min/Max non-Number")); - impStoreToTemp(tmp, qmark, CHECK_SPILL_NONE); - minMax = gtNewLclvNode(tmp, callType); + GenTreeQmark* qmark = + gtNewQmarkNode(callType, isOkForMinMax, gtNewColonNode(callType, minMax, nanPropagator)); + // QMARK has to be a root node + unsigned tmp = lvaGrabTemp(true DEBUGARG("Temp for Qmark in Math.Min/Max non-Number")); + impStoreToTemp(tmp, qmark, CHECK_SPILL_NONE); + minMax = gtNewLclvNode(tmp, callType); + } + return minMax; +#endif // TARGET_RISCV64 } + else + { + assert(varTypeIsIntegral(callType)); + assert(!isNumber && !isMagnitude); +#ifdef TARGET_RISCV64 + if (compOpportunisticallyDependsOn(InstructionSet_Zbb)) + { + GenTree* op2 = impPopStack().val; + GenTree* op1 = impPopStack().val; - return minMax; + // RISC-V integer min/max instructions operate on whole registers with preferrably ABI-extended values. + // We currently don't know if a register is ABI-extended so always cast, even for 'int' and 'uint'. + var_types preciseType = JitType2PreciseVarType(callJitType); + if (genTypeSize(preciseType) < REGSIZE_BYTES) + { + // Zero-extended 'uint' is unnatural on RISC-V + bool zeroExtend = varTypeIsUnsigned(preciseType) && (preciseType != TYP_UINT); + + op2 = gtNewCastNode(TYP_I_IMPL, op2, zeroExtend, TYP_I_IMPL); + op1 = gtNewCastNode(TYP_I_IMPL, op1, zeroExtend, TYP_I_IMPL); + } + if (varTypeIsUnsigned(preciseType)) + intrinsicName = isMax ? NI_System_Math_MaxUnsigned : NI_System_Math_MinUnsigned; + + GenTreeIntrinsic* minMax = new (this, GT_INTRINSIC) + GenTreeIntrinsic(TYP_I_IMPL, op1, op2, intrinsicName, nullptr R2RARG(CORINFO_CONST_LOOKUP{IAT_VALUE})); + + return minMax; + } #endif // TARGET_RISCV64 + } // TODO-CQ: Returning this as an intrinsic blocks inlining and is undesirable // return impMathIntrinsic(method, sig, callType, intrinsicName, tailCall, isSpecial); diff --git a/src/coreclr/jit/instrsriscv64.h b/src/coreclr/jit/instrsriscv64.h index db2a78a25e58e7..1f72938548b451 100644 --- a/src/coreclr/jit/instrsriscv64.h +++ b/src/coreclr/jit/instrsriscv64.h @@ -262,6 +262,7 @@ INST(amomaxu_w, "amomaxu.w", 0, 0xe000202f) // funct5:11100 INST(amomaxu_d, "amomaxu.d", 0, 0xe000302f) // funct5:11100 // Zbb (RV32 + RV64) +//// R_R INST(clz, "clz", 0, 0x60001013) INST(clzw, "clzw", 0, 0x6000101b) INST(ctz, "ctz", 0, 0x60101013) @@ -269,6 +270,12 @@ INST(ctzw, "ctzw", 0, 0x6010101b) INST(cpop, "cpop", 0, 0x60201013) INST(cpopw, "cpopw", 0, 0x6020101b) +//// R_R_R +INST(min, "min", 0, 0x0a004033) +INST(minu, "minu", 0, 0x0a005033) +INST(max, "max", 0, 0x0a006033) +INST(maxu, "maxu", 0, 0x0a007033) + // clang-format on /*****************************************************************************/ #undef INST diff --git a/src/coreclr/jit/lsrariscv64.cpp b/src/coreclr/jit/lsrariscv64.cpp index 5428e6b5c8860c..c307148b17769b 100644 --- a/src/coreclr/jit/lsrariscv64.cpp +++ b/src/coreclr/jit/lsrariscv64.cpp @@ -363,6 +363,18 @@ int LinearScan::BuildNode(GenTree* tree) assert(varTypeIsFloating(tree)); break; + // Integer Min/Max + case NI_System_Math_Min: + case NI_System_Math_Max: + case NI_System_Math_MinUnsigned: + case NI_System_Math_MaxUnsigned: + assert(compiler->compOpportunisticallyDependsOn(InstructionSet_Zbb)); + assert(op2 != nullptr); + assert(op2->TypeIs(tree->TypeGet())); + assert(op1->TypeIs(tree->TypeGet())); + assert(tree->TypeIs(TYP_I_IMPL)); + break; + // Operand and its result must be integers case NI_PRIMITIVE_LeadingZeroCount: case NI_PRIMITIVE_TrailingZeroCount: diff --git a/src/coreclr/jit/namedintrinsiclist.h b/src/coreclr/jit/namedintrinsiclist.h index ddee451a200bea..69b6c75c4a963d 100644 --- a/src/coreclr/jit/namedintrinsiclist.h +++ b/src/coreclr/jit/namedintrinsiclist.h @@ -48,10 +48,12 @@ enum NamedIntrinsic : unsigned short NI_System_Math_MaxMagnitude, NI_System_Math_MaxMagnitudeNumber, NI_System_Math_MaxNumber, + NI_System_Math_MaxUnsigned, NI_System_Math_Min, NI_System_Math_MinMagnitude, NI_System_Math_MinMagnitudeNumber, NI_System_Math_MinNumber, + NI_System_Math_MinUnsigned, NI_System_Math_MultiplyAddEstimate, NI_System_Math_Pow, NI_System_Math_ReciprocalEstimate, diff --git a/src/coreclr/jit/valuenum.cpp b/src/coreclr/jit/valuenum.cpp index b474ace3df6b7e..7d836353baea18 100644 --- a/src/coreclr/jit/valuenum.cpp +++ b/src/coreclr/jit/valuenum.cpp @@ -9585,7 +9585,7 @@ ValueNum ValueNumStore::EvalMathFuncUnary(var_types typ, NamedIntrinsic gtMathFN ValueNum ValueNumStore::EvalMathFuncBinary(var_types typ, NamedIntrinsic gtMathFN, ValueNum arg0VN, ValueNum arg1VN) { - assert(varTypeIsFloating(typ)); + assert(varTypeIsArithmetic(typ)); assert(arg0VN == VNNormalValue(arg0VN)); assert(arg1VN == VNNormalValue(arg1VN)); assert(m_pComp->IsMathIntrinsic(gtMathFN)); @@ -9691,10 +9691,9 @@ ValueNum ValueNumStore::EvalMathFuncBinary(var_types typ, NamedIntrinsic gtMathF return VNForDoubleCon(res); } - else + else if (typ == TYP_FLOAT) { // Both operand and its result must be of the same floating point type. - assert(typ == TYP_FLOAT); assert(typ == TypeOfVN(arg0VN)); float arg0Val = GetConstantSingle(arg0VN); @@ -9788,6 +9787,45 @@ ValueNum ValueNumStore::EvalMathFuncBinary(var_types typ, NamedIntrinsic gtMathF return VNForFloatCon(res); } + else + { +#ifdef TARGET_RISCV64 + // Both operands and its result must be of the same integer type. + assert(typ == TypeOfVN(arg0VN)); + assert(typ == TypeOfVN(arg1VN)); + // Note: GetConstantInt64 sign-extends 'uint' but for comparison purposes that's ok + INT64 arg0Val = GetConstantInt64(arg0VN); + INT64 arg1Val = GetConstantInt64(arg1VN); + INT64 result = 0; + + switch (gtMathFN) + { + case NI_System_Math_Min: + result = std::min(arg0Val, arg1Val); + break; + + case NI_System_Math_MinUnsigned: + result = std::min(arg0Val, arg1Val); + break; + + case NI_System_Math_Max: + result = std::max(arg0Val, arg1Val); + break; + + case NI_System_Math_MaxUnsigned: + result = std::max(arg0Val, arg1Val); + break; + + default: + // the above are the only binary math intrinsics at the time of this writing. + unreached(); + } + return (typ == TYP_LONG) || (typ == TYP_ULONG) ? VNForLongCon(result) + : VNForIntCon(static_cast(result)); +#else // !TARGET_RISCV64 + unreached(); +#endif // !TARGET_RISCV64 + } } else { @@ -9831,6 +9869,16 @@ ValueNum ValueNumStore::EvalMathFuncBinary(var_types typ, NamedIntrinsic gtMathF vnf = VNF_MinNumber; break; +#ifdef TARGET_RISCV64 + case NI_System_Math_MaxUnsigned: + vnf = VNF_Max_UN; + break; + + case NI_System_Math_MinUnsigned: + vnf = VNF_Min_UN; + break; +#endif // TARGET_RISCV64 + case NI_System_Math_Pow: vnf = VNF_Pow; break; diff --git a/src/coreclr/jit/valuenumfuncs.h b/src/coreclr/jit/valuenumfuncs.h index 30cac5c244ef10..158f67b456ce3e 100644 --- a/src/coreclr/jit/valuenumfuncs.h +++ b/src/coreclr/jit/valuenumfuncs.h @@ -90,14 +90,14 @@ ValueNumFuncDef(ILogB, 1, false, false, false) ValueNumFuncDef(Log, 1, false, false, false) ValueNumFuncDef(Log2, 1, false, false, false) ValueNumFuncDef(Log10, 1, false, false, false) -ValueNumFuncDef(Max, 2, false, false, false) -ValueNumFuncDef(MaxMagnitude, 2, false, false, false) -ValueNumFuncDef(MaxMagnitudeNumber, 2, false, false, false) -ValueNumFuncDef(MaxNumber, 2, false, false, false) -ValueNumFuncDef(Min, 2, false, false, false) -ValueNumFuncDef(MinMagnitude, 2, false, false, false) -ValueNumFuncDef(MinMagnitudeNumber, 2, false, false, false) -ValueNumFuncDef(MinNumber, 2, false, false, false) +ValueNumFuncDef(Max, 2, true, false, false) +ValueNumFuncDef(MaxMagnitude, 2, true, false, false) +ValueNumFuncDef(MaxMagnitudeNumber, 2, true, false, false) +ValueNumFuncDef(MaxNumber, 2, true, false, false) +ValueNumFuncDef(Min, 2, true, false, false) +ValueNumFuncDef(MinMagnitude, 2, true, false, false) +ValueNumFuncDef(MinMagnitudeNumber, 2, true, false, false) +ValueNumFuncDef(MinNumber, 2, true, false, false) ValueNumFuncDef(Pow, 2, false, false, false) ValueNumFuncDef(RoundDouble, 1, false, false, false) ValueNumFuncDef(RoundInt32, 1, false, false, false) @@ -205,8 +205,8 @@ ValueNumFuncDef(HWI_##isa##_##name, ((argCount == -1) ? -1 : (argCount + 1)), (( //TODO-LOONGARCH64-CQ: add LoongArch64's Hardware Intrinsics Instructions if supported. #elif defined (TARGET_RISCV64) - //TODO-RISCV64-CQ: add RISCV64's Hardware Intrinsics Instructions if supported. - + ValueNumFuncDef(Min_UN, 2, true, false, false) // unsigned min/max intrinsics + ValueNumFuncDef(Max_UN, 2, true, false, false) #else #error Unsupported platform #endif diff --git a/src/libraries/System.Private.CoreLib/src/System/Math.cs b/src/libraries/System.Private.CoreLib/src/System/Math.cs index 0c21426a16c155..a099c849e458f0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Math.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Math.cs @@ -936,6 +936,7 @@ public static double Log(double a, double newBase) return Log(a) / Log(newBase); } + [Intrinsic] [NonVersionable] public static byte Max(byte val1, byte val2) { @@ -971,18 +972,21 @@ public static double Max(double val1, double val2) return double.IsNegative(val2) ? val1 : val2; } + [Intrinsic] [NonVersionable] public static short Max(short val1, short val2) { return (val1 >= val2) ? val1 : val2; } + [Intrinsic] [NonVersionable] public static int Max(int val1, int val2) { return (val1 >= val2) ? val1 : val2; } + [Intrinsic] [NonVersionable] public static long Max(long val1, long val2) { @@ -993,6 +997,7 @@ public static long Max(long val1, long val2) /// The first of two native signed integers to compare. /// The second of two native signed integers to compare. /// Parameter or , whichever is larger. + [Intrinsic] [NonVersionable] public static nint Max(nint val1, nint val2) { @@ -1000,6 +1005,7 @@ public static nint Max(nint val1, nint val2) } [CLSCompliant(false)] + [Intrinsic] [NonVersionable] public static sbyte Max(sbyte val1, sbyte val2) { @@ -1030,6 +1036,7 @@ public static float Max(float val1, float val2) } [CLSCompliant(false)] + [Intrinsic] [NonVersionable] public static ushort Max(ushort val1, ushort val2) { @@ -1037,6 +1044,7 @@ public static ushort Max(ushort val1, ushort val2) } [CLSCompliant(false)] + [Intrinsic] [NonVersionable] public static uint Max(uint val1, uint val2) { @@ -1044,6 +1052,7 @@ public static uint Max(uint val1, uint val2) } [CLSCompliant(false)] + [Intrinsic] [NonVersionable] public static ulong Max(ulong val1, ulong val2) { @@ -1055,6 +1064,7 @@ public static ulong Max(ulong val1, ulong val2) /// The second of two native unsigned integers to compare. /// Parameter or , whichever is larger. [CLSCompliant(false)] + [Intrinsic] [NonVersionable] public static nuint Max(nuint val1, nuint val2) { @@ -1086,6 +1096,7 @@ public static double MaxMagnitude(double x, double y) return y; } + [Intrinsic] [NonVersionable] public static byte Min(byte val1, byte val2) { @@ -1121,18 +1132,21 @@ public static double Min(double val1, double val2) return double.IsNegative(val1) ? val1 : val2; } + [Intrinsic] [NonVersionable] public static short Min(short val1, short val2) { return (val1 <= val2) ? val1 : val2; } + [Intrinsic] [NonVersionable] public static int Min(int val1, int val2) { return (val1 <= val2) ? val1 : val2; } + [Intrinsic] [NonVersionable] public static long Min(long val1, long val2) { @@ -1143,6 +1157,7 @@ public static long Min(long val1, long val2) /// The first of two native signed integers to compare. /// The second of two native signed integers to compare. /// Parameter or , whichever is smaller. + [Intrinsic] [NonVersionable] public static nint Min(nint val1, nint val2) { @@ -1150,6 +1165,7 @@ public static nint Min(nint val1, nint val2) } [CLSCompliant(false)] + [Intrinsic] [NonVersionable] public static sbyte Min(sbyte val1, sbyte val2) { @@ -1180,6 +1196,7 @@ public static float Min(float val1, float val2) } [CLSCompliant(false)] + [Intrinsic] [NonVersionable] public static ushort Min(ushort val1, ushort val2) { @@ -1187,6 +1204,7 @@ public static ushort Min(ushort val1, ushort val2) } [CLSCompliant(false)] + [Intrinsic] [NonVersionable] public static uint Min(uint val1, uint val2) { @@ -1194,6 +1212,7 @@ public static uint Min(uint val1, uint val2) } [CLSCompliant(false)] + [Intrinsic] [NonVersionable] public static ulong Min(ulong val1, ulong val2) { @@ -1205,6 +1224,7 @@ public static ulong Min(ulong val1, ulong val2) /// The second of two native unsigned integers to compare. /// Parameter or , whichever is smaller. [CLSCompliant(false)] + [Intrinsic] [NonVersionable] public static nuint Min(nuint val1, nuint val2) { diff --git a/src/tests/JIT/Intrinsics/MathMinMaxInteger.cs b/src/tests/JIT/Intrinsics/MathMinMaxInteger.cs new file mode 100644 index 00000000000000..c088985542fa7c --- /dev/null +++ b/src/tests/JIT/Intrinsics/MathMinMaxInteger.cs @@ -0,0 +1,300 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// + +using System; +using System.Runtime.CompilerServices; +using Xunit; + +public class MathMinMaxIntegerTest +{ + [Fact] + public static void TestLong() + { + [MethodImpl(MethodImplOptions.NoInlining)] + static long Min(long a, long b) => Math.Min(a, b); + + [MethodImpl(MethodImplOptions.NoInlining)] + static long Max(long a, long b) => Math.Max(a, b); + + const long big = long.MaxValue, small = long.MinValue; + Assert.Equal(small, Min(big, small)); + Assert.Equal(small, Min(small, big)); + Assert.Equal(small, Math.Min(big, small)); + Assert.Equal(small, Math.Min(small, big)); + Assert.Equal(big, Max(big, small)); + Assert.Equal(big, Max(small, big)); + Assert.Equal(big, Math.Max(big, small)); + Assert.Equal(big, Math.Max(small, big)); + } + + [Fact] + public static void TestUlong() + { + [MethodImpl(MethodImplOptions.NoInlining)] + static ulong Min(ulong a, ulong b) => Math.Min(a, b); + + [MethodImpl(MethodImplOptions.NoInlining)] + static ulong Max(ulong a, ulong b) => Math.Max(a, b); + + const ulong big = ulong.MaxValue, small = ulong.MinValue; + Assert.Equal(small, Min(big, small)); + Assert.Equal(small, Min(small, big)); + Assert.Equal(small, Math.Min(big, small)); + Assert.Equal(small, Math.Min(small, big)); + Assert.Equal(big, Max(big, small)); + Assert.Equal(big, Max(small, big)); + Assert.Equal(big, Math.Max(big, small)); + Assert.Equal(big, Math.Max(small, big)); + } + + [Fact] + public static void TestInt() + { + [MethodImpl(MethodImplOptions.NoInlining)] + static int Min(int a, int b) => Math.Min(a, b); + + [MethodImpl(MethodImplOptions.NoInlining)] + static int Max(int a, int b) => Math.Max(a, b); + + const int big = int.MaxValue, small = int.MinValue; + Assert.Equal(small, Min(big, small)); + Assert.Equal(small, Min(small, big)); + Assert.Equal(small, Math.Min(big, small)); + Assert.Equal(small, Math.Min(small, big)); + Assert.Equal(big, Max(big, small)); + Assert.Equal(big, Max(small, big)); + Assert.Equal(big, Math.Max(big, small)); + Assert.Equal(big, Math.Max(small, big)); + } + + [Fact] + public static void TestUint() + { + [MethodImpl(MethodImplOptions.NoInlining)] + static uint Min(uint a, uint b) => Math.Min(a, b); + + [MethodImpl(MethodImplOptions.NoInlining)] + static uint Max(uint a, uint b) => Math.Max(a, b); + + const uint big = uint.MaxValue, small = uint.MinValue; + Assert.Equal(small, Min(big, small)); + Assert.Equal(small, Min(small, big)); + Assert.Equal(small, Math.Min(big, small)); + Assert.Equal(small, Math.Min(small, big)); + Assert.Equal(big, Max(big, small)); + Assert.Equal(big, Max(small, big)); + Assert.Equal(big, Math.Max(big, small)); + Assert.Equal(big, Math.Max(small, big)); + } + + [Fact] + public static void TestShort() + { + [MethodImpl(MethodImplOptions.NoInlining)] + static short Min(short a, short b) => Math.Min(a, b); + + [MethodImpl(MethodImplOptions.NoInlining)] + static short Max(short a, short b) => Math.Max(a, b); + + const short big = short.MaxValue, small = short.MinValue; + Assert.Equal(small, Min(big, small)); + Assert.Equal(small, Min(small, big)); + Assert.Equal(small, Math.Min(big, small)); + Assert.Equal(small, Math.Min(small, big)); + Assert.Equal(big, Max(big, small)); + Assert.Equal(big, Max(small, big)); + Assert.Equal(big, Math.Max(big, small)); + Assert.Equal(big, Math.Max(small, big)); + } + + [Fact] + public static void TestUshort() + { + [MethodImpl(MethodImplOptions.NoInlining)] + static ushort Min(ushort a, ushort b) => Math.Min(a, b); + + [MethodImpl(MethodImplOptions.NoInlining)] + static ushort Max(ushort a, ushort b) => Math.Max(a, b); + + const ushort big = ushort.MaxValue, small = ushort.MinValue; + Assert.Equal(small, Min(big, small)); + Assert.Equal(small, Min(small, big)); + Assert.Equal(small, Math.Min(big, small)); + Assert.Equal(small, Math.Min(small, big)); + Assert.Equal(big, Max(big, small)); + Assert.Equal(big, Max(small, big)); + Assert.Equal(big, Math.Max(big, small)); + Assert.Equal(big, Math.Max(small, big)); + } + + [Fact] + public static void TestSbyte() + { + [MethodImpl(MethodImplOptions.NoInlining)] + static sbyte Min(sbyte a, sbyte b) => Math.Min(a, b); + + [MethodImpl(MethodImplOptions.NoInlining)] + static sbyte Max(sbyte a, sbyte b) => Math.Max(a, b); + + const sbyte big = sbyte.MaxValue, small = sbyte.MinValue; + Assert.Equal(small, Min(big, small)); + Assert.Equal(small, Min(small, big)); + Assert.Equal(small, Math.Min(big, small)); + Assert.Equal(small, Math.Min(small, big)); + Assert.Equal(big, Max(big, small)); + Assert.Equal(big, Max(small, big)); + Assert.Equal(big, Math.Max(big, small)); + Assert.Equal(big, Math.Max(small, big)); + } + + [Fact] + public static void TestByte() + { + [MethodImpl(MethodImplOptions.NoInlining)] + static byte Min(byte a, byte b) => Math.Min(a, b); + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte Max(byte a, byte b) => Math.Max(a, b); + + const byte big = byte.MaxValue, small = byte.MinValue; + Assert.Equal(small, Min(big, small)); + Assert.Equal(small, Min(small, big)); + Assert.Equal(small, Math.Min(big, small)); + Assert.Equal(small, Math.Min(small, big)); + Assert.Equal(big, Max(big, small)); + Assert.Equal(big, Max(small, big)); + Assert.Equal(big, Math.Max(big, small)); + Assert.Equal(big, Math.Max(small, big)); + } + + [Fact] + public static void TestLongValueNumbering() + { + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + static (long, long, long, long, ulong, ulong, ulong, ulong) MinMaxes(long a, long b) => ( + Math.Min(a, b), + Math.Min(b, a), + Math.Max(a, b), + Math.Max(b, a), + Math.Min(unchecked((ulong)a), unchecked((ulong)b)), + Math.Min(unchecked((ulong)b), unchecked((ulong)a)), + Math.Max(unchecked((ulong)a), unchecked((ulong)b)), + Math.Max(unchecked((ulong)b), unchecked((ulong)a)) + ); + + var m = MinMaxes(long.MinValue, long.MaxValue); + Assert.Equal(long.MinValue, m.Item1); + Assert.Equal(long.MinValue, m.Item2); + Assert.Equal(long.MaxValue, m.Item3); + Assert.Equal(long.MaxValue, m.Item4); + Assert.Equal(unchecked((ulong)long.MaxValue), m.Item5); + Assert.Equal(unchecked((ulong)long.MaxValue), m.Item6); + Assert.Equal(unchecked((ulong)long.MinValue), m.Item7); + Assert.Equal(unchecked((ulong)long.MinValue), m.Item8); + } + + [Fact] + public static void TestIntValueNumbering() + { + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + static (int, int, int, int, uint, uint, uint, uint) MinMaxes(int a, int b) => ( + Math.Min(a, b), + Math.Min(b, a), + Math.Max(a, b), + Math.Max(b, a), + Math.Min(unchecked((uint)a), unchecked((uint)b)), + Math.Min(unchecked((uint)b), unchecked((uint)a)), + Math.Max(unchecked((uint)a), unchecked((uint)b)), + Math.Max(unchecked((uint)b), unchecked((uint)a)) + ); + + var m = MinMaxes(int.MinValue, int.MaxValue); + Assert.Equal(int.MinValue, m.Item1); + Assert.Equal(int.MinValue, m.Item2); + Assert.Equal(int.MaxValue, m.Item3); + Assert.Equal(int.MaxValue, m.Item4); + Assert.Equal(unchecked((uint)int.MaxValue), m.Item5); + Assert.Equal(unchecked((uint)int.MaxValue), m.Item6); + Assert.Equal(unchecked((uint)int.MinValue), m.Item7); + Assert.Equal(unchecked((uint)int.MinValue), m.Item8); + } + + [Fact] + public static void TestShortValueNumbering() + { + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + static (short, short, short, short, ushort, ushort, ushort, ushort) MinMaxes(short a, short b) => ( + Math.Min(a, b), + Math.Min(b, a), + Math.Max(a, b), + Math.Max(b, a), + Math.Min(unchecked((ushort)a), unchecked((ushort)b)), + Math.Min(unchecked((ushort)b), unchecked((ushort)a)), + Math.Max(unchecked((ushort)a), unchecked((ushort)b)), + Math.Max(unchecked((ushort)b), unchecked((ushort)a)) + ); + + var m = MinMaxes(short.MinValue, short.MaxValue); + Assert.Equal(short.MinValue, m.Item1); + Assert.Equal(short.MinValue, m.Item2); + Assert.Equal(short.MaxValue, m.Item3); + Assert.Equal(short.MaxValue, m.Item4); + Assert.Equal(unchecked((ushort)short.MaxValue), m.Item5); + Assert.Equal(unchecked((ushort)short.MaxValue), m.Item6); + Assert.Equal(unchecked((ushort)short.MinValue), m.Item7); + Assert.Equal(unchecked((ushort)short.MinValue), m.Item8); + } + + [Fact] + public static void TestByteValueNumbering() + { + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + static (sbyte, sbyte, sbyte, sbyte, byte, byte, byte, byte) MinMaxes(sbyte a, sbyte b) => ( + Math.Min(a, b), + Math.Min(b, a), + Math.Max(a, b), + Math.Max(b, a), + Math.Min(unchecked((byte)a), unchecked((byte)b)), + Math.Min(unchecked((byte)b), unchecked((byte)a)), + Math.Max(unchecked((byte)a), unchecked((byte)b)), + Math.Max(unchecked((byte)b), unchecked((byte)a)) + ); + + var m = MinMaxes(sbyte.MinValue, sbyte.MaxValue); + Assert.Equal(sbyte.MinValue, m.Item1); + Assert.Equal(sbyte.MinValue, m.Item2); + Assert.Equal(sbyte.MaxValue, m.Item3); + Assert.Equal(sbyte.MaxValue, m.Item4); + Assert.Equal(unchecked((byte)sbyte.MaxValue), m.Item5); + Assert.Equal(unchecked((byte)sbyte.MaxValue), m.Item6); + Assert.Equal(unchecked((byte)sbyte.MinValue), m.Item7); + Assert.Equal(unchecked((byte)sbyte.MinValue), m.Item8); + } + + [Fact] + public static void TestUnsignedIntLongValueNumbering() + { + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + static (uint, uint, uint, uint, ulong, ulong, ulong, ulong) MinMaxes(uint a, uint b) => ( + Math.Min(a, b), + Math.Min(b, a), + Math.Max(a, b), + Math.Max(b, a), + Math.Min((ulong)a, (ulong)b), + Math.Min((ulong)b, (ulong)a), + Math.Max((ulong)a, (ulong)b), + Math.Max((ulong)b, (ulong)a) + ); + + var m = MinMaxes(uint.MinValue, uint.MaxValue); + Assert.Equal(uint.MinValue, m.Item1); + Assert.Equal(uint.MinValue, m.Item2); + Assert.Equal(uint.MaxValue, m.Item3); + Assert.Equal(uint.MaxValue, m.Item4); + Assert.Equal((ulong)uint.MinValue, m.Item5); + Assert.Equal((ulong)uint.MinValue, m.Item6); + Assert.Equal((ulong)uint.MaxValue, m.Item7); + Assert.Equal((ulong)uint.MaxValue, m.Item8); + } +} diff --git a/src/tests/JIT/Intrinsics/MathMinMaxInteger_r.csproj b/src/tests/JIT/Intrinsics/MathMinMaxInteger_r.csproj new file mode 100644 index 00000000000000..666c2b240c423f --- /dev/null +++ b/src/tests/JIT/Intrinsics/MathMinMaxInteger_r.csproj @@ -0,0 +1,14 @@ + + + + true + + + true + None + + + + + + diff --git a/src/tests/JIT/Intrinsics/MathMinMaxInteger_ro.csproj b/src/tests/JIT/Intrinsics/MathMinMaxInteger_ro.csproj new file mode 100644 index 00000000000000..81b9c3a00b1021 --- /dev/null +++ b/src/tests/JIT/Intrinsics/MathMinMaxInteger_ro.csproj @@ -0,0 +1,14 @@ + + + + true + + + true + None + True + + + + +