Skip to content

Commit 3206a8e

Browse files
AndyAyersMSCopilot
andauthored
JIT: don't make local copy when devirtualizing boxed interface call (#128270)
When devirtualizing an interface call on a GT_BOX(local), the importer would replace the heap box with a stack-local copy and invoke the unboxed entry on that copy. This is unsafe: if the callee returns an interior managed pointer (for instance via [UnscopedRef]), the returned byref aliases the temporary copy and dangles once the enclosing frame goes away. Always keep the heap box and rewrite the call to invoke the unboxed entry on box + sizeof(MT). Object stack allocation already promotes non-escaping heap boxes to the stack, so we recover the previous optimization in the safe cases. The BR_MAKE_LOCAL_COPY removal option and its implementation in gtTryRemoveBoxUpstreamEffects are no longer reachable and are removed. Fixes #122099. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8400ed3 commit 3206a8e

6 files changed

Lines changed: 114 additions & 224 deletions

File tree

src/coreclr/jit/compiler.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3974,7 +3974,6 @@ class Compiler
39743974
BR_REMOVE_BUT_NOT_NARROW, // remove effects, return original source tree
39753975
BR_DONT_REMOVE, // check if removal is possible, return copy source tree
39763976
BR_DONT_REMOVE_WANT_TYPE_HANDLE, // check if removal is possible, return type handle tree
3977-
BR_MAKE_LOCAL_COPY // revise box to copy to temp local and return local's address
39783977
};
39793978

39803979
GenTree* gtTryRemoveBoxUpstreamEffects(GenTree* tree, BoxRemovalOptions options = BR_REMOVE_AND_NARROW);

src/coreclr/jit/gentree.cpp

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16536,10 +16536,9 @@ GenTree* Compiler::gtTryRemoveBoxUpstreamEffects(GenTree* op, BoxRemovalOptions
1653616536
Statement* allocStmt = box->gtDefStmtWhenInlinedBoxValue;
1653716537
Statement* copyStmt = box->gtCopyStmtWhenInlinedBoxValue;
1653816538

16539-
JITDUMP("gtTryRemoveBoxUpstreamEffects: %s to %s of BOX (valuetype)"
16539+
JITDUMP("gtTryRemoveBoxUpstreamEffects: %s of BOX (valuetype)"
1654016540
" [%06u] (assign/newobj " FMT_STMT " copy " FMT_STMT "\n",
16541-
(options == BR_DONT_REMOVE) ? "checking if it is possible" : "attempting",
16542-
(options == BR_MAKE_LOCAL_COPY) ? "make local unboxed version" : "remove side effects", dspTreeID(op),
16541+
(options == BR_DONT_REMOVE) ? "checking if it is possible" : "attempting", dspTreeID(op),
1654316542
allocStmt->GetID(), copyStmt->GetID());
1654416543

1654516544
// If we don't recognize the form of the store, bail.
@@ -16615,65 +16614,6 @@ GenTree* Compiler::gtTryRemoveBoxUpstreamEffects(GenTree* op, BoxRemovalOptions
1661516614
return nullptr;
1661616615
}
1661716616

16618-
// Handle case where we are optimizing the box into a local copy
16619-
if (options == BR_MAKE_LOCAL_COPY)
16620-
{
16621-
// Drill into the box to get at the box temp local and the box type
16622-
GenTree* boxTemp = box->BoxOp();
16623-
assert(boxTemp->IsLocal());
16624-
const unsigned boxTempLcl = boxTemp->AsLclVar()->GetLclNum();
16625-
assert(lvaTable[boxTempLcl].lvType == TYP_REF);
16626-
CORINFO_CLASS_HANDLE boxClass = lvaTable[boxTempLcl].lvClassHnd;
16627-
assert(boxClass != nullptr);
16628-
16629-
// Verify that the copy has the expected shape
16630-
// (store_blk|store_ind (add (boxTempLcl, ptr-size)))
16631-
//
16632-
// The shape here is constrained to the patterns we produce
16633-
// over in impImportAndPushBox for the inlined box case.
16634-
//
16635-
GenTree* copyDstAddr = copy->AsIndir()->Addr();
16636-
if (!copyDstAddr->OperIs(GT_ADD))
16637-
{
16638-
JITDUMP("Unexpected copy dest address tree\n");
16639-
return nullptr;
16640-
}
16641-
16642-
GenTree* copyDstAddrOp1 = copyDstAddr->AsOp()->gtOp1;
16643-
if (!copyDstAddrOp1->OperIs(GT_LCL_VAR) || (copyDstAddrOp1->AsLclVarCommon()->GetLclNum() != boxTempLcl))
16644-
{
16645-
JITDUMP("Unexpected copy dest address 1st addend\n");
16646-
return nullptr;
16647-
}
16648-
16649-
GenTree* copyDstAddrOp2 = copyDstAddr->AsOp()->gtOp2;
16650-
if (!copyDstAddrOp2->IsIntegralConst(TARGET_POINTER_SIZE))
16651-
{
16652-
JITDUMP("Unexpected copy dest address 2nd addend\n");
16653-
return nullptr;
16654-
}
16655-
16656-
// Screening checks have all passed. Do the transformation.
16657-
//
16658-
// Retype the box temp to be a struct
16659-
JITDUMP("Retyping box temp V%02u to struct %s\n", boxTempLcl, eeGetClassName(boxClass));
16660-
lvaTable[boxTempLcl].lvType = TYP_UNDEF;
16661-
const bool isUnsafeValueClass = false;
16662-
lvaSetStruct(boxTempLcl, boxClass, isUnsafeValueClass);
16663-
16664-
// Remove the newobj and store to box temp
16665-
JITDUMP("Bashing NEWOBJ [%06u] to NOP\n", dspTreeID(boxLclDef));
16666-
boxLclDef->gtBashToNOP();
16667-
16668-
// Update the copy from the value to be boxed to the box temp
16669-
copy->AsIndir()->Addr() = gtNewLclVarAddrNode(boxTempLcl, TYP_BYREF);
16670-
16671-
// Return the address of the now-struct typed box temp
16672-
GenTree* retValue = gtNewLclVarAddrNode(boxTempLcl, TYP_BYREF);
16673-
16674-
return retValue;
16675-
}
16676-
1667716617
// If the copy is a struct copy, make sure we know how to isolate any source side effects.
1667816618
GenTree* copySrc = copy->Data();
1667916619

src/coreclr/jit/importercalls.cpp

Lines changed: 38 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -9284,189 +9284,67 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call,
92849284

92859285
if (unboxedEntryMethod != nullptr)
92869286
{
9287-
bool optimizedTheBox = false;
9288-
9289-
// If the 'this' object is a local box, see if we can revise things
9290-
// to not require boxing.
9287+
// Rewrite the call to target the unboxed entry on the box payload. Keep the heap box,
9288+
// since the callee may return an interior managed pointer into it; object stack allocation
9289+
// can later promote the box to the stack when escape analysis proves the receiver does not
9290+
// escape.
92919291
//
9292-
if (thisObj->IsBoxedValue() && !isExplicitTailCall)
9292+
if (requiresInstMethodTableArg)
92939293
{
9294-
// Since the call is the only consumer of the box, we know the box can't escape
9295-
// since it is being passed an interior pointer.
9296-
//
9297-
// So, revise the box to simply create a local copy, use the address of that copy
9298-
// as the this pointer, and update the entry point to the unboxed entry.
9294+
// Get the method table from the boxed object.
92999295
//
9300-
// Ideally, we then inline the boxed method and and if it turns out not to modify
9301-
// the copy, we can undo the copy too.
9302-
GenTree* localCopyThis = nullptr;
9296+
// TODO-CallArgs-REVIEW: Use thisObj here? Differs by gtEffectiveVal.
9297+
GenTree* const clonedThisArg = gtClone(thisArg->GetEarlyNode());
93039298

9304-
if (requiresInstMethodTableArg)
9299+
if (clonedThisArg == nullptr)
93059300
{
9306-
// Perform a trial box removal and ask for the type handle tree that fed the box.
9307-
//
9308-
JITDUMP("Unboxed entry needs method table arg...\n");
9309-
GenTree* methodTableArg =
9310-
gtTryRemoveBoxUpstreamEffects(thisObj, BR_DONT_REMOVE_WANT_TYPE_HANDLE);
9311-
9312-
if (methodTableArg != nullptr)
9313-
{
9314-
// If that worked, turn the box into a copy to a local var
9315-
//
9316-
JITDUMP("Found suitable method table arg tree [%06u]\n", dspTreeID(methodTableArg));
9317-
localCopyThis = gtTryRemoveBoxUpstreamEffects(thisObj, BR_MAKE_LOCAL_COPY);
9318-
9319-
if (localCopyThis != nullptr)
9320-
{
9321-
// Pass the local var as this and the type handle as a new arg
9322-
//
9323-
JITDUMP("Success! invoking unboxed entry point on local copy, and passing method table "
9324-
"arg\n");
9325-
// TODO-CallArgs-REVIEW: Might discard commas otherwise?
9326-
assert(thisObj == thisArg->GetEarlyNode());
9327-
thisArg->SetEarlyNode(localCopyThis);
9328-
INDEBUG(call->gtCallDebugFlags |= GTF_CALL_MD_UNBOXED);
9329-
9330-
call->gtArgs.InsertInstParam(this, methodTableArg);
9331-
9332-
call->gtCallMethHnd = unboxedEntryMethod;
9333-
derivedMethod = unboxedEntryMethod;
9334-
pDerivedResolvedToken = &dvInfo.resolvedTokenDevirtualizedUnboxedMethod;
9335-
9336-
// Method attributes will differ because unboxed entry point is shared
9337-
//
9338-
const DWORD unboxedMethodAttribs =
9339-
info.compCompHnd->getMethodAttribs(unboxedEntryMethod);
9340-
JITDUMP("Updating method attribs from 0x%08x to 0x%08x\n", derivedMethodAttribs,
9341-
unboxedMethodAttribs);
9342-
derivedMethodAttribs = unboxedMethodAttribs;
9343-
optimizedTheBox = true;
9344-
}
9345-
else
9346-
{
9347-
JITDUMP("Sorry, failed to undo the box -- can't convert to local copy\n");
9348-
}
9349-
}
9350-
else
9351-
{
9352-
JITDUMP("Sorry, failed to undo the box -- can't find method table arg\n");
9353-
}
9301+
JITDUMP("unboxed entry needs MT arg, but `this` was too complex to clone. Deferring update.\n");
93549302
}
93559303
else
93569304
{
9357-
JITDUMP("Found unboxed entry point, trying to simplify box to a local copy\n");
9358-
localCopyThis = gtTryRemoveBoxUpstreamEffects(thisObj, BR_MAKE_LOCAL_COPY);
9359-
9360-
if (localCopyThis != nullptr)
9361-
{
9362-
JITDUMP("Success! invoking unboxed entry point on local copy\n");
9363-
assert(thisObj == thisArg->GetEarlyNode());
9364-
// TODO-CallArgs-REVIEW: Might discard commas otherwise?
9365-
thisArg->SetEarlyNode(localCopyThis);
9366-
call->gtCallMethHnd = unboxedEntryMethod;
9367-
INDEBUG(call->gtCallDebugFlags |= GTF_CALL_MD_UNBOXED);
9368-
derivedMethod = unboxedEntryMethod;
9369-
pDerivedResolvedToken = &dvInfo.resolvedTokenDevirtualizedUnboxedMethod;
9370-
9371-
optimizedTheBox = true;
9372-
}
9373-
else
9374-
{
9375-
JITDUMP("Sorry, failed to undo the box\n");
9376-
}
9377-
}
9378-
9379-
if (optimizedTheBox)
9380-
{
9381-
assert(localCopyThis->IsLclVarAddr());
9382-
9383-
// We may end up inlining this call, so the local copy must be marked as "aliased",
9384-
// making sure the inlinee importer will know when to spill references to its value.
9385-
lvaGetDesc(localCopyThis->AsLclFld())->lvHasLdAddrOp = true;
9386-
Metrics.DevirtualizedCallRemovedBox++;
9387-
Metrics.DevirtualizedCallUnboxedEntry++;
9388-
9389-
#if FEATURE_TAILCALL_OPT
9390-
if (call->IsImplicitTailCall())
9391-
{
9392-
JITDUMP("Clearing the implicit tail call flag\n");
9305+
JITDUMP("revising call to invoke unboxed entry with additional method table arg\n");
93939306

9394-
// If set, we clear the implicit tail call flag
9395-
// as we just introduced a new address taken local variable
9396-
//
9397-
call->gtCallMoreFlags &= ~GTF_CALL_M_IMPLICIT_TAILCALL;
9398-
}
9399-
#endif // FEATURE_TAILCALL_OPT
9400-
}
9401-
}
9307+
GenTree* const methodTableArg = gtNewMethodTableLookup(clonedThisArg);
94029308

9403-
if (!optimizedTheBox)
9404-
{
9405-
// If we get here, we have a boxed value class that either wasn't boxed
9406-
// locally, or was boxed locally but we were unable to remove the box for
9407-
// various reasons.
9408-
//
9409-
// We can still update the call to invoke the unboxed entry, if the
9410-
// boxed value is simple.
9411-
//
9412-
if (requiresInstMethodTableArg)
9413-
{
9414-
// Get the method table from the boxed object.
9309+
// Update the 'this' pointer to refer to the box payload
94159310
//
9416-
// TODO-CallArgs-REVIEW: Use thisObj here? Differs by gtEffectiveVal.
9417-
GenTree* const clonedThisArg = gtClone(thisArg->GetEarlyNode());
9418-
9419-
if (clonedThisArg == nullptr)
9420-
{
9421-
JITDUMP(
9422-
"unboxed entry needs MT arg, but `this` was too complex to clone. Deferring update.\n");
9423-
}
9424-
else
9425-
{
9426-
JITDUMP("revising call to invoke unboxed entry with additional method table arg\n");
9427-
9428-
GenTree* const methodTableArg = gtNewMethodTableLookup(clonedThisArg);
9429-
9430-
// Update the 'this' pointer to refer to the box payload
9431-
//
9432-
GenTree* const payloadOffset = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL);
9433-
GenTree* const boxPayload =
9434-
gtNewOperNode(GT_ADD, TYP_BYREF, thisArg->GetEarlyNode(), payloadOffset);
9435-
9436-
assert(thisObj == thisArg->GetEarlyNode());
9437-
thisArg->SetEarlyNode(boxPayload);
9438-
call->gtCallMethHnd = unboxedEntryMethod;
9439-
INDEBUG(call->gtCallDebugFlags |= GTF_CALL_MD_UNBOXED);
9440-
9441-
// Method attributes will differ because unboxed entry point is shared
9442-
//
9443-
const DWORD unboxedMethodAttribs = info.compCompHnd->getMethodAttribs(unboxedEntryMethod);
9444-
JITDUMP("Updating method attribs from 0x%08x to 0x%08x\n", derivedMethodAttribs,
9445-
unboxedMethodAttribs);
9446-
derivedMethod = unboxedEntryMethod;
9447-
pDerivedResolvedToken = &dvInfo.resolvedTokenDevirtualizedUnboxedMethod;
9448-
derivedMethodAttribs = unboxedMethodAttribs;
9449-
9450-
call->gtArgs.InsertInstParam(this, methodTableArg);
9451-
Metrics.DevirtualizedCallUnboxedEntry++;
9452-
}
9453-
}
9454-
else
9455-
{
9456-
JITDUMP("revising call to invoke unboxed entry\n");
9457-
94589311
GenTree* const payloadOffset = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL);
94599312
GenTree* const boxPayload =
94609313
gtNewOperNode(GT_ADD, TYP_BYREF, thisArg->GetEarlyNode(), payloadOffset);
94619314

9315+
assert(thisObj == thisArg->GetEarlyNode());
94629316
thisArg->SetEarlyNode(boxPayload);
94639317
call->gtCallMethHnd = unboxedEntryMethod;
94649318
INDEBUG(call->gtCallDebugFlags |= GTF_CALL_MD_UNBOXED);
9319+
9320+
// Method attributes will differ because unboxed entry point is shared
9321+
//
9322+
const DWORD unboxedMethodAttribs = info.compCompHnd->getMethodAttribs(unboxedEntryMethod);
9323+
JITDUMP("Updating method attribs from 0x%08x to 0x%08x\n", derivedMethodAttribs,
9324+
unboxedMethodAttribs);
94659325
derivedMethod = unboxedEntryMethod;
94669326
pDerivedResolvedToken = &dvInfo.resolvedTokenDevirtualizedUnboxedMethod;
9327+
derivedMethodAttribs = unboxedMethodAttribs;
9328+
9329+
call->gtArgs.InsertInstParam(this, methodTableArg);
94679330
Metrics.DevirtualizedCallUnboxedEntry++;
94689331
}
94699332
}
9333+
else
9334+
{
9335+
JITDUMP("revising call to invoke unboxed entry\n");
9336+
9337+
GenTree* const payloadOffset = gtNewIconNode(TARGET_POINTER_SIZE, TYP_I_IMPL);
9338+
GenTree* const boxPayload =
9339+
gtNewOperNode(GT_ADD, TYP_BYREF, thisArg->GetEarlyNode(), payloadOffset);
9340+
9341+
thisArg->SetEarlyNode(boxPayload);
9342+
call->gtCallMethHnd = unboxedEntryMethod;
9343+
INDEBUG(call->gtCallDebugFlags |= GTF_CALL_MD_UNBOXED);
9344+
derivedMethod = unboxedEntryMethod;
9345+
pDerivedResolvedToken = &dvInfo.resolvedTokenDevirtualizedUnboxedMethod;
9346+
Metrics.DevirtualizedCallUnboxedEntry++;
9347+
}
94709348
}
94719349
else
94729350
{

src/coreclr/jit/jitmetadatalist.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ JITMETADATAMETRIC(ImporterBranchFold, int, 0)
5757
JITMETADATAMETRIC(ImporterSwitchFold, int, 0)
5858
JITMETADATAMETRIC(DevirtualizedCall, int, 0)
5959
JITMETADATAMETRIC(DevirtualizedCallUnboxedEntry, int, 0)
60-
JITMETADATAMETRIC(DevirtualizedCallRemovedBox, int, 0)
6160
JITMETADATAMETRIC(GDV, int, 0)
6261
JITMETADATAMETRIC(ClassGDV, int, 0)
6362
JITMETADATAMETRIC(MethodGDV, int, 0)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Runtime.CompilerServices;
7+
using Xunit;
8+
using TestLibrary;
9+
10+
namespace Runtime_122099;
11+
12+
public class Runtime_122099
13+
{
14+
// Verifies that an interface call on a boxed value type whose target returns an
15+
// `[UnscopedRef]` ref to an instance field keeps the boxed receiver alive long
16+
// enough for the returned ref to remain valid after the call returns.
17+
[ActiveIssue("https://github.com/dotnet/runtime/issues/122099", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoRuntime))]
18+
[Fact]
19+
public static int TestEntryPoint()
20+
{
21+
ref int r = ref EscapeRef();
22+
r = 42;
23+
24+
// Reuse the same stack region the callees occupied.
25+
Clobber();
26+
27+
return r == 42 ? 100 : 101;
28+
}
29+
30+
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
31+
private static ref int EscapeRef()
32+
{
33+
Struct1 s = new Struct1 { Field = 7 };
34+
return ref BoxedRefEscape(s);
35+
}
36+
37+
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
38+
private static ref int BoxedRefEscape(Struct1 x)
39+
{
40+
return ref ((I1)x).Method1();
41+
}
42+
43+
[MethodImpl(MethodImplOptions.NoInlining)]
44+
private static void Clobber()
45+
{
46+
Span<int> spam = stackalloc int[64];
47+
for (int i = 0; i < spam.Length; i++)
48+
{
49+
spam[i] = unchecked((int)0xDEADBEEF);
50+
}
51+
// Prevent the stackalloc from being elided.
52+
if (spam[0] != unchecked((int)0xDEADBEEF))
53+
{
54+
throw new Exception();
55+
}
56+
}
57+
}
58+
59+
public interface I1
60+
{
61+
[UnscopedRef]
62+
ref int Method1();
63+
}
64+
65+
public struct Struct1 : I1
66+
{
67+
public int Field;
68+
69+
[UnscopedRef]
70+
[MethodImpl(MethodImplOptions.NoInlining)]
71+
public ref int Method1() => ref Field;
72+
}
73+

0 commit comments

Comments
 (0)