Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JIT: Add post-phase validation of DFS tree #95682

Merged
merged 7 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 6 additions & 11 deletions src/coreclr/jit/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4946,6 +4946,12 @@ void Compiler::compCompile(void** methodCodePtr, uint32_t* methodCodeSize, JitFl
//
DoPhase(this, PHASE_OPTIMIZE_BRANCHES, &Compiler::optRedundantBranches);
}
else
{
// DFS tree is always invalid after this point.
//
fgInvalidateDfsTree();
}

if (doCse)
{
Expand All @@ -4954,17 +4960,6 @@ void Compiler::compCompile(void** methodCodePtr, uint32_t* methodCodeSize, JitFl
DoPhase(this, PHASE_OPTIMIZE_VALNUM_CSES, &Compiler::optOptimizeCSEs);
}

// Assertion prop can do arbitrary statement remorphing, which
// can clone code and disrupt our simpleminded SSA accounting.
//
// So, disable the ssa checks.
//
if (fgSsaValid)
{
JITDUMP("Marking SSA as invalid before assertion prop\n");
fgSsaValid = false;
}
Comment on lines -4957 to -4966
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndyAyersMS The SSA checker is dependent on SSA's dominator tree, which is stale after RBO, so I'm also setting fgSsaValid = false in fgInvalidateDfsTree. It means we no longer check after CSEs, but I suppose that's not a big issue since RBO is really the last user.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Seems like the SSA checker could be switched to iterate the post order of the DFS as well)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok by me -- at one point I wanted to keep the SSA checked properties viable longer, but I don't recall why.


if (doAssertionProp)
{
// Assertion propagation
Expand Down
6 changes: 6 additions & 0 deletions src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -6049,7 +6049,11 @@ class Compiler
PhaseStatus fgSetBlockOrder();
bool fgHasCycleWithoutGCSafePoint();

template<typename TFuncAssignPreorder, typename TFuncAssignPostorder>
unsigned fgRunDfs(TFuncAssignPreorder assignPreorder, TFuncAssignPostorder assignPostorder);

FlowGraphDfsTree* fgComputeDfs();
void fgInvalidateDfsTree();

void fgRemoveReturnBlock(BasicBlock* block);

Expand Down Expand Up @@ -6115,6 +6119,8 @@ class Compiler
bool fgDebugCheckIncomingProfileData(BasicBlock* block, ProfileChecks checks);
bool fgDebugCheckOutgoingProfileData(BasicBlock* block, ProfileChecks checks);

void fgDebugCheckDfsTree();

#endif // DEBUG

static bool fgProfileWeightsEqual(weight_t weight1, weight_t weight2, weight_t epsilon = 0.01);
Expand Down
76 changes: 76 additions & 0 deletions src/coreclr/jit/compiler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4915,6 +4915,82 @@ inline bool Compiler::compCanHavePatchpoints(const char** reason)
return whyNot == nullptr;
}

//------------------------------------------------------------------------
// fgRunDfs: Run DFS over the flow graph.
//
// Type parameters:
// VisitPreorder - Functor type that takes a BasicBlock* and its preorder number
// VisitPostorder - Functor type that takes a BasicBlock* and its postorder number
//
// Parameters:
// visitPreorder - Functor to visit block in its preorder
// visitPostorder - Functor to visit block in its postorder
//
// Returns:
// Number of blocks visited.
//
template <typename VisitPreorder, typename VisitPostorder>
unsigned Compiler::fgRunDfs(VisitPreorder visitPreorder, VisitPostorder visitPostorder)
{
BitVecTraits traits(fgBBNumMax + 1, this);
BitVec visited(BitVecOps::MakeEmpty(&traits));

unsigned preOrderIndex = 0;
unsigned postOrderIndex = 0;

ArrayStack<AllSuccessorEnumerator> blocks(getAllocator(CMK_DepthFirstSearch));

auto dfsFrom = [&](BasicBlock* firstBB) {

BitVecOps::AddElemD(&traits, visited, firstBB->bbNum);
blocks.Emplace(this, firstBB);
visitPreorder(firstBB, preOrderIndex++);

while (!blocks.Empty())
{
BasicBlock* block = blocks.TopRef().Block();
BasicBlock* succ = blocks.TopRef().NextSuccessor();

if (succ != nullptr)
{
if (BitVecOps::TryAddElemD(&traits, visited, succ->bbNum))
{
blocks.Emplace(this, succ);
visitPreorder(succ, preOrderIndex++);
}
}
else
{
blocks.Pop();
visitPostorder(block, postOrderIndex++);
}
}

};

dfsFrom(fgFirstBB);

if ((fgEntryBB != nullptr) && !BitVecOps::IsMember(&traits, visited, fgEntryBB->bbNum))
{
// OSR methods will early on create flow that looks like it goes to the
// patchpoint, but during morph we may transform to something that
// requires the original entry (fgEntryBB).
assert(opts.IsOSR());
assert((fgEntryBB->bbRefs == 1) && (fgEntryBB->bbPreds == nullptr));
dfsFrom(fgEntryBB);
}

if ((genReturnBB != nullptr) && !BitVecOps::IsMember(&traits, visited, genReturnBB->bbNum) && !fgGlobalMorphDone)
{
// We introduce the merged return BB before morph and will redirect
// other returns to it as part of morph; keep it reachable.
dfsFrom(genReturnBB);
}

assert(preOrderIndex == postOrderIndex);
return preOrderIndex;
}

//------------------------------------------------------------------------------
// FlowGraphNaturalLoop::VisitLoopBlocksReversePostOrder: Visit all of the
// loop's blocks in reverse post order.
Expand Down
55 changes: 22 additions & 33 deletions src/coreclr/jit/fgdiagnostic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4664,43 +4664,17 @@ void Compiler::fgDebugCheckSsa()
assert(fgSsaPassesCompleted > 0);
assert(fgDomsComputed);

// This class visits the flow graph the same way the SSA builder does.
// In particular it may skip over blocks that SSA did not rename.
//
class SsaCheckDomTreeVisitor : public NewDomTreeVisitor<SsaCheckDomTreeVisitor>
{
SsaCheckVisitor& m_checkVisitor;

public:
SsaCheckDomTreeVisitor(Compiler* compiler, SsaCheckVisitor& checkVisitor)
: NewDomTreeVisitor(compiler), m_checkVisitor(checkVisitor)
{
}

void PreOrderVisit(BasicBlock* block)
{
m_checkVisitor.SetBlock(block);

for (Statement* const stmt : block->Statements())
{
m_checkVisitor.WalkTree(stmt->GetRootNodePointer(), nullptr);
}
}
};

// Visit the blocks that SSA initially renamed
//
SsaCheckVisitor scv(this);
SsaCheckDomTreeVisitor visitor(this, scv);
visitor.WalkTree(fgSsaDomTree);

// Also visit any blocks added after SSA was built
//
for (BasicBlock* const block : Blocks())
SsaCheckVisitor scv(this);
for (unsigned i = 0; i < m_dfsTree->GetPostOrderCount(); i++)
{
if (block->bbNum > fgDomBBcount)
BasicBlock* block = m_dfsTree->GetPostOrder()[i];
scv.SetBlock(block);

for (Statement* const stmt : block->Statements())
{
visitor.PreOrderVisit(block);
scv.WalkTree(stmt->GetRootNodePointer(), nullptr);
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed this since fgDomBBcount is not SSA's dom count, and since the new DFS validation would trigger if a (reachable) block was added anyway. Also switched the above to iterate the blocks via the computed post order from the DFS tree.


Expand Down Expand Up @@ -5136,5 +5110,20 @@ void Compiler::fgDebugCheckLoopTable()
assert(preHeaderCount == 0);
}

//------------------------------------------------------------------------------
// fgDebugCheckDfsTree: Checks that the DFS tree matches the current flow graph.
//
void Compiler::fgDebugCheckDfsTree()
{
unsigned count =
fgRunDfs([](BasicBlock* block, unsigned preorderNum) { assert(block->bbPreorderNum == preorderNum); },
[=](BasicBlock* block, unsigned postorderNum) {
assert(block->bbNewPostorderNum == postorderNum);
assert(m_dfsTree->GetPostOrder()[postorderNum] == block);
});

assert(m_dfsTree->GetPostOrderCount() == count);
}

/*****************************************************************************/
#endif // DEBUG
27 changes: 26 additions & 1 deletion src/coreclr/jit/fgopt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6438,7 +6438,32 @@ PhaseStatus Compiler::fgDfsBlocksAndRemove()
}
#endif

fgRemoveUnreachableBlocks([=](BasicBlock* block) { return !m_dfsTree->Contains(block); });
// The DFS we run is not precise around call-finally, so
// `fgRemoveUnreachableBlocks` can expose newly unreachable blocks
// that we did not uncover during the DFS. If we did remove any
// call-finally blocks then iterate to closure. This is a very rare
// case.
while (true)
{
bool anyCallFinallyPairs = false;
fgRemoveUnreachableBlocks([=, &anyCallFinallyPairs](BasicBlock* block) {
if (!m_dfsTree->Contains(block))
{
anyCallFinallyPairs |= block->isBBCallAlwaysPair();
return true;
}

return false;
});

if (!anyCallFinallyPairs)
{
break;
}

m_dfsTree = fgComputeDfs();
}
Comment on lines +6441 to +6465
Copy link
Member Author

@jakobbotsch jakobbotsch Dec 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly even with the more precise EH treatment we do not get around this issue, so I've had to add this iteration to closure to make sure the validation passes in cases where removing a callfinally/always pair results in new unreachable blocks (without the validation it would just result in a bit conservative behavior -- considering some blocks reachable that aren't). I only saw one assertion in all of our collections, so at least TP wise it does not seem like an issue.


status = PhaseStatus::MODIFIED_EVERYTHING;
}

Expand Down
76 changes: 21 additions & 55 deletions src/coreclr/jit/flowgraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4053,64 +4053,29 @@ bool FlowGraphDfsTree::IsAncestor(BasicBlock* ancestor, BasicBlock* descendant)
FlowGraphDfsTree* Compiler::fgComputeDfs()
{
BasicBlock** postOrder = new (this, CMK_DepthFirstSearch) BasicBlock*[fgBBcount];
BitVecTraits traits(fgBBNumMax + 1, this);

BitVec visited(BitVecOps::MakeEmpty(&traits));
unsigned numBlocks = fgRunDfs([](BasicBlock* block, unsigned preorderNum) { block->bbPreorderNum = preorderNum; },
[=](BasicBlock* block, unsigned postorderNum) {
block->bbNewPostorderNum = postorderNum;
assert(postorderNum < fgBBcount);
postOrder[postorderNum] = block;
});

unsigned preOrderIndex = 0;
unsigned postOrderIndex = 0;

ArrayStack<AllSuccessorEnumerator> blocks(getAllocator(CMK_DepthFirstSearch));

auto dfsFrom = [&, postOrder](BasicBlock* firstBB) {

BitVecOps::AddElemD(&traits, visited, firstBB->bbNum);
blocks.Emplace(this, firstBB);
firstBB->bbPreorderNum = preOrderIndex++;

while (!blocks.Empty())
{
BasicBlock* block = blocks.TopRef().Block();
BasicBlock* succ = blocks.TopRef().NextSuccessor();

if (succ != nullptr)
{
if (BitVecOps::TryAddElemD(&traits, visited, succ->bbNum))
{
blocks.Emplace(this, succ);
succ->bbPreorderNum = preOrderIndex++;
}
}
else
{
blocks.Pop();
postOrder[postOrderIndex] = block;
block->bbNewPostorderNum = postOrderIndex++;
}
}

};

dfsFrom(fgFirstBB);

if ((fgEntryBB != nullptr) && !BitVecOps::IsMember(&traits, visited, fgEntryBB->bbNum))
{
// OSR methods will early on create flow that looks like it goes to the
// patchpoint, but during morph we may transform to something that
// requires the original entry (fgEntryBB).
assert(opts.IsOSR());
assert((fgEntryBB->bbRefs == 1) && (fgEntryBB->bbPreds == nullptr));
dfsFrom(fgEntryBB);
}

if ((genReturnBB != nullptr) && !BitVecOps::IsMember(&traits, visited, genReturnBB->bbNum) && !fgGlobalMorphDone)
{
// We introduce the merged return BB before morph and will redirect
// other returns to it as part of morph; keep it reachable.
dfsFrom(genReturnBB);
}
return new (this, CMK_DepthFirstSearch) FlowGraphDfsTree(this, postOrder, numBlocks);
}

return new (this, CMK_DepthFirstSearch) FlowGraphDfsTree(this, postOrder, postOrderIndex);
//------------------------------------------------------------------------
// fgInvalidateDfsTree: Invalidate computed DFS tree and dependent annotations
// (like loops, dominators and SSA).
//
void Compiler::fgInvalidateDfsTree()
{
m_dfsTree = nullptr;
m_loops = nullptr;
fgSsaDomTree = nullptr;
m_newToOldLoop = nullptr;
m_oldToNewLoop = nullptr;
fgSsaValid = false;
}

//------------------------------------------------------------------------
Expand Down Expand Up @@ -5425,6 +5390,7 @@ FlowGraphDominatorTree* FlowGraphDominatorTree::Build(const FlowGraphDfsTree* df
{
BasicBlock* block = postOrder[i];
BasicBlock* parent = block->bbIDom;
assert(parent != nullptr);
assert(dfsTree->Contains(block) && dfsTree->Contains(parent));

domTree[i].nextSibling = domTree[parent->bbNewPostorderNum].firstChild;
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/jit/morph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14102,10 +14102,10 @@ PhaseStatus Compiler::fgMorphBlocks()

// We are done with the global morphing phase
//
fgInvalidateDfsTree();
fgGlobalMorph = false;
fgGlobalMorphDone = true;
compCurBB = nullptr;
m_dfsTree = nullptr;

#ifdef DEBUG
if (optLocalAssertionProp)
Expand Down
5 changes: 5 additions & 0 deletions src/coreclr/jit/phase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ void Phase::PostPhase(PhaseStatus status)
{
comp->fgDebugCheckLinkedLocals();
}

if (comp->m_dfsTree != nullptr)
{
comp->fgDebugCheckDfsTree();
}
}
#endif // DEBUG
}
3 changes: 3 additions & 0 deletions src/coreclr/jit/redundantbranchopts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ PhaseStatus Compiler::optRedundantBranches()
}
#endif // DEBUG

// DFS tree is always considered invalid after RBO.
fgInvalidateDfsTree();

return visitor.madeChanges ? PhaseStatus::MODIFIED_EVERYTHING : PhaseStatus::MODIFIED_NOTHING;
}

Expand Down
Loading