Skip to content

Commit 61126df

Browse files
authored
JIT: fix issue in conditional escape analysis (#118214)
Conditional escape analysis is triggered by a GDV guarded call to `GetEnumerator`. On the fast path this call is devirtualized and inlined and exposes one or more enumerator object allocations. The "more" allows for collections to do special-case optimizations like returning a static enumerator object for an empty collection. When there is more than one enumerator object allocation under the GDV guard, ensure that only the allocation that was analyzed for conditional escape is stack allocated; the remaining enumerators will end up on the "slow path" where enumeration happens via interface calls, and so must be heap objects. If we clone an allocation site the allocation block won't be in the DFS tree. Such sites will not be in an enumerator GDV hammock so can be exempted from checking. Fixes #111922.
1 parent c37e685 commit 61126df

File tree

4 files changed

+140
-1
lines changed

4 files changed

+140
-1
lines changed

src/coreclr/jit/objectalloc.cpp

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1324,7 +1324,8 @@ void ObjectAllocator::MorphAllocObjNode(AllocationCandidate& candidate)
13241324
else
13251325
{
13261326
assert(candidate.m_onHeapReason != nullptr);
1327-
JITDUMP("Allocating V%02u on the heap: %s\n", lclNum, candidate.m_onHeapReason);
1327+
JITDUMP("Allocating V%02u / [%06u] on the heap: %s\n", lclNum, comp->dspTreeID(candidate.m_tree),
1328+
candidate.m_onHeapReason);
13281329
if ((candidate.m_allocType == OAT_NEWOBJ) || (candidate.m_allocType == OAT_NEWOBJ_HEAP))
13291330
{
13301331
GenTree* const stmtExpr = candidate.m_tree;
@@ -1367,6 +1368,58 @@ bool ObjectAllocator::MorphAllocObjNodeHelper(AllocationCandidate& candidate)
13671368
return false;
13681369
}
13691370

1371+
// If we have done any cloning for conditional escape, verify this allocation
1372+
// is not on one of the "slow paths" that are now forcibly making interface calls.
1373+
//
1374+
for (CloneInfo* const c : CloneMap::ValueIteration(&m_CloneMap))
1375+
{
1376+
if (!c->m_willClone)
1377+
{
1378+
// We didn't clone so there is no check needed.
1379+
//
1380+
continue;
1381+
}
1382+
1383+
if (c->m_guardBlock == nullptr)
1384+
{
1385+
// This clone wasn't guarded by a GDV, so we should
1386+
// only have a fast path.
1387+
continue;
1388+
}
1389+
1390+
GenTree* const allocTree = candidate.m_tree->AsLclVar()->Data();
1391+
1392+
if (c->m_allocTree == allocTree)
1393+
{
1394+
// This is the conditionally escaping allocation, so it's
1395+
// on the fast path.
1396+
//
1397+
continue;
1398+
}
1399+
1400+
if (!comp->m_dfsTree->Contains(candidate.m_block))
1401+
{
1402+
// If the def block is not in the DFS tree then the allocation
1403+
// is not within the hammock, so it is not on the slow path.
1404+
//
1405+
continue;
1406+
}
1407+
1408+
// This allocation candidate might be on the "slow path". Check.
1409+
//
1410+
// c->m_guardBlock and c->m_defBlock form a GDV hammock.
1411+
//
1412+
// So if an allocation site is dominated by c->m_guardBlock and
1413+
// not dominated by c->m_defBlock, it is within the hammock.
1414+
//
1415+
if (comp->m_domTree->Dominates(c->m_guardBlock, candidate.m_block) &&
1416+
!comp->m_domTree->Dominates(c->m_defBlock, candidate.m_block))
1417+
{
1418+
candidate.m_onHeapReason = "[on slow path of conditional escape clone]";
1419+
return false;
1420+
}
1421+
}
1422+
13701423
switch (candidate.m_allocType)
13711424
{
13721425
case OAT_NEWARR:
@@ -3772,6 +3825,8 @@ bool ObjectAllocator::CheckCanClone(CloneInfo* info)
37723825
JITDUMP("V%02u has single def in " FMT_BB " at [%06u]\n", info->m_local, defBlock->bbNum,
37733826
comp->dspTreeID(defStmt->GetRootNode()));
37743827

3828+
info->m_defBlock = defBlock;
3829+
37753830
// We expect to be able to follow all paths from alloc block to defBlock
37763831
// without reaching "beyond" defBlock.
37773832
//
@@ -3861,6 +3916,35 @@ bool ObjectAllocator::CheckCanClone(CloneInfo* info)
38613916
JITDUMP("Unexpected, alloc site " FMT_BB " dominates def block " FMT_BB ". We will clone anyways.\n",
38623917
allocBlock->bbNum, defBlock->bbNum);
38633918
}
3919+
else
3920+
{
3921+
// The allocation is conditional. See if we can find the GDV guard
3922+
// so we can check what other possible stack allocations might reach
3923+
// the def block.
3924+
//
3925+
BasicBlock* possibleGuardBlock = defBlock->bbIDom;
3926+
GuardInfo gi;
3927+
if (IsGuard(possibleGuardBlock, &gi))
3928+
{
3929+
// Todo: validate guard is the right guard.
3930+
//
3931+
JITDUMP("Conditional allocation is guarded by a GDV\n");
3932+
info->m_guardBlock = possibleGuardBlock;
3933+
}
3934+
else
3935+
{
3936+
// The allocation is conditional but the enumerator var def is
3937+
// not dominated by a GDV. Perhaps we resolved the GDV already.
3938+
//
3939+
// We want to allow cloning to proceed in this case as alternative
3940+
// enumerators might be non-allocating (say the static empty array
3941+
// enumerator).
3942+
//
3943+
// But consider adding more checking in this case.
3944+
//
3945+
JITDUMP("Conditional allocation is not guarded by a GDV\n");
3946+
}
3947+
}
38643948

38653949
// Classify the other local appearances
38663950
// as Ts (allocTemps) or Us (useTemps), and look for guard appearances.

src/coreclr/jit/objectalloc.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ struct CloneInfo : public GuardInfo
8989
unsigned m_appearanceCount = 0;
9090
jitstd::vector<unsigned>* m_allocTemps = nullptr;
9191

92+
// The initial GDV guard block, if any.
93+
BasicBlock* m_guardBlock = nullptr;
94+
95+
// The block where the enumerator var is assigned.
96+
BasicBlock* m_defBlock = nullptr;
97+
9298
// Where the enumerator allocation happens
9399
GenTree* m_allocTree = nullptr;
94100
Statement* m_allocStmt = nullptr;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Runtime.CompilerServices;
9+
using Xunit;
10+
11+
public class Runtime_111922v2
12+
{
13+
[Fact]
14+
public static int Test()
15+
{
16+
LinkedList<string> e = new LinkedList<string>();
17+
LinkedList<string> e1 = new LinkedList<string>();
18+
e1.AddLast("b");
19+
e1.AddLast("a");
20+
21+
int sum = -80;
22+
23+
for (int i = 0; i < 200; i++)
24+
{
25+
sum += Problem(i % 10 == 0 ? e : e1) ? 1 : 0;
26+
Thread.Sleep(5);
27+
}
28+
29+
Console.WriteLine(sum);
30+
return sum;
31+
}
32+
33+
[MethodImpl(MethodImplOptions.NoInlining)]
34+
static bool Problem(IEnumerable<string> e)
35+
{
36+
return e.Contains("a", StringComparer.OrdinalIgnoreCase);
37+
}
38+
}
39+
40+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<DebugType>None</DebugType>
4+
<Optimize>True</Optimize>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<Compile Include="$(MSBuildProjectName).cs" />
8+
</ItemGroup>
9+
</Project>

0 commit comments

Comments
 (0)