Skip to content

Commit f0dd9dd

Browse files
authored
JIT: escape analysis for delegates (#115172)
Mark delegate invoke calls as non-escaping for `this`. Later, in the stack allocation code rewriting post-pass, if the `this` at a delegate invoke is definitely a stack pointing local, expand the delegate invoke so that physical promotion can see all the accesses. Contributes to #104936.
1 parent 5d2444b commit f0dd9dd

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

src/coreclr/jit/objectalloc.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,6 +1869,14 @@ bool ObjectAllocator::CanLclVarEscapeViaParentStack(ArrayStack<GenTree*>* parent
18691869
break;
18701870
}
18711871
}
1872+
else if (call->IsDelegateInvoke())
1873+
{
1874+
if (tree == call->gtArgs.GetThisArg()->GetNode())
1875+
{
1876+
JITDUMP("Delegate invoke this...\n");
1877+
canLclVarEscapeViaParentStack = false;
1878+
}
1879+
}
18721880

18731881
// Note there is nothing special here about the parent being a call. We could move all this processing
18741882
// up to the caller and handle any sort of tree that could lead to escapes this way.
@@ -2219,6 +2227,7 @@ void ObjectAllocator::RewriteUses()
22192227
}
22202228
}
22212229
// Make box accesses explicit for UNBOX_HELPER
2230+
// Expand delegate invoke for calls where "this" is possibly stack pointing
22222231
//
22232232
else if (tree->IsCall())
22242233
{
@@ -2267,6 +2276,51 @@ void ObjectAllocator::RewriteUses()
22672276
}
22682277
}
22692278
}
2279+
else if (call->IsDelegateInvoke())
2280+
{
2281+
CallArg* const thisArg = call->gtArgs.GetThisArg();
2282+
GenTree* const delegateThis = thisArg->GetNode();
2283+
2284+
if (delegateThis->OperIs(GT_LCL_VAR))
2285+
{
2286+
GenTreeLclVarCommon* const lcl = delegateThis->AsLclVarCommon();
2287+
2288+
if (m_allocator->DoesLclVarPointToStack(lcl->GetLclNum()))
2289+
{
2290+
JITDUMP("Expanding delegate invoke [%06u]\n", m_compiler->dspTreeID(call));
2291+
// Expand the delgate invoke early, so that physical promotion has
2292+
// a chance to promote the delegate fields.
2293+
//
2294+
// Note the instance field may also be stack allocatable (someday)
2295+
//
2296+
GenTree* const cloneThis = m_compiler->gtClone(lcl);
2297+
unsigned const instanceOffset = m_compiler->eeGetEEInfo()->offsetOfDelegateInstance;
2298+
GenTree* const newThisAddr =
2299+
m_compiler->gtNewOperNode(GT_ADD, TYP_I_IMPL, cloneThis,
2300+
m_compiler->gtNewIconNode(instanceOffset, TYP_I_IMPL));
2301+
2302+
// For now assume the instance is heap...
2303+
//
2304+
GenTree* const newThis = m_compiler->gtNewIndir(TYP_REF, newThisAddr);
2305+
thisArg->SetEarlyNode(newThis);
2306+
2307+
// the control target is
2308+
// [originalThis + firstTgtOffs]
2309+
//
2310+
unsigned const targetOffset = m_compiler->eeGetEEInfo()->offsetOfDelegateFirstTarget;
2311+
GenTree* const targetAddr =
2312+
m_compiler->gtNewOperNode(GT_ADD, TYP_I_IMPL, lcl,
2313+
m_compiler->gtNewIconNode(targetOffset, TYP_I_IMPL));
2314+
GenTree* const target = m_compiler->gtNewIndir(TYP_I_IMPL, targetAddr);
2315+
2316+
// Update call state -- now an indirect call to the delegate target
2317+
//
2318+
call->gtCallAddr = target;
2319+
call->gtCallType = CT_INDIRECT;
2320+
call->gtCallMoreFlags &= ~(GTF_CALL_M_DELEGATE_INV | GTF_CALL_M_WRAPPER_DELEGATE_INV);
2321+
}
2322+
}
2323+
}
22702324
}
22712325
else if (tree->OperIsIndir())
22722326
{
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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.Reflection;
6+
using System.Diagnostics;
7+
using System.Runtime.CompilerServices;
8+
using Xunit;
9+
10+
enum AllocationKind
11+
{
12+
Heap,
13+
Stack,
14+
Undefined
15+
}
16+
17+
delegate int Test();
18+
19+
public class Delegates
20+
{
21+
static bool GCStressEnabled()
22+
{
23+
return Environment.GetEnvironmentVariable("DOTNET_GCStress") != null;
24+
}
25+
26+
static AllocationKind StackAllocation()
27+
{
28+
AllocationKind expectedAllocationKind = AllocationKind.Stack;
29+
if (GCStressEnabled())
30+
{
31+
Console.WriteLine("GCStress is enabled");
32+
expectedAllocationKind = AllocationKind.Undefined;
33+
}
34+
return expectedAllocationKind;
35+
}
36+
37+
static int CallTestAndVerifyAllocation(Test test, int expectedResult, AllocationKind expectedAllocationsKind, bool throws = false)
38+
{
39+
string methodName = test.Method.Name;
40+
try
41+
{
42+
long allocatedBytesBefore = GC.GetAllocatedBytesForCurrentThread();
43+
int testResult = test();
44+
long allocatedBytesAfter = GC.GetAllocatedBytesForCurrentThread();
45+
46+
if (testResult != expectedResult)
47+
{
48+
Console.WriteLine($"FAILURE ({methodName}): expected {expectedResult}, got {testResult}");
49+
return -1;
50+
}
51+
52+
if ((expectedAllocationsKind == AllocationKind.Stack) && (allocatedBytesBefore != allocatedBytesAfter))
53+
{
54+
Console.WriteLine($"FAILURE ({methodName}): unexpected allocation of {allocatedBytesAfter - allocatedBytesBefore} bytes");
55+
return -1;
56+
}
57+
58+
if ((expectedAllocationsKind == AllocationKind.Heap) && (allocatedBytesBefore == allocatedBytesAfter))
59+
{
60+
Console.WriteLine($"FAILURE ({methodName}): unexpected stack allocation");
61+
return -1;
62+
}
63+
else
64+
{
65+
Console.WriteLine($"SUCCESS ({methodName})");
66+
return 100;
67+
}
68+
}
69+
catch
70+
{
71+
if (throws)
72+
{
73+
Console.WriteLine($"SUCCESS ({methodName})");
74+
return 100;
75+
}
76+
else
77+
{
78+
return -1;
79+
}
80+
}
81+
}
82+
83+
[MethodImpl(MethodImplOptions.NoInlining)]
84+
static int DoTest0(int a)
85+
{
86+
var f = (int x) => x + 1;
87+
return f(a);
88+
}
89+
90+
[MethodImpl(MethodImplOptions.NoInlining)]
91+
static int RunTest0() => DoTest0(100);
92+
93+
[Fact]
94+
public static int Test0()
95+
{
96+
RunTest0();
97+
return CallTestAndVerifyAllocation(RunTest0, 101, StackAllocation());
98+
}
99+
100+
[MethodImpl(MethodImplOptions.NoInlining)]
101+
static int DoTest1(int[] a)
102+
{
103+
var f = (int x) => x + 1;
104+
int sum = 0;
105+
106+
foreach (int i in a)
107+
{
108+
sum += f(i);
109+
}
110+
111+
return sum;
112+
}
113+
114+
static int[] s_a = new int[100];
115+
116+
[MethodImpl(MethodImplOptions.NoInlining)]
117+
static int RunTest1() => DoTest1(s_a);
118+
119+
[Fact]
120+
public static int Test1()
121+
{
122+
RunTest1();
123+
return CallTestAndVerifyAllocation(RunTest1, 100, StackAllocation());
124+
}
125+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<!-- Needed for CLRTestEnvironmentVariable -->
4+
<RequiresProcessIsolation>true</RequiresProcessIsolation>
5+
<DebugType>None</DebugType>
6+
<Optimize>True</Optimize>
7+
<JitOptimizationSensitive>true</JitOptimizationSensitive>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<Compile Include="$(MSBuildProjectName).cs" />
11+
</ItemGroup>
12+
</Project>

0 commit comments

Comments
 (0)