From 6dca675de39da5f9785259203007f011527ea52b Mon Sep 17 00:00:00 2001 From: EgorBo Date: Fri, 12 Jul 2024 19:52:00 +0200 Subject: [PATCH 1/6] Optimize boxing for ThrowIfNull --- src/coreclr/jit/importercalls.cpp | 36 +++++++++++++++++++ src/coreclr/jit/namedintrinsiclist.h | 2 ++ .../src/System/ArgumentNullException.cs | 2 ++ 3 files changed, 40 insertions(+) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index e7889ed79fccd6..f95faaaabd989b 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -3256,6 +3256,9 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd, // to avoid some unnecessary boxing case NI_System_Enum_HasFlag: + // This one is made intrinsic specifically to avoid boxing in Tier0 + case NI_System_ArgumentNullException_ThrowIfNull: + // Most atomics are compiled to single instructions case NI_System_Threading_Interlocked_And: case NI_System_Threading_Interlocked_Or: @@ -3786,6 +3789,32 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd, break; } + case NI_System_ArgumentNullException_ThrowIfNull: + { + // void ThrowIfNull(object? argument, string? paramName = null) + // void ThrowIfNull(object? argument, ExceptionArgument paramName) + assert(sig->numArgs == 2); + assert(sig->retType == CORINFO_TYPE_VOID); + + // if we see: + // + // ArgumentNullException_ThrowIfNull(GT_BOX(...), ...) + // + // We should be able to remove the call (and the box). It is done via intrinsic only + // because Tier0 is not able to remove the box itself. + // + GenTree* arg = impStackTop(1).val; + if (arg->OperIs(GT_BOX)) + { + impSpillSideEffects(true, CHECK_SPILL_ALL DEBUGARG("spill side effects for ThrowIfNull")); + gtTryRemoveBoxUpstreamEffects(arg, BR_REMOVE_AND_NARROW); + impPopStack(); + impPopStack(); + retNode = gtNewNothingNode(); + } + break; + } + case NI_System_Enum_HasFlag: { GenTree* thisOp = impStackTop(1).val; @@ -9825,6 +9854,13 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method) result = NI_System_Activator_DefaultConstructorOf; } } + else if (strcmp(className, "ArgumentNullException") == 0) + { + if (strcmp(methodName, "ThrowIfNull") == 0) + { + result = NI_System_ArgumentNullException_ThrowIfNull; + } + } else if (strcmp(className, "Array") == 0) { if (strcmp(methodName, "Clone") == 0) diff --git a/src/coreclr/jit/namedintrinsiclist.h b/src/coreclr/jit/namedintrinsiclist.h index 0ec8fd2496ba38..93e4ce7893f98b 100644 --- a/src/coreclr/jit/namedintrinsiclist.h +++ b/src/coreclr/jit/namedintrinsiclist.h @@ -13,6 +13,8 @@ enum NamedIntrinsic : unsigned short { NI_Illegal = 0, + NI_System_ArgumentNullException_ThrowIfNull, + NI_System_Enum_HasFlag, NI_System_BitConverter_DoubleToInt64Bits, diff --git a/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs b/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs index b563c12dfa3567..8094b882ef5486 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs @@ -51,6 +51,7 @@ protected ArgumentNullException(SerializationInfo info, StreamingContext context /// Throws an if is null. /// The reference type argument to validate as non-null. /// The name of the parameter with which corresponds. + [Intrinsic] // Tier0 intrinsic to avoid redundant boxing in generics public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) { if (argument is null) @@ -59,6 +60,7 @@ public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpres } } + [Intrinsic] // Tier0 intrinsic to avoid redundant boxing in generics internal static void ThrowIfNull([NotNull] object? argument, ExceptionArgument paramName) { if (argument is null) From 3d25a884d0d5dd7044c6d5c153b6e1caa41e07cc Mon Sep 17 00:00:00 2001 From: EgorBo Date: Fri, 12 Jul 2024 19:55:07 +0200 Subject: [PATCH 2/6] fix debug --- src/coreclr/jit/importercalls.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index f95faaaabd989b..b5838b4c87cb42 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -3804,7 +3804,7 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd, // because Tier0 is not able to remove the box itself. // GenTree* arg = impStackTop(1).val; - if (arg->OperIs(GT_BOX)) + if (arg->OperIs(GT_BOX) && !opts.compDbgCode && !opts.jitFlags->IsSet(JitFlags::JIT_FLAG_MIN_OPT)) { impSpillSideEffects(true, CHECK_SPILL_ALL DEBUGARG("spill side effects for ThrowIfNull")); gtTryRemoveBoxUpstreamEffects(arg, BR_REMOVE_AND_NARROW); From ae84b623f9557a81d5316628916d7b80e923acf1 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Sat, 13 Jul 2024 17:57:54 +0200 Subject: [PATCH 3/6] Handle nullable --- src/coreclr/jit/gentree.h | 8 +++- src/coreclr/jit/importercalls.cpp | 74 ++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/coreclr/jit/gentree.h b/src/coreclr/jit/gentree.h index 3f839c4ae8ec6f..55aaab16943718 100644 --- a/src/coreclr/jit/gentree.h +++ b/src/coreclr/jit/gentree.h @@ -2267,6 +2267,7 @@ struct GenTree return OperGet() == GT_CALL; } inline bool IsHelperCall(); + inline bool IsHelperCall(Compiler* compiler, unsigned helper); bool gtOverflow() const; bool gtOverflowEx() const; @@ -10504,7 +10505,12 @@ inline bool GenTree::IsCnsVec() const inline bool GenTree::IsHelperCall() { - return OperGet() == GT_CALL && AsCall()->IsHelperCall(); + return IsCall() && AsCall()->IsHelperCall(); +} + +inline bool GenTree::IsHelperCall(Compiler* compiler, unsigned helper) +{ + return IsCall() && AsCall()->IsHelperCall(compiler, helper); } inline var_types GenTree::CastFromType() diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index b5838b4c87cb42..de832a56d7227b 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -1330,6 +1330,55 @@ var_types Compiler::impImportCall(OPCODE opcode, } else { + // We have the following pattern: + // + // ArgumentNullException.ThrowIfNull(CORINFO_HELP_BOX_NULLABLE(classHandle, addr), valueName); + // + // We need to transform it to: + // + // var addrTmp = addr; + // var valueNameTmp = valueName; + // addr->hasValue != 0 ? NOP : ArgumentNullException.ThrowIfNull(null, valueNameTmp) + // + if (call->IsCall() && call->AsCall()->IsSpecialIntrinsic(this, NI_System_ArgumentNullException_ThrowIfNull)) + { + GenTreeCall* throwIfNullCall = call->AsCall(); + assert(throwIfNullCall->gtArgs.CountUserArgs() == 2); + + GenTree* value = throwIfNullCall->gtArgs.GetUserArgByIndex(0)->GetNode(); + GenTree* valueName = throwIfNullCall->gtArgs.GetUserArgByIndex(1)->GetNode(); + + if (value->IsHelperCall(this, CORINFO_HELP_BOX_NULLABLE)) + { + GenTree* boxHelperClsArg = value->AsCall()->gtArgs.GetUserArgByIndex(0)->GetNode(); + GenTree* boxHelperAddrArg = value->AsCall()->gtArgs.GetUserArgByIndex(1)->GetNode(); + + // boxHelperClsArg is always just a class handle constant, so we don't bother spilling it. + if ((boxHelperClsArg->gtFlags & GTF_SIDE_EFFECT) == 0) + { + // Now we need to spill the addr and argName arguments in the correct order + // to preserve possible side effects. + unsigned boxedValTmp = lvaGrabTemp(true DEBUGARG("boxedVal spilled")); + unsigned boxedArgNameTmp = lvaGrabTemp(true DEBUGARG("boxedArg spilled")); + impStoreToTemp(boxedValTmp, boxHelperAddrArg, CHECK_SPILL_ALL); + impStoreToTemp(boxedArgNameTmp, valueName, CHECK_SPILL_ALL); + + // Change arguments to 'ThrowIfNull(null, valueNameTmp)' + throwIfNullCall->gtArgs.GetUserArgByIndex(0)->EarlyNodeRef() = gtNewNull(); + throwIfNullCall->gtArgs.GetUserArgByIndex(1)->EarlyNodeRef() = + gtNewLclvNode(boxedArgNameTmp, valueName->TypeGet()); + + // This is Tier0 specific, so we create a raw indir node to access Nullable.hasValue field + // which is the first field of Nullable struct and is of type 'bool'. + // + GenTree* hasValueField = + gtNewIndir(TYP_UBYTE, gtNewLclvNode(boxedValTmp, boxHelperAddrArg->TypeGet())); + GenTreeOp* cond = gtNewOperNode(GT_NE, TYP_INT, hasValueField, gtNewIconNode(0)); + + call = gtNewQmarkNode(TYP_VOID, cond, gtNewColonNode(TYP_VOID, gtNewNothingNode(), call)); + } + } + } impAppendTree(call, CHECK_SPILL_ALL, impCurStmtDI); } } @@ -3791,20 +3840,26 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd, case NI_System_ArgumentNullException_ThrowIfNull: { - // void ThrowIfNull(object? argument, string? paramName = null) - // void ThrowIfNull(object? argument, ExceptionArgument paramName) + // void ThrowIfNull(object argument, string paramName = null) + // void ThrowIfNull(object argument, ExceptionArgument paramName) assert(sig->numArgs == 2); assert(sig->retType == CORINFO_TYPE_VOID); + if (opts.compDbgCode || opts.jitFlags->IsSet(JitFlags::JIT_FLAG_MIN_OPT)) + { + // Don't fold it for debug code or forced MinOpts + break; + } + // if we see: // // ArgumentNullException_ThrowIfNull(GT_BOX(...), ...) // - // We should be able to remove the call (and the box). It is done via intrinsic only - // because Tier0 is not able to remove the box itself. + // We should be able to remove the call (and the box). It is done via intrinsic + // because Tier0 is not able to remove the box itself (it doesn't inline callees). // GenTree* arg = impStackTop(1).val; - if (arg->OperIs(GT_BOX) && !opts.compDbgCode && !opts.jitFlags->IsSet(JitFlags::JIT_FLAG_MIN_OPT)) + if (arg->OperIs(GT_BOX)) { impSpillSideEffects(true, CHECK_SPILL_ALL DEBUGARG("spill side effects for ThrowIfNull")); gtTryRemoveBoxUpstreamEffects(arg, BR_REMOVE_AND_NARROW); @@ -3812,6 +3867,15 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd, impPopStack(); retNode = gtNewNothingNode(); } + else + { + // Nullable is a bit more complicated, we need to materialize the actual ThrowIfNull call first + // NOTE: when optimizations are enabled, we generate a better code for this case as is. + if (!opts.OptimizationEnabled() && arg->IsHelperCall(this, CORINFO_HELP_BOX_NULLABLE)) + { + isSpecial = true; + } + } break; } From d446a2b70992d7542b230ad0b0d73db8440a3750 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Sun, 14 Jul 2024 00:19:06 +0200 Subject: [PATCH 4/6] clean up --- src/coreclr/jit/compiler.h | 2 + src/coreclr/jit/importercalls.cpp | 110 +++++++++++++++++------------- 2 files changed, 66 insertions(+), 46 deletions(-) diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 5a7977ff88e873..c52a59a9554d77 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -4541,6 +4541,8 @@ class Compiler GenTree* impDuplicateWithProfiledArg(GenTreeCall* call, IL_OFFSET ilOffset); + GenTree* impThrowIfNull(GenTreeCall* call); + #ifdef DEBUG var_types impImportJitTestLabelMark(int numArgs); #endif // DEBUG diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index de832a56d7227b..c96e683b1c8b97 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -1330,54 +1330,9 @@ var_types Compiler::impImportCall(OPCODE opcode, } else { - // We have the following pattern: - // - // ArgumentNullException.ThrowIfNull(CORINFO_HELP_BOX_NULLABLE(classHandle, addr), valueName); - // - // We need to transform it to: - // - // var addrTmp = addr; - // var valueNameTmp = valueName; - // addr->hasValue != 0 ? NOP : ArgumentNullException.ThrowIfNull(null, valueNameTmp) - // if (call->IsCall() && call->AsCall()->IsSpecialIntrinsic(this, NI_System_ArgumentNullException_ThrowIfNull)) { - GenTreeCall* throwIfNullCall = call->AsCall(); - assert(throwIfNullCall->gtArgs.CountUserArgs() == 2); - - GenTree* value = throwIfNullCall->gtArgs.GetUserArgByIndex(0)->GetNode(); - GenTree* valueName = throwIfNullCall->gtArgs.GetUserArgByIndex(1)->GetNode(); - - if (value->IsHelperCall(this, CORINFO_HELP_BOX_NULLABLE)) - { - GenTree* boxHelperClsArg = value->AsCall()->gtArgs.GetUserArgByIndex(0)->GetNode(); - GenTree* boxHelperAddrArg = value->AsCall()->gtArgs.GetUserArgByIndex(1)->GetNode(); - - // boxHelperClsArg is always just a class handle constant, so we don't bother spilling it. - if ((boxHelperClsArg->gtFlags & GTF_SIDE_EFFECT) == 0) - { - // Now we need to spill the addr and argName arguments in the correct order - // to preserve possible side effects. - unsigned boxedValTmp = lvaGrabTemp(true DEBUGARG("boxedVal spilled")); - unsigned boxedArgNameTmp = lvaGrabTemp(true DEBUGARG("boxedArg spilled")); - impStoreToTemp(boxedValTmp, boxHelperAddrArg, CHECK_SPILL_ALL); - impStoreToTemp(boxedArgNameTmp, valueName, CHECK_SPILL_ALL); - - // Change arguments to 'ThrowIfNull(null, valueNameTmp)' - throwIfNullCall->gtArgs.GetUserArgByIndex(0)->EarlyNodeRef() = gtNewNull(); - throwIfNullCall->gtArgs.GetUserArgByIndex(1)->EarlyNodeRef() = - gtNewLclvNode(boxedArgNameTmp, valueName->TypeGet()); - - // This is Tier0 specific, so we create a raw indir node to access Nullable.hasValue field - // which is the first field of Nullable struct and is of type 'bool'. - // - GenTree* hasValueField = - gtNewIndir(TYP_UBYTE, gtNewLclvNode(boxedValTmp, boxHelperAddrArg->TypeGet())); - GenTreeOp* cond = gtNewOperNode(GT_NE, TYP_INT, hasValueField, gtNewIconNode(0)); - - call = gtNewQmarkNode(TYP_VOID, cond, gtNewColonNode(TYP_VOID, gtNewNothingNode(), call)); - } - } + call = impThrowIfNull(call->AsCall()); } impAppendTree(call, CHECK_SPILL_ALL, impCurStmtDI); } @@ -1577,6 +1532,69 @@ var_types Compiler::impImportCall(OPCODE opcode, #pragma warning(pop) #endif +//------------------------------------------------------------------------ +// impThrowIfNull: Remove redundandant boxing from ArgumentNullException_ThrowIfNull +// it is done for Tier0 where we can't remove it without inlining otherwise. +// +// We have the following pattern: +// +// ArgumentNullException.ThrowIfNull(CORINFO_HELP_BOX_NULLABLE(classHandle, addr), valueName); +// +// We need to transform it to: +// +// var addrTmp = addr; +// var valueNameTmp = valueName; +// addr->hasValue != 0 ? NOP : ArgumentNullException.ThrowIfNull(null, valueNameTmp) +// +// Arguments: +// call -- call representing ArgumentNullException_ThrowIfNull +// +// Return Value: +// Optimized tree (or the original call tree if we can't optimize it). +// +GenTree* Compiler::impThrowIfNull(GenTreeCall* call) +{ + assert(call->IsSpecialIntrinsic(this, NI_System_ArgumentNullException_ThrowIfNull)); + assert(call->gtArgs.CountUserArgs() == 2); + + GenTree* value = call->gtArgs.GetUserArgByIndex(0)->GetNode(); + GenTree* valueName = call->gtArgs.GetUserArgByIndex(1)->GetNode(); + + if (!value->IsHelperCall(this, CORINFO_HELP_BOX_NULLABLE)) + { + // We're not boxing - bail out. + return call; + } + + GenTree* boxHelperClsArg = value->AsCall()->gtArgs.GetUserArgByIndex(0)->GetNode(); + GenTree* boxHelperAddrArg = value->AsCall()->gtArgs.GetUserArgByIndex(1)->GetNode(); + + if ((boxHelperClsArg->gtFlags & GTF_SIDE_EFFECT) != 0) + { + // boxHelperClsArg is always just a class handle constant, so we don't bother spilling it. + return call; + } + + // Now we need to spill the addr and argName arguments in the correct order + // to preserve possible side effects. + unsigned boxedValTmp = lvaGrabTemp(true DEBUGARG("boxedVal spilled")); + unsigned boxedArgNameTmp = lvaGrabTemp(true DEBUGARG("boxedArg spilled")); + impStoreToTemp(boxedValTmp, boxHelperAddrArg, CHECK_SPILL_ALL); + impStoreToTemp(boxedArgNameTmp, valueName, CHECK_SPILL_ALL); + + // Change arguments to 'ThrowIfNull(null, valueNameTmp)' + call->gtArgs.GetUserArgByIndex(0)->EarlyNodeRef() = gtNewNull(); + call->gtArgs.GetUserArgByIndex(1)->EarlyNodeRef() = gtNewLclvNode(boxedArgNameTmp, valueName->TypeGet()); + + // This is Tier0 specific, so we create a raw indir node to access Nullable.hasValue field + // which is the first field of Nullable struct and is of type 'bool'. + // + GenTree* hasValueField = gtNewIndir(TYP_UBYTE, gtNewLclvNode(boxedValTmp, boxHelperAddrArg->TypeGet())); + GenTreeOp* cond = gtNewOperNode(GT_NE, TYP_INT, hasValueField, gtNewIconNode(0)); + + return gtNewQmarkNode(TYP_VOID, cond, gtNewColonNode(TYP_VOID, gtNewNothingNode(), call)); +} + //------------------------------------------------------------------------ // impDuplicateWithProfiledArg: duplicates a call with a profiled argument, e.g.: // Given `Buffer.Memmove(dst, src, len)` call, From ee83f6b33c361c3097c59c164fbc8e36e05405aa Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Sun, 14 Jul 2024 22:41:15 +0200 Subject: [PATCH 5/6] Update src/coreclr/jit/importercalls.cpp Co-authored-by: Jakob Botsch Nielsen --- src/coreclr/jit/importercalls.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index c96e683b1c8b97..5d205bc036d34f 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -1589,6 +1589,7 @@ GenTree* Compiler::impThrowIfNull(GenTreeCall* call) // This is Tier0 specific, so we create a raw indir node to access Nullable.hasValue field // which is the first field of Nullable struct and is of type 'bool'. // + static_assert_no_msg(OFFSETOF__CORINFO_NullableOfT__hasValue == 0); GenTree* hasValueField = gtNewIndir(TYP_UBYTE, gtNewLclvNode(boxedValTmp, boxHelperAddrArg->TypeGet())); GenTreeOp* cond = gtNewOperNode(GT_NE, TYP_INT, hasValueField, gtNewIconNode(0)); From 6a7fdf7f56938e1890959e08b45ad411468249d8 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Mon, 15 Jul 2024 12:40:17 +0200 Subject: [PATCH 6/6] Address feedback --- src/coreclr/jit/importercalls.cpp | 92 ++++++++++++++----------------- 1 file changed, 41 insertions(+), 51 deletions(-) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 05b96bfb33aa7f..0d2045d553c826 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -1536,16 +1536,6 @@ var_types Compiler::impImportCall(OPCODE opcode, // impThrowIfNull: Remove redundandant boxing from ArgumentNullException_ThrowIfNull // it is done for Tier0 where we can't remove it without inlining otherwise. // -// We have the following pattern: -// -// ArgumentNullException.ThrowIfNull(CORINFO_HELP_BOX_NULLABLE(classHandle, addr), valueName); -// -// We need to transform it to: -// -// var addrTmp = addr; -// var valueNameTmp = valueName; -// addr->hasValue != 0 ? NOP : ArgumentNullException.ThrowIfNull(null, valueNameTmp) -// // Arguments: // call -- call representing ArgumentNullException_ThrowIfNull // @@ -1554,15 +1544,52 @@ var_types Compiler::impImportCall(OPCODE opcode, // GenTree* Compiler::impThrowIfNull(GenTreeCall* call) { + // We have two overloads: + // + // void ThrowIfNull(object argument, string paramName = null) + // void ThrowIfNull(object argument, ExceptionArgument paramName) + // assert(call->IsSpecialIntrinsic(this, NI_System_ArgumentNullException_ThrowIfNull)); assert(call->gtArgs.CountUserArgs() == 2); + assert(call->TypeIs(TYP_VOID)); + + if (opts.compDbgCode || opts.jitFlags->IsSet(JitFlags::JIT_FLAG_MIN_OPT)) + { + // Don't fold it for debug code or forced MinOpts + return call; + } GenTree* value = call->gtArgs.GetUserArgByIndex(0)->GetNode(); GenTree* valueName = call->gtArgs.GetUserArgByIndex(1)->GetNode(); - if (!value->IsHelperCall(this, CORINFO_HELP_BOX_NULLABLE)) + // Case 1: value-type (non-nullable): + // + // ArgumentNullException_ThrowIfNull(GT_BOX(value), valueName) + // -> + // NOP (with side-effects if any) + // + if (value->OperIs(GT_BOX)) + { + // Now we need to spill the addr and argName arguments in the correct order + // to preserve possible side effects. + unsigned boxedValTmp = lvaGrabTemp(true DEBUGARG("boxedVal spilled")); + unsigned boxedArgNameTmp = lvaGrabTemp(true DEBUGARG("boxedArg spilled")); + impStoreToTemp(boxedValTmp, value, CHECK_SPILL_ALL); + impStoreToTemp(boxedArgNameTmp, valueName, CHECK_SPILL_ALL); + gtTryRemoveBoxUpstreamEffects(value, BR_REMOVE_AND_NARROW); + return gtNewNothingNode(); + } + + // Case 2: nullable: + // + // ArgumentNullException.ThrowIfNull(CORINFO_HELP_BOX_NULLABLE(classHandle, addr), valueName); + // -> + // addr->hasValue != 0 ? NOP : ArgumentNullException.ThrowIfNull(null, valueNameTmp) + // + if (opts.OptimizationEnabled() || !value->IsHelperCall(this, CORINFO_HELP_BOX_NULLABLE)) { // We're not boxing - bail out. + // NOTE: when opts are enabled, we remove the box as is (with better CQ) return call; } @@ -1583,8 +1610,8 @@ GenTree* Compiler::impThrowIfNull(GenTreeCall* call) impStoreToTemp(boxedArgNameTmp, valueName, CHECK_SPILL_ALL); // Change arguments to 'ThrowIfNull(null, valueNameTmp)' - call->gtArgs.GetUserArgByIndex(0)->EarlyNodeRef() = gtNewNull(); - call->gtArgs.GetUserArgByIndex(1)->EarlyNodeRef() = gtNewLclvNode(boxedArgNameTmp, valueName->TypeGet()); + call->gtArgs.GetUserArgByIndex(0)->SetEarlyNode(gtNewNull()); + call->gtArgs.GetUserArgByIndex(1)->SetEarlyNode(gtNewLclvNode(boxedArgNameTmp, valueName->TypeGet())); // This is Tier0 specific, so we create a raw indir node to access Nullable.hasValue field // which is the first field of Nullable struct and is of type 'bool'. @@ -3859,45 +3886,8 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd, } case NI_System_ArgumentNullException_ThrowIfNull: - { - // void ThrowIfNull(object argument, string paramName = null) - // void ThrowIfNull(object argument, ExceptionArgument paramName) - assert(sig->numArgs == 2); - assert(sig->retType == CORINFO_TYPE_VOID); - - if (opts.compDbgCode || opts.jitFlags->IsSet(JitFlags::JIT_FLAG_MIN_OPT)) - { - // Don't fold it for debug code or forced MinOpts - break; - } - - // if we see: - // - // ArgumentNullException_ThrowIfNull(GT_BOX(...), ...) - // - // We should be able to remove the call (and the box). It is done via intrinsic - // because Tier0 is not able to remove the box itself (it doesn't inline callees). - // - GenTree* arg = impStackTop(1).val; - if (arg->OperIs(GT_BOX)) - { - impSpillSideEffects(true, CHECK_SPILL_ALL DEBUGARG("spill side effects for ThrowIfNull")); - gtTryRemoveBoxUpstreamEffects(arg, BR_REMOVE_AND_NARROW); - impPopStack(); - impPopStack(); - retNode = gtNewNothingNode(); - } - else - { - // Nullable is a bit more complicated, we need to materialize the actual ThrowIfNull call first - // NOTE: when optimizations are enabled, we generate a better code for this case as is. - if (!opts.OptimizationEnabled() && arg->IsHelperCall(this, CORINFO_HELP_BOX_NULLABLE)) - { - isSpecial = true; - } - } + isSpecial = true; break; - } case NI_System_Enum_HasFlag: {