Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand unboxing for Nullable<> in JIT #105073

Merged
merged 6 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -4514,6 +4514,7 @@ class Compiler

GenTree* impStoreNullableFields(CORINFO_CLASS_HANDLE nullableCls,
GenTree* value);
GenTree* impInlineUnboxNullable(CORINFO_CLASS_HANDLE nullableCls, GenTree* nullableClsNode, GenTree* obj);
void impLoadNullableFields(GenTree* nullableObj, CORINFO_CLASS_HANDLE nullableCls, GenTree** hasValueFld, GenTree** valueFld);

int impBoxPatternMatch(CORINFO_RESOLVED_TOKEN* pResolvedToken,
Expand Down
143 changes: 119 additions & 24 deletions src/coreclr/jit/importer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2832,6 +2832,117 @@ GenTree* Compiler::impImportLdvirtftn(GenTree* thisPtr,
return call;
}

//------------------------------------------------------------------------
// impInlineUnboxNullable: Generate code for unboxing Nullable<T> from an object (obj)
// We either inline the unbox operation (if profitable) or call the helper.
// The inline expansion is as follows:
//
// Nullable<T> result;
// if (obj == null)
// {
// result = default;
// }
// else if (obj->pMT == <real-boxed-type>)
// {
// result._hasValue = true;
// result._value = *(T*)(obj + sizeof(void*));
// }
// else
// {
// result = CORINFO_HELP_UNBOX_NULLABLE(&result, nullableCls, obj);
// }
//
// Arguments:
// nullableCls - class handle representing the Nullable<T> type
// nullableClsNode - tree node representing the Nullable<T> type (can be a runtime lookup tree)
// obj - object to unbox
//
// Return Value:
// A local node representing the unboxed value (Nullable<T>)
//
GenTree* Compiler::impInlineUnboxNullable(CORINFO_CLASS_HANDLE nullableCls, GenTree* nullableClsNode, GenTree* obj)
{
assert(info.compCompHnd->isNullableType(nullableCls) == TypeCompareState::Must);

unsigned resultTmp = lvaGrabTemp(true DEBUGARG("Nullable<T> tmp"));
lvaSetStruct(resultTmp, nullableCls, false);
lvaGetDesc(resultTmp)->lvHasLdAddrOp = true;
GenTreeLclFld* resultAddr = gtNewLclAddrNode(resultTmp, 0);

// Check profitability of inlining the unbox operation
bool shouldExpandInline = !compCurBB->isRunRarely() && opts.OptimizationEnabled() && !eeIsSharedInst(nullableCls);

// It's less profitable to inline the unbox operation if the underlying type is too large
CORINFO_CLASS_HANDLE unboxType = NO_CLASS_HANDLE;
if (shouldExpandInline)
{
// The underlying type of the nullable:
unboxType = info.compCompHnd->getTypeForBox(nullableCls);
shouldExpandInline = info.compCompHnd->getClassSize(unboxType) <= getUnrollThreshold(Memcpy);
}

if (!shouldExpandInline)
{
// No expansion needed, just call the helper
GenTreeCall* call =
gtNewHelperCallNode(CORINFO_HELP_UNBOX_NULLABLE, TYP_VOID, resultAddr, nullableClsNode, obj);
impAppendTree(call, CHECK_SPILL_ALL, impCurStmtDI);
return gtNewLclvNode(resultTmp, TYP_STRUCT);
}

// Clone the object (and spill side effects)
GenTree* objClone;
obj = impCloneExpr(obj, &objClone, CHECK_SPILL_ALL, nullptr DEBUGARG("op1 spilled for Nullable unbox"));

// Unbox the object to the result local:
//
// result._hasValue = true;
// result._value = MethodTableLookup(obj);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, wrong comment, it's obj->RawData

//
CORINFO_FIELD_HANDLE valueFldHnd = info.compCompHnd->getFieldInClass(nullableCls, 1);
CORINFO_CLASS_HANDLE valueStructCls;
var_types valueType = JITtype2varType(info.compCompHnd->getFieldType(valueFldHnd, &valueStructCls));
static_assert_no_msg(OFFSETOF__CORINFO_NullableOfT__hasValue == 0);
unsigned hasValOffset = OFFSETOF__CORINFO_NullableOfT__hasValue;
unsigned valueOffset = info.compCompHnd->getFieldOffset(valueFldHnd);

GenTree* boxedContentAddr =
gtNewOperNode(GT_ADD, TYP_BYREF, gtCloneExpr(objClone), gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL));
// Load the boxed content from the object (op1):
GenTree* boxedContent = valueType == TYP_STRUCT ? gtNewBlkIndir(typGetObjLayout(valueStructCls), boxedContentAddr)
: gtNewIndir(valueType, boxedContentAddr);
// Now do two stores via a comma:
GenTree* setHasValue = gtNewStoreLclFldNode(resultTmp, TYP_UBYTE, hasValOffset, gtNewIconNode(1));
GenTree* setValue = gtNewStoreLclFldNode(resultTmp, valueType, valueOffset, boxedContent);
GenTree* unboxTree = gtNewOperNode(GT_COMMA, TYP_VOID, setHasValue, setValue);

// Fallback helper call
// TODO: Mark as no-return when appropriate
GenTreeCall* helperCall =
gtNewHelperCallNode(CORINFO_HELP_UNBOX_NULLABLE, TYP_VOID, resultAddr, nullableClsNode, gtCloneExpr(objClone));

// Nested QMARK - "obj->pMT == <boxed-type> ? unboxTree : helperCall"
assert(unboxType != NO_CLASS_HANDLE);
GenTree* unboxTypeNode = gtNewIconEmbClsHndNode(unboxType);
GenTree* objMT = gtNewMethodTableLookup(objClone);
GenTree* mtLookupCond = gtNewOperNode(GT_NE, TYP_INT, objMT, unboxTypeNode);
GenTreeColon* mtCheckColon = gtNewColonNode(TYP_VOID, helperCall, unboxTree);
GenTreeQmark* mtCheckQmark = gtNewQmarkNode(TYP_VOID, mtLookupCond, mtCheckColon);
mtCheckQmark->SetThenNodeLikelihood(0);

// Zero initialize the result in case of "obj == null"
GenTreeLclVar* zeroInitResultNode = gtNewStoreLclVarNode(resultTmp, gtNewIconNode(0));

// Root condition - "obj == null ? zeroInitResultNode : mtCheckQmark"
GenTree* nullcheck = gtNewOperNode(GT_NE, TYP_INT, obj, gtNewNull());
GenTreeColon* nullCheckColon = gtNewColonNode(TYP_VOID, mtCheckQmark, zeroInitResultNode);
GenTreeQmark* nullCheckQmark = gtNewQmarkNode(TYP_VOID, nullcheck, nullCheckColon);

// Spill the root QMARK and return the result local
impAppendTree(nullCheckQmark, CHECK_SPILL_ALL, impCurStmtDI);
return gtNewLclvNode(resultTmp, TYP_STRUCT);
}

//------------------------------------------------------------------------
// impStoreNullableFields: create a Nullable<T> object and store
// 'hasValue' (always true) and the given value for 'value' field
Expand Down Expand Up @@ -10098,36 +10209,20 @@ void Compiler::impImportBlockCode(BasicBlock* block)
op2 = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL);
op1 = gtNewOperNode(GT_ADD, TYP_BYREF, cloneOperand, op2);
}
else if (helper == CORINFO_HELP_UNBOX_NULLABLE)
{
// op1 is the object being unboxed
// op2 is either a class handle node or a runtime lookup node (it's fine to reorder)
op1 = impInlineUnboxNullable(resolvedToken.hClass, op2, op1);
}
else
{
// Don't optimize, just call the helper and be done with it
JITDUMP("\n Importing %s as helper call because %s\n", opcode == CEE_UNBOX ? "UNBOX" : "UNBOX.ANY",
canExpandInline ? "want smaller code or faster jitting" : "inline expansion not legal");

if (helper == CORINFO_HELP_UNBOX)
{
op1 = gtNewHelperCallNode(helper, TYP_BYREF, op2, op1);
}
else
{
assert(helper == CORINFO_HELP_UNBOX_NULLABLE);

// We're going to emit the following sequence of IR:
//
// Nullable<T> result;
// void CORINFO_HELP_UNBOX_NULLABLE(&result, unboxCls, obj);
// push result;
//
unsigned resultTmp = lvaGrabTemp(true DEBUGARG("Nullable<T> tmp"));
lvaSetStruct(resultTmp, resolvedToken.hClass, false);
lvaGetDesc(resultTmp)->lvHasLdAddrOp = true;

GenTreeLclFld* resultAddr = gtNewLclAddrNode(resultTmp, 0);
// NOTE: it's fine for op2 to be evaluated before op1
GenTreeCall* helperCall = gtNewHelperCallNode(helper, TYP_VOID, resultAddr, op2, op1);
impAppendTree(helperCall, CHECK_SPILL_ALL, impCurStmtDI);
op1 = gtNewLclvNode(resultTmp, TYP_STRUCT);
}
assert(helper == CORINFO_HELP_UNBOX);
op1 = gtNewHelperCallNode(helper, TYP_BYREF, op2, op1);
}

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