Skip to content

Commit 80d3434

Browse files
hez2010jakobbotsch
andauthored
Stop spilling ldvirtftn (#120866)
This PR stops spilling the target for calls implemented via `ldvirtftn` in the importer, which is the way we implement GVMs. We were previously spilling the target with `CHECK_SPILL_NONE`, but this is incorrect because it induces a null check (see #121711). So this PR fixes that issue. Also, avoiding the spill gives us a simple uncontroversial way to devirtualize in some cases when we can extract information directly from `gtCallAddr`. The problem with removing the spill is code size regressions. `gtCallAddr` is normally evaluated after arguments, and the backend is not good at handling the ABI constraints of the arguments when `gtCallAddr` contains calls. To counteract that we introduce a new optimize in lowering. The optimization tries to move the evaluation of the target to happen before the arguments. It effectively accomplishes what the previous `CHECK_SPILL_NONE` spill was doing, except with the right legality checks. Contributes to #112596 Fixes #121711 --------- Co-authored-by: Jakob Botsch Nielsen <[email protected]>
1 parent 39ad112 commit 80d3434

File tree

6 files changed

+208
-9
lines changed

6 files changed

+208
-9
lines changed

src/coreclr/jit/compiler.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1608,7 +1608,7 @@ inline GenTreeCall* Compiler::gtNewHelperCallNode(
16081608
/*****************************************************************************/
16091609

16101610
//------------------------------------------------------------------------------
1611-
// gtNewHelperCallNode : Helper to create a call helper node.
1611+
// gtNewVirtualFunctionLookupHelperCallNode : Helper to create a virtual function lookup helper node.
16121612
//
16131613
//
16141614
// Arguments:

src/coreclr/jit/importercalls.cpp

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,16 @@ var_types Compiler::impImportCall(OPCODE opcode,
387387
{
388388
assert(!(mflags & CORINFO_FLG_STATIC)); // can't call a static method
389389
assert(!(clsFlags & CORINFO_FLG_VALUECLASS));
390+
391+
const bool needsFatPointerHandling =
392+
(sig->sigInst.methInstCount != 0) && IsTargetAbi(CORINFO_NATIVEAOT_ABI);
393+
if (needsFatPointerHandling)
394+
{
395+
// NativeAOT generic virtual method: need to handle potential fat function pointers
396+
// Spill any side-effecting arguments before we do the LDVIRTFTN
397+
impSpillSideEffects(false, CHECK_SPILL_ALL DEBUGARG("fat pointer arg spill"));
398+
}
399+
390400
// OK, We've been told to call via LDVIRTFTN, so just
391401
// take the call now....
392402
call = gtNewIndCallNode(nullptr, callRetTyp, di);
@@ -419,17 +429,14 @@ var_types Compiler::impImportCall(OPCODE opcode,
419429
->gtArgs.PushFront(this, NewCallArg::Primitive(thisPtrCopy).WellKnown(WellKnownArg::ThisPointer));
420430

421431
// Now make an indirect call through the function pointer
422-
423-
unsigned lclNum = lvaGrabTemp(true DEBUGARG("VirtualCall through function pointer"));
424-
impStoreToTemp(lclNum, fptr, CHECK_SPILL_ALL);
425-
fptr = gtNewLclvNode(lclNum, TYP_I_IMPL);
426-
427432
call->AsCall()->gtCallAddr = fptr;
428433
call->gtFlags |= GTF_EXCEPT | (fptr->gtFlags & GTF_GLOB_EFFECT);
429434

430-
if ((sig->sigInst.methInstCount != 0) && IsTargetAbi(CORINFO_NATIVEAOT_ABI))
435+
if (needsFatPointerHandling)
431436
{
432-
// NativeAOT generic virtual method: need to handle potential fat function pointers
437+
const unsigned fptrLclNum = lvaGrabTemp(true DEBUGARG("fat pointer temp"));
438+
impStoreToTemp(fptrLclNum, fptr, CHECK_SPILL_ALL);
439+
call->AsCall()->gtCallAddr = gtNewLclvNode(fptrLclNum, genActualType(fptr->TypeGet()));
433440
addFatPointerCandidate(call->AsCall());
434441
}
435442
#ifdef FEATURE_READYTORUN
@@ -6980,6 +6987,7 @@ class SpillRetExprHelper
69806987
void Compiler::addFatPointerCandidate(GenTreeCall* call)
69816988
{
69826989
JITDUMP("Marking call [%06u] as fat pointer candidate\n", dspTreeID(call));
6990+
69836991
setMethodHasFatPointer();
69846992
call->SetFatPointerCandidate();
69856993
SpillRetExprHelper helper(this);
@@ -8787,7 +8795,7 @@ void Compiler::impDevirtualizeCall(GenTreeCall* call,
87878795

87888796
if (dvInfo.isInstantiatingStub)
87898797
{
8790-
// We should only end up with generic methods for array interface devirt.
8798+
// We should only end up with generic methods that needs a method context (eg. array interface).
87918799
//
87928800
assert(dvInfo.wasArrayInterfaceDevirt);
87938801

src/coreclr/jit/lower.cpp

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2833,6 +2833,14 @@ GenTree* Lowering::LowerCall(GenTree* node)
28332833
#endif
28342834

28352835
call->ClearOtherRegs();
2836+
2837+
#if HAS_FIXED_REGISTER_SET
2838+
if ((call->gtCallType == CT_INDIRECT) && comp->opts.Tier0OptimizationEnabled())
2839+
{
2840+
OptimizeCallIndirectTargetEvaluation(call);
2841+
}
2842+
#endif
2843+
28362844
LowerArgsForCall(call);
28372845

28382846
// note that everything generated from this point might run AFTER the outgoing args are placed
@@ -6286,6 +6294,137 @@ GenTree* Lowering::LowerDelegateInvoke(GenTreeCall* call)
62866294
return callTarget;
62876295
}
62886296

6297+
//------------------------------------------------------------------------
6298+
// OptimizeCallIndirectTargetEvaluation:
6299+
// Try to optimize the evaluation of the indirect call target to happen
6300+
// before arguments, if possible.
6301+
//
6302+
// Parameters:
6303+
// call - Call node
6304+
//
6305+
void Lowering::OptimizeCallIndirectTargetEvaluation(GenTreeCall* call)
6306+
{
6307+
assert((call->gtCallType == CT_INDIRECT) && (call->gtCallAddr != nullptr));
6308+
6309+
if (!call->gtCallAddr->IsHelperCall(comp, CORINFO_HELP_VIRTUAL_FUNC_PTR) &&
6310+
!call->gtCallAddr->IsHelperCall(comp, CORINFO_HELP_READYTORUN_VIRTUAL_FUNC_PTR) &&
6311+
!call->gtCallAddr->IsHelperCall(comp, CORINFO_HELP_GVMLOOKUP_FOR_SLOT) &&
6312+
!call->gtCallAddr->IsHelperCall(comp, CORINFO_HELP_READYTORUN_GENERIC_HANDLE))
6313+
{
6314+
return;
6315+
}
6316+
6317+
JITDUMP("Target is a GVM; seeing if we can move arguments ahead of resolution\n");
6318+
6319+
m_scratchSideEffects.Clear();
6320+
6321+
// We start at the call and move backwards from it. When we see a node that
6322+
// is part of the call's data flow we leave it in place. For nodes that are
6323+
// not part of the data flow, or that are part of the target's data flow,
6324+
// we move them before the call's data flow if legal. We stop when we run
6325+
// out of the call's data flow.
6326+
//
6327+
// The end result is that all nodes outside the call's data flow or inside
6328+
// the target's data flow are computed before call arguments, allowing LSRA
6329+
// to resolve the ABI constraints without interfering with the target
6330+
// computation.
6331+
unsigned numMarked = 1;
6332+
call->gtLIRFlags |= LIR::Flags::Mark;
6333+
6334+
LIR::ReadOnlyRange movingRange;
6335+
6336+
GenTree* prev;
6337+
for (GenTree* cur = call; numMarked > 0; cur = prev)
6338+
{
6339+
prev = cur->gtPrev;
6340+
6341+
if ((cur->gtLIRFlags & LIR::Flags::Mark) == 0)
6342+
{
6343+
// If we are still moving nodes then extend the range so that we
6344+
// also move this node outside the data flow of the call.
6345+
if (!movingRange.IsEmpty())
6346+
{
6347+
assert(cur->gtNext == movingRange.FirstNode());
6348+
movingRange = LIR::ReadOnlyRange(cur, movingRange.LastNode());
6349+
m_scratchSideEffects.AddNode(comp, cur);
6350+
}
6351+
6352+
continue;
6353+
}
6354+
6355+
cur->gtLIRFlags &= ~LIR::Flags::Mark;
6356+
numMarked--;
6357+
6358+
if (cur == call->gtCallAddr)
6359+
{
6360+
// Start moving this range. Do not add its side effects as we will
6361+
// check the NRE manually for precision.
6362+
movingRange = LIR::ReadOnlyRange(cur, cur);
6363+
continue;
6364+
}
6365+
6366+
cur->VisitOperands([&](GenTree* op) {
6367+
assert((op->gtLIRFlags & LIR::Flags::Mark) == 0);
6368+
op->gtLIRFlags |= LIR::Flags::Mark;
6369+
numMarked++;
6370+
return GenTree::VisitResult::Continue;
6371+
});
6372+
6373+
if (!movingRange.IsEmpty())
6374+
{
6375+
// This node is in the dataflow. See if we can move it ahead of the
6376+
// range we are moving.
6377+
bool interferes = false;
6378+
if (m_scratchSideEffects.InterferesWith(comp, cur, /* strict */ true))
6379+
{
6380+
JITDUMP(" Stopping at [%06u]; it interferes with the current range we are moving\n",
6381+
Compiler::dspTreeID(cur));
6382+
interferes = true;
6383+
}
6384+
6385+
if (!interferes)
6386+
{
6387+
// No problem so far. However the side effect set does not
6388+
// include the GVM call itself, which can throw NRE. Check the
6389+
// NRE now for precision.
6390+
GenTreeFlags flags = cur->OperEffects(comp);
6391+
if ((flags & GTF_PERSISTENT_SIDE_EFFECTS) != 0)
6392+
{
6393+
JITDUMP(" Stopping at [%06u]; it has persistent side effects\n", Compiler::dspTreeID(cur));
6394+
interferes = true;
6395+
}
6396+
else if ((flags & GTF_EXCEPT) != 0)
6397+
{
6398+
ExceptionSetFlags preciseExceptions = cur->OperExceptions(comp);
6399+
if (preciseExceptions != ExceptionSetFlags::NullReferenceException)
6400+
{
6401+
JITDUMP(" Stopping at [%06u]; it throws an exception that is not NRE\n",
6402+
Compiler::dspTreeID(cur));
6403+
interferes = true;
6404+
}
6405+
}
6406+
}
6407+
6408+
if (interferes)
6409+
{
6410+
// Stop moving the range, but keep going through the rest
6411+
// of the nodes to unmark them
6412+
movingRange = LIR::ReadOnlyRange();
6413+
}
6414+
else
6415+
{
6416+
// Move 'cur' ahead of 'movingRange'
6417+
assert(cur->gtNext == movingRange.FirstNode());
6418+
BlockRange().Remove(cur);
6419+
BlockRange().InsertAfter(movingRange.LastNode(), cur);
6420+
}
6421+
}
6422+
}
6423+
6424+
JITDUMP("Result of moved target evaluation:\n");
6425+
DISPTREERANGE(BlockRange(), call);
6426+
}
6427+
62896428
GenTree* Lowering::LowerIndirectNonvirtCall(GenTreeCall* call)
62906429
{
62916430
#ifdef TARGET_X86

src/coreclr/jit/lower.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ class Lowering final : public Phase
196196
GenTreeLclVar* SpillStructCallResult(GenTreeCall* call) const;
197197
#endif // WINDOWS_AMD64_ABI
198198
GenTree* LowerDelegateInvoke(GenTreeCall* call);
199+
void OptimizeCallIndirectTargetEvaluation(GenTreeCall* call);
199200
GenTree* LowerIndirectNonvirtCall(GenTreeCall* call);
200201
GenTree* LowerDirectCall(GenTreeCall* call);
201202
GenTree* LowerNonvirtPinvokeCall(GenTreeCall* call);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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.Runtime.CompilerServices;
6+
using Xunit;
7+
8+
public class Runtime_121711
9+
{
10+
[Fact]
11+
public static int TestEntryPoint()
12+
{
13+
try
14+
{
15+
Test(null);
16+
return 101;
17+
}
18+
catch (NullReferenceException)
19+
{
20+
return _exitCode;
21+
}
22+
}
23+
24+
[MethodImpl(MethodImplOptions.NoInlining)]
25+
private static void Test(Base b)
26+
{
27+
b.Foo<string>(Bar());
28+
}
29+
30+
[MethodImpl(MethodImplOptions.NoInlining)]
31+
private static int Bar()
32+
{
33+
_exitCode = 100;
34+
return 42;
35+
}
36+
37+
private static int _exitCode = 102;
38+
}
39+
40+
public abstract class Base
41+
{
42+
public abstract void Foo<T>(int x);
43+
}
44+
45+
public class Derived : Base
46+
{
47+
public override void Foo<T>(int x)
48+
{
49+
}
50+
}

src/tests/JIT/Regression/Regression_ro_1.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
<Compile Include="JitBlue\Runtime_120270\Runtime_120270.cs" />
7575
<Compile Include="JitBlue\Runtime_120414\Runtime_120414.cs" />
7676
<Compile Include="JitBlue\Runtime_120522\Runtime_120522.cs" />
77+
<Compile Include="JitBlue\Runtime_121711\Runtime_121711.cs" />
7778
<Compile Include="JitBlue\Runtime_31615\Runtime_31615.cs" />
7879
<Compile Include="JitBlue\Runtime_33884\Runtime_33884.cs" />
7980
<Compile Include="JitBlue\Runtime_38920\Runtime_38920.cs" />

0 commit comments

Comments
 (0)