Skip to content

Commit fbef29b

Browse files
Expand the GC hole fix for explicitly initialized structs (#69501)
* Expand the fix for explicitly initialized structs Liveness code already recognizes that it cannot delete certain local stores with the implicit side effect of "explicit initialization". However, it was only doing that for indirect "InitBlk" forms, while the store can have more or less arbitrary shape (the only requirement is that is must be "entire"). Fix this by not constraining the check to "InitBlk"s. * Add a test
1 parent 1d1df64 commit fbef29b

File tree

5 files changed

+109
-44
lines changed

5 files changed

+109
-44
lines changed

src/coreclr/jit/compiler.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -4591,7 +4591,8 @@ class Compiler
45914591

45924592
bool fgTryRemoveNonLocal(GenTree* node, LIR::Range* blockRange);
45934593

4594-
void fgRemoveDeadStoreLIR(GenTree* store, BasicBlock* block);
4594+
bool fgTryRemoveDeadStoreLIR(GenTree* store, GenTreeLclVarCommon* lclNode, BasicBlock* block);
4595+
45954596
bool fgRemoveDeadStore(GenTree** pTree,
45964597
LclVarDsc* varDsc,
45974598
VARSET_VALARG_TP life,

src/coreclr/jit/gentree.h

+12
Original file line numberDiff line numberDiff line change
@@ -3280,6 +3280,12 @@ struct GenTreeLclVarCommon : public GenTreeUnOp
32803280
SetLclNum(lclNum);
32813281
}
32823282

3283+
GenTree*& Data()
3284+
{
3285+
assert(OperIsLocalStore());
3286+
return gtOp1;
3287+
}
3288+
32833289
unsigned GetLclNum() const
32843290
{
32853291
return _gtLclNum;
@@ -6602,6 +6608,12 @@ struct GenTreeIndir : public GenTreeOp
66026608
gtOp1 = addr;
66036609
}
66046610

6611+
GenTree*& Data()
6612+
{
6613+
assert(OperIs(GT_STOREIND) || OperIsStoreBlk());
6614+
return gtOp2;
6615+
}
6616+
66056617
// these methods provide an interface to the indirection node which
66066618
bool HasBase();
66076619
bool HasIndex();

src/coreclr/jit/liveness.cpp

+37-43
Original file line numberDiff line numberDiff line change
@@ -1970,42 +1970,19 @@ void Compiler::fgComputeLifeLIR(VARSET_TP& life, BasicBlock* block, VARSET_VALAR
19701970
{
19711971
GenTreeIndir* const store = addrUse.User()->AsIndir();
19721972

1973-
// If this is a zero init of an explicit zero init gc local
1974-
// that has at least one other reference, we will keep the zero init.
1975-
//
1976-
const LclVarDsc& varDsc = lvaTable[node->AsLclVarCommon()->GetLclNum()];
1977-
const bool isExplicitInitLocal = varDsc.lvHasExplicitInit;
1978-
const bool isReferencedLocal = varDsc.lvRefCnt() > 1;
1979-
const bool isZeroInit = store->OperIsInitBlkOp();
1980-
const bool isGCInit = varDsc.HasGCPtr();
1981-
1982-
if (isExplicitInitLocal && isReferencedLocal && isZeroInit && isGCInit)
1973+
if (fgTryRemoveDeadStoreLIR(store, node->AsLclVarCommon(), block))
19831974
{
1984-
// Yes, we'd better keep it around.
1985-
//
1986-
JITDUMP("Keeping dead indirect store -- explicit zero init of gc type\n");
1987-
DISPNODE(store);
1988-
}
1989-
else
1990-
{
1991-
// Remove the store. DCE will iteratively clean up any ununsed operands.
1992-
//
1993-
JITDUMP("Removing dead indirect store:\n");
1994-
DISPNODE(store);
1995-
1996-
assert(store->Addr() == node);
1997-
blockRange.Delete(this, block, node);
1975+
JITDUMP("Removing dead LclVar address:\n");
1976+
DISPNODE(node);
1977+
blockRange.Remove(node);
19981978

1999-
GenTree* data =
2000-
store->OperIs(GT_STOREIND) ? store->AsStoreInd()->Data() : store->AsBlk()->Data();
1979+
GenTree* data = store->AsIndir()->Data();
20011980
data->SetUnusedValue();
20021981

20031982
if (data->isIndir())
20041983
{
20051984
Lowering::TransformUnusedIndirection(data->AsIndir(), this, block);
20061985
}
2007-
2008-
fgRemoveDeadStoreLIR(store, block);
20091986
}
20101987
}
20111988
}
@@ -2027,17 +2004,10 @@ void Compiler::fgComputeLifeLIR(VARSET_TP& life, BasicBlock* block, VARSET_VALAR
20272004
isDeadStore = fgComputeLifeUntrackedLocal(life, keepAliveVars, varDsc, lclVarNode);
20282005
}
20292006

2030-
if (isDeadStore)
2007+
if (isDeadStore && fgTryRemoveDeadStoreLIR(node, lclVarNode, block))
20312008
{
2032-
JITDUMP("Removing dead store:\n");
2033-
DISPNODE(lclVarNode);
2034-
2035-
// Remove the store. DCE will iteratively clean up any ununsed operands.
2036-
lclVarNode->gtOp1->SetUnusedValue();
2037-
2038-
fgRemoveDeadStoreLIR(node, block);
2009+
lclVarNode->Data()->SetUnusedValue();
20392010
}
2040-
20412011
break;
20422012
}
20432013

@@ -2185,17 +2155,41 @@ bool Compiler::fgTryRemoveNonLocal(GenTree* node, LIR::Range* blockRange)
21852155
}
21862156

21872157
//---------------------------------------------------------------------
2188-
// fgRemoveDeadStoreLIR - remove a dead store from LIR
2158+
// fgTryRemoveDeadStoreLIR - try to remove a dead store from LIR
21892159
//
2190-
// store - A store tree
2191-
// block - Block that the store is part of
2160+
// Arguments:
2161+
// store - A store tree
2162+
// lclNode - The node representing the local being stored to
2163+
// block - Block that the store is part of
2164+
//
2165+
// Return Value:
2166+
// Whether the store was successfully removed from "block"'s range.
21922167
//
2193-
void Compiler::fgRemoveDeadStoreLIR(GenTree* store, BasicBlock* block)
2168+
bool Compiler::fgTryRemoveDeadStoreLIR(GenTree* store, GenTreeLclVarCommon* lclNode, BasicBlock* block)
21942169
{
2195-
LIR::Range& blockRange = LIR::AsRange(block);
2196-
blockRange.Remove(store);
21972170
assert(!opts.MinOpts());
2171+
2172+
// We cannot remove stores to (tracked) TYP_STRUCT locals with GC pointers marked as "explicit init",
2173+
// as said locals will be reported to the GC untracked, and deleting the explicit initializer risks
2174+
// exposing uninitialized references.
2175+
if ((lclNode->gtFlags & GTF_VAR_USEASG) == 0)
2176+
{
2177+
LclVarDsc* varDsc = lvaGetDesc(lclNode);
2178+
if (varDsc->lvHasExplicitInit && (varDsc->TypeGet() == TYP_STRUCT) && varDsc->HasGCPtr() &&
2179+
(varDsc->lvRefCnt() > 1))
2180+
{
2181+
JITDUMP("Not removing a potential explicit init [%06u] of V%02u\n", dspTreeID(store), lclNode->GetLclNum());
2182+
return false;
2183+
}
2184+
}
2185+
2186+
JITDUMP("Removing dead %s:\n", store->OperIsIndir() ? "indirect store" : "local store");
2187+
DISPNODE(store);
2188+
2189+
LIR::AsRange(block).Remove(store);
21982190
fgStmtRemoved = true;
2191+
2192+
return true;
21992193
}
22002194

22012195
//---------------------------------------------------------------------
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
7+
public class Runtime_65694_2
8+
{
9+
public static int Main()
10+
{
11+
var a = new StructWithObj { Obj = new object() };
12+
var c = new StructWithObj { Obj = new object() };
13+
14+
return Problem(a, c).Obj == c.Obj ? 100 : 101;
15+
}
16+
17+
[MethodImpl(MethodImplOptions.NoInlining)]
18+
private static StructWithObj Problem(StructWithObj a, StructWithObj c)
19+
{
20+
StructWithObj b = a;
21+
22+
[MethodImpl(MethodImplOptions.NoInlining)]
23+
static void GcSafePoint() { GC.Collect(); }
24+
25+
GcSafePoint();
26+
GcSafePoint();
27+
28+
if (a.Obj == b.Obj)
29+
{
30+
b = c;
31+
}
32+
33+
return b;
34+
}
35+
36+
struct StructWithObj
37+
{
38+
public object Obj;
39+
}
40+
}
41+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<Optimize>True</Optimize>
5+
<CLRTestBatchPreCommands><![CDATA[
6+
$(CLRTestBatchPreCommands)
7+
set DOTNET_JitNoStructPromotion=1
8+
]]></CLRTestBatchPreCommands>
9+
<BashCLRTestPreCommands><![CDATA[
10+
$(BashCLRTestPreCommands)
11+
export DOTNET_JitNoStructPromotion=1
12+
]]></BashCLRTestPreCommands>
13+
</PropertyGroup>
14+
<ItemGroup>
15+
<Compile Include="$(MSBuildProjectName).cs" />
16+
</ItemGroup>
17+
</Project>

0 commit comments

Comments
 (0)