Skip to content

Commit e5a2132

Browse files
authored
[NativeAOT-LLVM] Stop caching GC_EXPOSED_NO results in LSSA #3138 from SingleAccretion/LSSA-DefStatusFix
[NativeAOT-LLVM] Stop caching `GC_EXPOSED_NO` results in LSSA
2 parents d964353 + 5bcc1c9 commit e5a2132

File tree

3 files changed

+114
-58
lines changed

3 files changed

+114
-58
lines changed

src/coreclr/jit/llvmlssa.cpp

Lines changed: 35 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,6 @@ class ShadowStackAllocator
242242
class LssaDomTreeVisitor : public DomTreeVisitor<LssaDomTreeVisitor>
243243
{
244244
class NotExposedReason;
245-
struct NotExposedReasonLink;
246245

247246
// Cannot use raw node pointers as their values influence hash table iteration order.
248247
struct DeterministicNodeHashInfo : public HashTableInfo<DeterministicNodeHashInfo>
@@ -329,7 +328,7 @@ class ShadowStackAllocator
329328
ArrayStack<BitVec> m_candidateBitSetPool;
330329

331330
#ifdef DEBUG
332-
JitHashTable<LclSsaVarDsc*, JitPtrKeyFuncs<LclSsaVarDsc>, NotExposedReasonLink*> m_notExposedReasonMap;
331+
JitHashTable<LclSsaVarDsc*, JitPtrKeyFuncs<LclSsaVarDsc>, const char*> m_notExposedReasonMap;
333332
#endif // DEBUG
334333

335334
public:
@@ -616,6 +615,16 @@ class ShadowStackAllocator
616615

617616
void SpillSdsuValue(LIR::Range& blockRange, GenTree* defNode, unsigned* pSpillLclNum)
618617
{
618+
// Like with candidates, the GC status of an SDSU can change during its lifetime, so we need to check it
619+
// at every safepoint, in the general case. We could tighten this by filtering out more SDSUs early (like
620+
// 'null' and such), but live-across-a-safepoint SDSUs are not that common, so we currently don't.
621+
INDEBUG(NotExposedReason reason(this));
622+
if (!IsGcExposedSdsuValue(defNode, DefStatus::Active DEBUGARG(&reason)))
623+
{
624+
JITDUMPEXEC(reason.Print("[%06u] is live, but not exposed: ", "\n", Compiler::dspTreeID(defNode)));
625+
return;
626+
}
627+
619628
if (*pSpillLclNum != BAD_VAR_NUM)
620629
{
621630
// We may have already spilled this def live across multiple safe points.
@@ -748,9 +757,7 @@ class ShadowStackAllocator
748757
unsigned status = GetGcExposedStatus(ssaDsc);
749758
if (status == GC_EXPOSED_UNKNOWN)
750759
{
751-
bool isExposed = true;
752760
GenTreeLclVarCommon* defNode = ssaDsc->GetDefNode();
753-
INDEBUG(NotExposedReason defReason(this));
754761
if (defNode == nullptr)
755762
{
756763
assert(ssaNum == SsaConfig::FIRST_SSA_NUM);
@@ -760,19 +767,25 @@ class ShadowStackAllocator
760767
assert(initKind != ValueInitKind::None);
761768
if (initKind != ValueInitKind::Param)
762769
{
763-
INDEBUG(defReason.Add("implicit zero def"));
764-
isExposed = false;
770+
INDEBUG(pReason->Add("implicit zero def"));
771+
status = GC_EXPOSED_NO;
765772
}
766773
}
767774
else if (defNode->OperIs(GT_STORE_LCL_VAR) &&
768-
!IsGcExposedValue(defNode->Data(), DefStatus::Unknown DEBUGARG(&defReason)))
775+
!IsGcExposedValue(defNode->Data(), DefStatus::Unknown DEBUGARG(pReason)))
769776
{
770-
isExposed = false;
777+
status = GC_EXPOSED_NO;
771778
}
772779

773-
// This caching is designed to prevent quadratic behavior.
774-
status = isExposed ? GC_EXPOSED_YES : GC_EXPOSED_NO;
775-
SetGcExposedStatus(ssaDsc, status DEBUGARG(varDsc) DEBUGARG(&defReason));
780+
if (status != GC_EXPOSED_NO)
781+
{
782+
// This caching is designed to prevent quadratic behavior. However, we can't cache "NO" results as
783+
// they depend on the current execution point (different points may yield different results as
784+
// shadow slots get overwritten with new values), so this is not a complete solution...
785+
// TODO-LLVM-LSSA: fix the above with a new LSSA algorithm.
786+
status = GC_EXPOSED_YES;
787+
SetGcExposedStatus(ssaDsc, status DEBUGARG(varDsc));
788+
}
776789
}
777790
// Take care not to depend on potentially stale information. Consider:
778791
//
@@ -809,7 +822,6 @@ class ShadowStackAllocator
809822
}
810823

811824
LclVarDsc* varDsc;
812-
INDEBUG(NotExposedReason reason(this));
813825
if (IsCandidateLocalNode(node, &varDsc))
814826
{
815827
JITDUMP(" -- Processing a candidate:\n");
@@ -836,8 +848,7 @@ class ShadowStackAllocator
836848
IncrementActiveUseCount(varDsc->GetPerSsaData(ssaNum));
837849
}
838850
}
839-
else if (node->IsValue() && !node->IsUnusedValue() && IsGcExposedType(node) &&
840-
IsGcExposedSdsuValue(node, DefStatus::Active DEBUGARG(&reason)))
851+
else if (node->IsValue() && !node->IsUnusedValue() && IsGcExposedType(node))
841852
{
842853
node->gtLIRFlags |= LIR::Flags::Mark;
843854
m_liveSdsuGcDefs.AddOrUpdate(node, BAD_VAR_NUM);
@@ -852,17 +863,7 @@ class ShadowStackAllocator
852863
}
853864
if (node->TypeIs(TYP_STRUCT))
854865
{
855-
// TODO-LLVM: delete this once we're up to date with upstream deleting "STORE_DYN_BLK".
856-
if (node->OperIs(GT_IND))
857-
{
858-
return false;
859-
}
860-
if (!node->GetLayout(m_compiler)->HasGCPtr())
861-
{
862-
return false;
863-
}
864-
865-
return true;
866+
return node->GetLayout(m_compiler)->HasGCPtr();
866867
}
867868

868869
return false;
@@ -1112,7 +1113,7 @@ class ShadowStackAllocator
11121113
}
11131114

11141115
void SetGcExposedStatus(
1115-
LclSsaVarDsc* ssaDsc, unsigned status DEBUGARG(LclVarDsc* varDsc) DEBUGARG(NotExposedReason* pReason))
1116+
LclSsaVarDsc* ssaDsc, unsigned status DEBUGARG(LclVarDsc* varDsc) DEBUGARG(NotExposedReason* pReason = nullptr))
11161117
{
11171118
unsigned oldStatus = GetGcExposedStatus(ssaDsc);
11181119
assert((oldStatus == GC_EXPOSED_UNKNOWN) || ((oldStatus == GC_EXPOSED_YES) && (status == GC_EXPOSED_SPILL)));
@@ -1123,7 +1124,7 @@ class ShadowStackAllocator
11231124

11241125
JITDUMP("V%02u/%u: %s -> %s\n", m_compiler->lvaGetLclNum(varDsc),
11251126
varDsc->GetSsaNumForSsaDef(ssaDsc), GcStatusToString(oldStatus), GcStatusToString(status));
1126-
INDEBUG(pReason->SaveForDef(ssaDsc));
1127+
DBEXEC(pReason != nullptr, pReason->SaveForDef(ssaDsc));
11271128
}
11281129

11291130
unsigned GetGcExposedStatus(LclSsaVarDsc* ssaDsc)
@@ -1353,12 +1354,6 @@ class ShadowStackAllocator
13531354
}
13541355
}
13551356

1356-
struct NotExposedReasonLink
1357-
{
1358-
const char* Reason;
1359-
NotExposedReasonLink* Next;
1360-
};
1361-
13621357
class NotExposedReason
13631358
{
13641359
LssaDomTreeVisitor* m_visitor;
@@ -1393,12 +1388,11 @@ class ShadowStackAllocator
13931388
{
13941389
if (m_enabled)
13951390
{
1391+
const char* reason;
13961392
LclSsaVarDsc* ssaDsc = varDsc->GetPerSsaData(ssaNum);
1397-
NotExposedReasonLink* link = m_visitor->m_notExposedReasonMap[ssaDsc];
1398-
while (link != nullptr)
1393+
if (m_visitor->m_notExposedReasonMap.Lookup(ssaDsc, &reason))
13991394
{
1400-
Add(link->Reason);
1401-
link = link->Next;
1395+
Add(reason);
14021396
}
14031397

14041398
unsigned lclNum = m_visitor->m_compiler->lvaGetLclNum(varDsc);
@@ -1429,26 +1423,11 @@ class ShadowStackAllocator
14291423

14301424
void SaveForDef(LclSsaVarDsc* ssaDsc)
14311425
{
1432-
CompAllocator alloc = m_visitor->m_compiler->getAllocator(CMK_DebugOnly);
1433-
NotExposedReasonLink* links = nullptr;
1434-
NotExposedReasonLink* last = nullptr;
1435-
for (int i = 0; i < m_chain.Height(); i++)
1436-
{
1437-
NotExposedReasonLink* link = new (alloc) NotExposedReasonLink();
1438-
link->Reason = m_chain.Bottom(i);
1439-
if (last == nullptr)
1440-
{
1441-
links = link;
1442-
}
1443-
else
1444-
{
1445-
last->Next = link;
1446-
}
1447-
last = link;
1448-
}
1449-
if (links != nullptr)
1426+
if (m_enabled)
14501427
{
1451-
m_visitor->m_notExposedReasonMap.Set(ssaDsc, links);
1428+
assert(m_chain.Height() == 1); // Full support currently not needed.
1429+
const char* reason = m_chain.Top();
1430+
m_visitor->m_notExposedReasonMap.Set(ssaDsc, reason);
14521431
}
14531432
}
14541433

src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,8 @@ private static unsafe int Main(string[] args)
430430

431431
LSSATests.Run();
432432

433+
FunctionalLSSATests.Run();
434+
433435
TestThreadStaticAlignment();
434436

435437
TestLiveInFirstBlockForTracked(new object());

src/tests/nativeaot/SmokeTests/HelloWasm/LSSATests.cs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Runtime.CompilerServices;
6+
using System.Runtime.InteropServices;
67

78
namespace System.Runtime.JitTesting;
89

@@ -630,9 +631,83 @@ private static void YetAnotherSafePoint() { }
630631

631632
// Roslyn likes to eliminate locals we write. Force it to abstain with this function.
632633
private static void ForceILLocal(void* x) { }
634+
}
635+
636+
public unsafe class FunctionalLSSATests
637+
{
638+
private static bool s_failed;
639+
private static GCHandle s_chkObjWeakHandle;
640+
641+
// These tests, unlike the ones above, test the generated code indirectly, by creating objects and checking they're
642+
// kept alive as expected. They are ideally suited for testing correctness, where we need something to be visible
643+
// to the GC but do not care particularly about how precisely that is achieved.
644+
public static void Run()
645+
{
646+
s_chkObjWeakHandle = GCHandle.Alloc(null, GCHandleType.Weak);
647+
648+
Program.StartTest("FunctionalLSSATests");
649+
650+
SpilledCandidateSlotInvalidated();
651+
if (!FunctionalTestSucceeded(nameof(SpilledCandidateSlotInvalidated)))
652+
return;
653+
654+
Program.PassTest();
655+
}
656+
657+
private static bool FunctionalTestSucceeded(string name)
658+
{
659+
if (s_failed)
660+
{
661+
Program.FailTest($" {name} failed");
662+
return false;
663+
}
664+
return true;
665+
}
666+
667+
[MethodImpl(MethodImplOptions.NoInlining)]
668+
private static ref int SpilledCandidateSlotInvalidated()
669+
{
670+
// Here we are testing that 'derivedRef' keeps 'chkObj' alive as expected,
671+
// despite the fact 'chkObj' is spilled at the point 'derivedRef' is formed.
672+
ClassWithField chkObj = NewCheckedObject();
673+
LogicalSafePoint(chkObj, chkObj);
674+
LogicalSafePoint(chkObj, chkObj);
675+
676+
ref int derivedRef = ref chkObj.Field;
677+
chkObj = GetNullOpaquely(); // Invalidate the pin.
678+
LogicalSafePoint(chkObj, chkObj);
679+
LogicalSafePoint(chkObj, chkObj);
680+
ValidateCheckedObjectIsAlive();
681+
682+
return ref derivedRef;
683+
}
633684

634-
class ClassWithField
685+
[MethodImpl(MethodImplOptions.NoInlining)]
686+
private static ClassWithField NewCheckedObject()
635687
{
636-
public int Field;
688+
ClassWithField obj = new();
689+
s_chkObjWeakHandle.Target = obj;
690+
return obj;
637691
}
692+
693+
[MethodImpl(MethodImplOptions.NoInlining)]
694+
private static void ValidateCheckedObjectIsAlive()
695+
{
696+
GC.Collect();
697+
if (s_chkObjWeakHandle.Target is null)
698+
{
699+
s_failed = true;
700+
}
701+
}
702+
703+
[MethodImpl(MethodImplOptions.NoInlining)]
704+
private static void LogicalSafePoint(object x = null, object y = null) { }
705+
706+
[MethodImpl(MethodImplOptions.NoInlining)]
707+
private static ClassWithField GetNullOpaquely() => null;
708+
}
709+
710+
class ClassWithField
711+
{
712+
public int Field;
638713
}

0 commit comments

Comments
 (0)