Skip to content

Commit f8bcd05

Browse files
authored
Expand unboxing for Nullable<> in JIT (#105073)
1 parent b9e842a commit f8bcd05

File tree

2 files changed

+120
-24
lines changed

2 files changed

+120
-24
lines changed

src/coreclr/jit/compiler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4514,6 +4514,7 @@ class Compiler
45144514

45154515
GenTree* impStoreNullableFields(CORINFO_CLASS_HANDLE nullableCls,
45164516
GenTree* value);
4517+
GenTree* impInlineUnboxNullable(CORINFO_CLASS_HANDLE nullableCls, GenTree* nullableClsNode, GenTree* obj);
45174518
void impLoadNullableFields(GenTree* nullableObj, CORINFO_CLASS_HANDLE nullableCls, GenTree** hasValueFld, GenTree** valueFld);
45184519

45194520
int impBoxPatternMatch(CORINFO_RESOLVED_TOKEN* pResolvedToken,

src/coreclr/jit/importer.cpp

Lines changed: 119 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2832,6 +2832,117 @@ GenTree* Compiler::impImportLdvirtftn(GenTree* thisPtr,
28322832
return call;
28332833
}
28342834

2835+
//------------------------------------------------------------------------
2836+
// impInlineUnboxNullable: Generate code for unboxing Nullable<T> from an object (obj)
2837+
// We either inline the unbox operation (if profitable) or call the helper.
2838+
// The inline expansion is as follows:
2839+
//
2840+
// Nullable<T> result;
2841+
// if (obj == null)
2842+
// {
2843+
// result = default;
2844+
// }
2845+
// else if (obj->pMT == <real-boxed-type>)
2846+
// {
2847+
// result._hasValue = true;
2848+
// result._value = *(T*)(obj + sizeof(void*));
2849+
// }
2850+
// else
2851+
// {
2852+
// result = CORINFO_HELP_UNBOX_NULLABLE(&result, nullableCls, obj);
2853+
// }
2854+
//
2855+
// Arguments:
2856+
// nullableCls - class handle representing the Nullable<T> type
2857+
// nullableClsNode - tree node representing the Nullable<T> type (can be a runtime lookup tree)
2858+
// obj - object to unbox
2859+
//
2860+
// Return Value:
2861+
// A local node representing the unboxed value (Nullable<T>)
2862+
//
2863+
GenTree* Compiler::impInlineUnboxNullable(CORINFO_CLASS_HANDLE nullableCls, GenTree* nullableClsNode, GenTree* obj)
2864+
{
2865+
assert(info.compCompHnd->isNullableType(nullableCls) == TypeCompareState::Must);
2866+
2867+
unsigned resultTmp = lvaGrabTemp(true DEBUGARG("Nullable<T> tmp"));
2868+
lvaSetStruct(resultTmp, nullableCls, false);
2869+
lvaGetDesc(resultTmp)->lvHasLdAddrOp = true;
2870+
GenTreeLclFld* resultAddr = gtNewLclAddrNode(resultTmp, 0);
2871+
2872+
// Check profitability of inlining the unbox operation
2873+
bool shouldExpandInline = !compCurBB->isRunRarely() && opts.OptimizationEnabled() && !eeIsSharedInst(nullableCls);
2874+
2875+
// It's less profitable to inline the unbox operation if the underlying type is too large
2876+
CORINFO_CLASS_HANDLE unboxType = NO_CLASS_HANDLE;
2877+
if (shouldExpandInline)
2878+
{
2879+
// The underlying type of the nullable:
2880+
unboxType = info.compCompHnd->getTypeForBox(nullableCls);
2881+
shouldExpandInline = info.compCompHnd->getClassSize(unboxType) <= getUnrollThreshold(Memcpy);
2882+
}
2883+
2884+
if (!shouldExpandInline)
2885+
{
2886+
// No expansion needed, just call the helper
2887+
GenTreeCall* call =
2888+
gtNewHelperCallNode(CORINFO_HELP_UNBOX_NULLABLE, TYP_VOID, resultAddr, nullableClsNode, obj);
2889+
impAppendTree(call, CHECK_SPILL_ALL, impCurStmtDI);
2890+
return gtNewLclvNode(resultTmp, TYP_STRUCT);
2891+
}
2892+
2893+
// Clone the object (and spill side effects)
2894+
GenTree* objClone;
2895+
obj = impCloneExpr(obj, &objClone, CHECK_SPILL_ALL, nullptr DEBUGARG("op1 spilled for Nullable unbox"));
2896+
2897+
// Unbox the object to the result local:
2898+
//
2899+
// result._hasValue = true;
2900+
// result._value = MethodTableLookup(obj);
2901+
//
2902+
CORINFO_FIELD_HANDLE valueFldHnd = info.compCompHnd->getFieldInClass(nullableCls, 1);
2903+
CORINFO_CLASS_HANDLE valueStructCls;
2904+
var_types valueType = JITtype2varType(info.compCompHnd->getFieldType(valueFldHnd, &valueStructCls));
2905+
static_assert_no_msg(OFFSETOF__CORINFO_NullableOfT__hasValue == 0);
2906+
unsigned hasValOffset = OFFSETOF__CORINFO_NullableOfT__hasValue;
2907+
unsigned valueOffset = info.compCompHnd->getFieldOffset(valueFldHnd);
2908+
2909+
GenTree* boxedContentAddr =
2910+
gtNewOperNode(GT_ADD, TYP_BYREF, gtCloneExpr(objClone), gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL));
2911+
// Load the boxed content from the object (op1):
2912+
GenTree* boxedContent = valueType == TYP_STRUCT ? gtNewBlkIndir(typGetObjLayout(valueStructCls), boxedContentAddr)
2913+
: gtNewIndir(valueType, boxedContentAddr);
2914+
// Now do two stores via a comma:
2915+
GenTree* setHasValue = gtNewStoreLclFldNode(resultTmp, TYP_UBYTE, hasValOffset, gtNewIconNode(1));
2916+
GenTree* setValue = gtNewStoreLclFldNode(resultTmp, valueType, valueOffset, boxedContent);
2917+
GenTree* unboxTree = gtNewOperNode(GT_COMMA, TYP_VOID, setHasValue, setValue);
2918+
2919+
// Fallback helper call
2920+
// TODO: Mark as no-return when appropriate
2921+
GenTreeCall* helperCall =
2922+
gtNewHelperCallNode(CORINFO_HELP_UNBOX_NULLABLE, TYP_VOID, resultAddr, nullableClsNode, gtCloneExpr(objClone));
2923+
2924+
// Nested QMARK - "obj->pMT == <boxed-type> ? unboxTree : helperCall"
2925+
assert(unboxType != NO_CLASS_HANDLE);
2926+
GenTree* unboxTypeNode = gtNewIconEmbClsHndNode(unboxType);
2927+
GenTree* objMT = gtNewMethodTableLookup(objClone);
2928+
GenTree* mtLookupCond = gtNewOperNode(GT_NE, TYP_INT, objMT, unboxTypeNode);
2929+
GenTreeColon* mtCheckColon = gtNewColonNode(TYP_VOID, helperCall, unboxTree);
2930+
GenTreeQmark* mtCheckQmark = gtNewQmarkNode(TYP_VOID, mtLookupCond, mtCheckColon);
2931+
mtCheckQmark->SetThenNodeLikelihood(0);
2932+
2933+
// Zero initialize the result in case of "obj == null"
2934+
GenTreeLclVar* zeroInitResultNode = gtNewStoreLclVarNode(resultTmp, gtNewIconNode(0));
2935+
2936+
// Root condition - "obj == null ? zeroInitResultNode : mtCheckQmark"
2937+
GenTree* nullcheck = gtNewOperNode(GT_NE, TYP_INT, obj, gtNewNull());
2938+
GenTreeColon* nullCheckColon = gtNewColonNode(TYP_VOID, mtCheckQmark, zeroInitResultNode);
2939+
GenTreeQmark* nullCheckQmark = gtNewQmarkNode(TYP_VOID, nullcheck, nullCheckColon);
2940+
2941+
// Spill the root QMARK and return the result local
2942+
impAppendTree(nullCheckQmark, CHECK_SPILL_ALL, impCurStmtDI);
2943+
return gtNewLclvNode(resultTmp, TYP_STRUCT);
2944+
}
2945+
28352946
//------------------------------------------------------------------------
28362947
// impStoreNullableFields: create a Nullable<T> object and store
28372948
// 'hasValue' (always true) and the given value for 'value' field
@@ -10098,36 +10209,20 @@ void Compiler::impImportBlockCode(BasicBlock* block)
1009810209
op2 = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL);
1009910210
op1 = gtNewOperNode(GT_ADD, TYP_BYREF, cloneOperand, op2);
1010010211
}
10212+
else if (helper == CORINFO_HELP_UNBOX_NULLABLE)
10213+
{
10214+
// op1 is the object being unboxed
10215+
// op2 is either a class handle node or a runtime lookup node (it's fine to reorder)
10216+
op1 = impInlineUnboxNullable(resolvedToken.hClass, op2, op1);
10217+
}
1010110218
else
1010210219
{
1010310220
// Don't optimize, just call the helper and be done with it
1010410221
JITDUMP("\n Importing %s as helper call because %s\n", opcode == CEE_UNBOX ? "UNBOX" : "UNBOX.ANY",
1010510222
canExpandInline ? "want smaller code or faster jitting" : "inline expansion not legal");
1010610223

10107-
if (helper == CORINFO_HELP_UNBOX)
10108-
{
10109-
op1 = gtNewHelperCallNode(helper, TYP_BYREF, op2, op1);
10110-
}
10111-
else
10112-
{
10113-
assert(helper == CORINFO_HELP_UNBOX_NULLABLE);
10114-
10115-
// We're going to emit the following sequence of IR:
10116-
//
10117-
// Nullable<T> result;
10118-
// void CORINFO_HELP_UNBOX_NULLABLE(&result, unboxCls, obj);
10119-
// push result;
10120-
//
10121-
unsigned resultTmp = lvaGrabTemp(true DEBUGARG("Nullable<T> tmp"));
10122-
lvaSetStruct(resultTmp, resolvedToken.hClass, false);
10123-
lvaGetDesc(resultTmp)->lvHasLdAddrOp = true;
10124-
10125-
GenTreeLclFld* resultAddr = gtNewLclAddrNode(resultTmp, 0);
10126-
// NOTE: it's fine for op2 to be evaluated before op1
10127-
GenTreeCall* helperCall = gtNewHelperCallNode(helper, TYP_VOID, resultAddr, op2, op1);
10128-
impAppendTree(helperCall, CHECK_SPILL_ALL, impCurStmtDI);
10129-
op1 = gtNewLclvNode(resultTmp, TYP_STRUCT);
10130-
}
10224+
assert(helper == CORINFO_HELP_UNBOX);
10225+
op1 = gtNewHelperCallNode(helper, TYP_BYREF, op2, op1);
1013110226
}
1013210227

1013310228
assert((helper == CORINFO_HELP_UNBOX && op1->gtType == TYP_BYREF) || // Unbox helper returns a byref.

0 commit comments

Comments
 (0)