Optimize circular logic in Dfg

Introduce a new DfgPrev vertex, representing the value of a variable
before any in-graph assignments. This can be used to break all remaining
cycles in the graph, so all Dfgs become acyclic after V3DfgBreakCycles.
The circular dataflow is still represented, and is taken care of by the
scheduler, it is just the DfgGraph that represents the logic that
becomes acyclic.

This makes V3DfgBreakCycles a mandatory transform, so drop the disabling
-fno-dfg-break-cycles option (still parsed, but has no effect).

Note the effect of this is small, as most cycles can be fixed up by
driver tracing, which is unchanged, but this is required for some
upcoming work.
This commit is contained in:
Geza Lore 2026-07-07 16:21:15 +01:00
parent 646dcd3838
commit 1364f402da
19 changed files with 227 additions and 126 deletions

View File

@ -715,6 +715,10 @@ Summary:
.. option:: -fno-dfg-break-cycles .. option:: -fno-dfg-break-cycles
Deprecated and has no effect (ignored).
In versions before 5.052:
Rarely needed. Disable breaking combinational cycles during DFG. Rarely needed. Disable breaking combinational cycles during DFG.
.. option:: -fno-dfg-peephole .. option:: -fno-dfg-peephole

View File

@ -179,6 +179,40 @@ static void dumpDotVertex(std::ostream& os, const DfgVertex& vtx) {
return; return;
} }
if (const DfgPrev* const prevVtxp = vtx.cast<DfgPrev>()) {
const AstVarScope* const vscp = prevVtxp->vscp();
os << toDotId(vtx);
// Begin attributes
os << " [";
// Begin 'label'
os << "label=\"";
// Name
os << vscp->prettyName();
// Address
os << '\n' << cvtToHex(prevVtxp);
// Type and fanout
os << '\n';
prevVtxp->dtype().astDtypep()->dumpSmall(os);
os << " / F" << prevVtxp->fanout();
// End 'label'
os << '"';
// Shape
if (prevVtxp->isPacked()) {
os << ", shape=box";
} else if (prevVtxp->isArray()) {
os << ", shape=box3d";
} else {
prevVtxp->v3fatalSrc("Unhandled variable type");
}
// Color
const char* const colorp = "mediumorchid1"; // Purple
os << ", style=filled";
os << ", fillcolor=\"" << colorp << "\"";
// End attributes
os << "]\n";
return;
}
if (const DfgConst* const constVtxp = vtx.cast<DfgConst>()) { if (const DfgConst* const constVtxp = vtx.cast<DfgConst>()) {
const V3Number& num = constVtxp->num(); const V3Number& num = constVtxp->num();
@ -479,6 +513,10 @@ void DfgVertex::typeCheck(const DfgGraph& dfg) const {
CHECK(!v.srcp() || v.srcp()->dtype() == v.dtype(), "'srcp' should match"); CHECK(!v.srcp() || v.srcp()->dtype() == v.dtype(), "'srcp' should match");
return; return;
} }
case VDfgType::Prev: {
CHECK(isPacked() || isArray(), "Should be Packed or Array type");
return;
}
case VDfgType::SpliceArray: case VDfgType::SpliceArray:
case VDfgType::SplicePacked: { case VDfgType::SplicePacked: {
const DfgVertexSplice& v = *as<DfgVertexSplice>(); const DfgVertexSplice& v = *as<DfgVertexSplice>();

View File

@ -816,6 +816,7 @@ bool DfgVertex::isCheaperThanLoad() const {
if (is<DfgConst>()) return true; if (is<DfgConst>()) return true;
// Variables // Variables
if (is<DfgVertexVar>()) return true; if (is<DfgVertexVar>()) return true;
if (is<DfgPrev>()) return true;
// Array sels are just address computation, but the address itself can be expensive // Array sels are just address computation, but the address itself can be expensive
if (const DfgArraySel* aselp = cast<DfgArraySel>()) { if (const DfgArraySel* aselp = cast<DfgArraySel>()) {
if (aselp->bitp()->is<DfgMatchMasked>()) return false; if (aselp->bitp()->is<DfgMatchMasked>()) return false;

View File

@ -108,6 +108,18 @@ public:
}); });
} }
// Update after replacing 'vtxp' with 'replacement'
void updateReplacement(const DfgVertex& vtx, DfgVertex& replacement) {
updateAcyclic(vtx);
if (!get(replacement)) {
replacement.foreachSink([&](DfgVertex& dst) {
if (!get(dst)) return false;
if (!stillCyclicFwd(dst)) updateAcyclic(dst);
return false;
});
}
}
/* /*
Check stored information is consistent with actual SCCs. Note we Check stored information is consistent with actual SCCs. Note we
can't detect during updates if an SCC has split into multiple SCCs. can't detect during updates if an SCC has split into multiple SCCs.
@ -1518,17 +1530,8 @@ class FixUp final {
UASSERT_OBJ(!vtx.hasSinks() == !m_sccInfo.get(*replacementp), &vtx, UASSERT_OBJ(!vtx.hasSinks() == !m_sccInfo.get(*replacementp), &vtx,
"Replacement vertex SCC inconsistent"); "Replacement vertex SCC inconsistent");
// If we broke the cycle through this vertex we can update the SccInfo // If we broke the cycle through this vertex, update the SccInfo
if (!vtx.hasSinks()) { if (!vtx.hasSinks()) m_sccInfo.updateReplacement(vtx, *replacementp);
m_sccInfo.updateAcyclic(vtx);
if (!m_sccInfo.get(*replacementp)) {
replacementp->foreachSink([&](DfgVertex& dst) {
if (!m_sccInfo.get(dst)) return false;
if (!m_sccInfo.stillCyclicFwd(dst)) m_sccInfo.updateAcyclic(dst);
return false;
});
}
}
} }
void main(DfgVertexVar& var) { void main(DfgVertexVar& var) {
@ -1583,7 +1586,7 @@ public:
} }
}; };
bool breakCycles(DfgGraph& dfg, V3DfgBreakCyclesContext& ctx) { void breakCycles(DfgGraph& dfg, V3DfgBreakCyclesContext& ctx) {
// Shorthand for dumping graph at given dump level // Shorthand for dumping graph at given dump level
const auto dump = [&](int level, const DfgGraph& dfg, const std::string& name) { const auto dump = [&](int level, const DfgGraph& dfg, const std::string& name) {
if (dumpDfgLevel() >= level) dfg.dumpDotFilePrefixed("breakCycles-" + name); if (dumpDfgLevel() >= level) dfg.dumpDotFilePrefixed("breakCycles-" + name);
@ -1622,7 +1625,7 @@ bool breakCycles(DfgGraph& dfg, V3DfgBreakCyclesContext& ctx) {
UINFO(7, "Graph became acyclic after " << nImprovements << " improvements"); UINFO(7, "Graph became acyclic after " << nImprovements << " improvements");
dump(7, dfg, "result-acyclic"); dump(7, dfg, "result-acyclic");
++ctx.m_nFixed; ++ctx.m_nFixed;
return true; return;
} }
} while (nImprovements != prevNImprovements); } while (nImprovements != prevNImprovements);
@ -1641,17 +1644,62 @@ bool breakCycles(DfgGraph& dfg, V3DfgBreakCyclesContext& ctx) {
++ctx.m_nImproved; ++ctx.m_nImproved;
} else { } else {
UINFO(7, "Graph NOT improved"); UINFO(7, "Graph NOT improved");
dump(7, dfg, "result-original");
++ctx.m_nUnchanged; ++ctx.m_nUnchanged;
} }
return false;
// Break any remaining cycles by inserting a DfgPrev for variables still in a cycle.
// Doing this unconditionally is both safe and sufficient:
// - Safe: a DfgPrev makes the variable's readers read its stored value instead
// of its in-graph driver, while the variable itself is still assigned. This
// merely reconstructs the cyclic variable-level dataflow, which the scheduler
// will resolve via its settle loop, so the computation is equivalent.
// - Sufficient: every cycle must pass through a variable, and in particular
// through a non-temporary one (synthesis doesn't create cycles within a single
// logic block).
// Also note that synthesized logic that depends on an in-graph update will read
// the synthesis temporary, not the real variable (which is what we are cutting),
// so the graph still holds all updates in a form visible to later optimizers.
// Gather all non-temporary variables as candidate cut points, prefer those that
// are kept regardless. Breaking a cycle there is essentially free, as they must be
// materialized anyway, whereas cutting an internal variable forces it to survive
// via its DfgPrev when it could otherwise be inlined away later.
std::vector<DfgVertexVar*> varps;
for (DfgVertexVar& vtx : dfg.varVertices()) {
if (!vtx.tmpForp()) varps.push_back(&vtx);
}
std::stable_partition(varps.begin(), varps.end(), [](const DfgVertexVar* varp) {
return varp->hasExtRefs() || varp->isVolatile();
});
// Insert DfgPrev vertices until the graph becomes acyclic
SccInfo sccInfo{dfg};
for (DfgVertexVar* const varp : varps) {
// Stop as soon as it becomes acyclic
if (!sccInfo.isCyclic()) break;
// Ignore if variable is not part of a cycle
if (!sccInfo.get(*varp)) continue;
// Insert a DfgPrev vertex for the variable
DfgPrev* const prevp = new DfgPrev{dfg, varp->vscp()};
sccInfo.add(*prevp, 0);
varp->replaceWith(prevp);
++ctx.m_nPrevInserted;
// Update SCC info
sccInfo.updateReplacement(*varp, *prevp);
}
// Validate SccInfo if in debug mode
if (v3Global.opt.debugCheck()) sccInfo.validate();
UASSERT(!sccInfo.isCyclic(), "Graph should become acyclic");
dump(7, dfg, "result-withprev");
} }
} //namespace V3DfgBreakCycles } //namespace V3DfgBreakCycles
void V3DfgPasses::breakCycles(DfgGraph& dfg, V3DfgContext& ctx) {
bool V3DfgPasses::breakCycles(DfgGraph& dfg, V3DfgContext& ctx) { V3DfgBreakCycles::breakCycles(dfg, ctx.m_breakCyclesContext);
const bool res = V3DfgBreakCycles::breakCycles(dfg, ctx.m_breakCyclesContext);
if (v3Global.opt.debugCheck()) V3DfgPasses::typeCheck(dfg); if (v3Global.opt.debugCheck()) V3DfgPasses::typeCheck(dfg);
V3DfgPasses::removeUnused(dfg); V3DfgPasses::removeUnused(dfg);
return res;
} }

View File

@ -103,6 +103,7 @@ public:
VDouble0 m_nImproved; // Number of graphs that were improved but still cyclic VDouble0 m_nImproved; // Number of graphs that were improved but still cyclic
VDouble0 m_nUnchanged; // Number of graphs that were left unchanged VDouble0 m_nUnchanged; // Number of graphs that were left unchanged
VDouble0 m_nImprovements; // Number of changes made to graphs VDouble0 m_nImprovements; // Number of changes made to graphs
VDouble0 m_nPrevInserted; // Number of Prev vertices inserted
private: private:
V3DfgBreakCyclesContext() V3DfgBreakCyclesContext()
@ -112,6 +113,7 @@ private:
addStat("improved", m_nImproved); addStat("improved", m_nImproved);
addStat("left unchanged", m_nUnchanged); addStat("left unchanged", m_nUnchanged);
addStat("changes applied", m_nImprovements); addStat("changes applied", m_nImprovements);
addStat("prev vertices inserted", m_nPrevInserted);
} }
}; };
class V3DfgCseContext final : public V3DfgSubContext { class V3DfgCseContext final : public V3DfgSubContext {

View File

@ -55,6 +55,7 @@ class V3DfgCse final {
case VDfgType::CReset: case VDfgType::CReset:
case VDfgType::VarArray: case VDfgType::VarArray:
case VDfgType::VarPacked: case VDfgType::VarPacked:
case VDfgType::Prev:
case VDfgType::AstRd: // LCOV_EXCL_STOP case VDfgType::AstRd: // LCOV_EXCL_STOP
vtx.v3fatalSrc("Hash should have been pre-computed"); vtx.v3fatalSrc("Hash should have been pre-computed");
@ -173,6 +174,7 @@ class V3DfgCse final {
// Special vertices // Special vertices
case VDfgType::Const: return a.as<DfgConst>()->num().isCaseEq(b.as<DfgConst>()->num()); case VDfgType::Const: return a.as<DfgConst>()->num().isCaseEq(b.as<DfgConst>()->num());
case VDfgType::CReset: return false; case VDfgType::CReset: return false;
case VDfgType::Prev: return false;
case VDfgType::VarArray: case VDfgType::VarArray:
case VDfgType::VarPacked: // CSE does not combine variables case VDfgType::VarPacked: // CSE does not combine variables
@ -307,9 +309,10 @@ class V3DfgCse final {
for (const DfgVertexVar& vtx : dfg.varVertices()) m_hashCache[vtx] = V3Hash{++varHash}; for (const DfgVertexVar& vtx : dfg.varVertices()) m_hashCache[vtx] = V3Hash{++varHash};
// Pre-hash Ast references, these are all unique like variables // Pre-hash Ast references, these are all unique like variables
for (const DfgVertexAst& vtx : dfg.astVertices()) m_hashCache[vtx] = V3Hash{++varHash}; for (const DfgVertexAst& vtx : dfg.astVertices()) m_hashCache[vtx] = V3Hash{++varHash};
// Pre-hash CReset vertices, these are all unique // Pre-hash CReset and Prev vertices, these are all unique
for (const DfgVertex& vtx : dfg.opVertices()) { for (const DfgVertex& vtx : dfg.opVertices()) {
if (vtx.is<DfgCReset>()) m_hashCache[vtx] = V3Hash{++varHash}; if (vtx.is<DfgCReset>()) m_hashCache[vtx] = V3Hash{++varHash};
if (vtx.is<DfgPrev>()) m_hashCache[vtx] = V3Hash{++varHash};
} }
// Similarly pre-hash constants for speed. While we don't combine constants, we do want // Similarly pre-hash constants for speed. While we don't combine constants, we do want

View File

@ -224,6 +224,10 @@ class DfgToAstVisitor final : DfgVisitor {
m_resultp = new AstVarRef{vtxp->fileline(), vtxp->vscp(), VAccess::READ}; m_resultp = new AstVarRef{vtxp->fileline(), vtxp->vscp(), VAccess::READ};
} }
void visit(DfgPrev* vtxp) override {
m_resultp = new AstVarRef{vtxp->fileline(), vtxp->vscp(), VAccess::READ};
}
void visit(DfgConst* vtxp) override { // void visit(DfgConst* vtxp) override { //
m_resultp = new AstConst{vtxp->fileline(), vtxp->num()}; m_resultp = new AstConst{vtxp->fileline(), vtxp->num()};
} }

View File

@ -39,7 +39,8 @@ class DataflowOptimize final {
// - bit2: Read by logic in same module/netlist not represented in DFG // - bit2: Read by logic in same module/netlist not represented in DFG
// - bit3: Written by logic in same module/netlist not represented in DFG // - bit3: Written by logic in same module/netlist not represented in DFG
// - bit4: Has READWRITE references // - bit4: Has READWRITE references
// - bit31-5: Reference count, how many DfgVertexVar represent this variable // - bit5: Has DfgPrev instance
// - bit31-6: Reference count, how many DfgVertexVar represent this variable
// //
// AstNode::user2/user3/user4 can be used by various DFG algorithms // AstNode::user2/user3/user4 can be used by various DFG algorithms
const VNUser1InUse m_user1InUse; const VNUser1InUse m_user1InUse;
@ -116,65 +117,44 @@ class DataflowOptimize final {
V3DfgPasses::synthesize(dfg, m_ctx); V3DfgPasses::synthesize(dfg, m_ctx);
endOfStage("synthesize", dfg, {}); endOfStage("synthesize", dfg, {});
// Extract the cyclic sub-graphs. We do this because a lot of the optimizations assume a // Extract the cyclic sub-graphs, so breakCycles can operate on small graphs,
// DAG, and large, mostly acyclic graphs could not be optimized due to the presence of // make all of them acyclic, then merge them back to the main graph
// small cycles. {
std::vector<std::unique_ptr<DfgGraph>> cyclicComps = dfg.extractCyclicComponents("cyclic"); std::vector<std::unique_ptr<DfgGraph>> comps = dfg.extractCyclicComponents("cyclic");
endOfStage("extractCyclic", dfg, cyclicComps); for (const auto& cp : comps) V3DfgPasses::breakCycles(*cp, m_ctx);
dfg.mergeGraphs(std::move(comps));
// Attempt to convert cyclic components into acyclic ones endOfStage("breakCycles", dfg, {});
std::vector<std::unique_ptr<DfgGraph>> madeAcyclicComponents;
if (v3Global.opt.fDfgBreakCycles()) {
for (auto it = cyclicComps.begin(); it != cyclicComps.end();) {
const bool madeAcyclic = V3DfgPasses::breakCycles(**it, m_ctx);
// If not made acyclic, keep it in 'cyclicComps'
if (!madeAcyclic) {
++it;
continue;
}
// Otherwise move to 'madeAcyclicComponents'
madeAcyclicComponents.emplace_back(std::move(*it));
it = cyclicComps.erase(it);
}
} }
// Merge those that were made acyclic back to the graph, this enables optimizing more
dfg.mergeGraphs(std::move(madeAcyclicComponents));
endOfStage("breakCycles", dfg, cyclicComps);
// Remove redundant selects // Split the now entirely acyclic DFG into [weakly] connected components
V3DfgPasses::removeSelects(dfg, m_ctx.m_removeSelectsContext); std::vector<std::unique_ptr<DfgGraph>> comps = dfg.splitIntoComponents("acyclic");
for (std::unique_ptr<DfgGraph>& compp : cyclicComps) {
V3DfgPasses::removeSelects(*compp, m_ctx.m_removeSelectsContext);
}
endOfStage("removeSelects", dfg, cyclicComps);
// Split the acyclic DFG into [weakly] connected components
std::vector<std::unique_ptr<DfgGraph>> acyclicComps = dfg.splitIntoComponents("acyclic");
UASSERT(dfg.size() == 0, "DfgGraph should have become empty"); UASSERT(dfg.size() == 0, "DfgGraph should have become empty");
endOfStage("splitAcyclic", dfg, acyclicComps); endOfStage("splitAcyclic", dfg, comps);
// Optimize each acyclic component // Main pass pipeline - optimize each acyclic component
for (auto& cp : acyclicComps) V3DfgPasses::inlineVars(*cp); {
endOfStage("inlineVars", dfg, acyclicComps); for (auto& cp : comps) V3DfgPasses::removeSelects(*cp, m_ctx.m_removeSelectsContext);
for (auto& cp : acyclicComps) V3DfgPasses::cse(*cp, m_ctx.m_cseContext0); endOfStage("removeSelects", dfg, comps);
endOfStage("cse0", dfg, acyclicComps); for (const auto& cp : comps) V3DfgPasses::inlineVars(*cp);
for (auto& cp : acyclicComps) V3DfgPasses::binToOneHot(*cp, m_ctx.m_binToOneHotContext); endOfStage("inlineVars", dfg, comps);
endOfStage("binToOneHot", dfg, acyclicComps); for (auto& cp : comps) V3DfgPasses::cse(*cp, m_ctx.m_cseContext0);
for (auto& cp : acyclicComps) V3DfgPasses::peephole(*cp, m_ctx.m_peepholeContext); endOfStage("cse0", dfg, comps);
endOfStage("peephole", dfg, acyclicComps); for (auto& cp : comps) V3DfgPasses::binToOneHot(*cp, m_ctx.m_binToOneHotContext);
// Accumulate patterns for reporting endOfStage("binToOneHot", dfg, comps);
if (v3Global.opt.dumpDfgPatterns()) { for (auto& cp : comps) V3DfgPasses::peephole(*cp, m_ctx.m_peepholeContext);
V3DfgPasses::dumpPatterns(acyclicComps); endOfStage("peephole", dfg, comps);
endOfStage("dumpPatterns");
// Accumulate patterns for reporting
if (v3Global.opt.dumpDfgPatterns()) V3DfgPasses::dumpPatterns(comps);
for (auto& cp : comps) V3DfgPasses::pushDownSels(*cp, m_ctx.m_pushDownSelsContext);
endOfStage("pushDownSels", dfg, comps);
for (auto& cp : comps) V3DfgPasses::cse(*cp, m_ctx.m_cseContext1);
endOfStage("cse1", dfg, comps);
} }
for (auto& cp : acyclicComps) V3DfgPasses::pushDownSels(*cp, m_ctx.m_pushDownSelsContext);
endOfStage("pushDownSels", dfg, acyclicComps);
for (auto& cp : acyclicComps) V3DfgPasses::cse(*cp, m_ctx.m_cseContext1);
endOfStage("cse1", dfg, acyclicComps);
// Merge everything back under the main DFG // Merge everything back under the main graph
dfg.mergeGraphs(std::move(acyclicComps)); dfg.mergeGraphs(std::move(comps));
dfg.mergeGraphs(std::move(cyclicComps));
endOfStage("optimized", dfg, {}); endOfStage("optimized", dfg, {});
// Regularize the graph after merging it all back together so all // Regularize the graph after merging it all back together so all

View File

@ -43,10 +43,10 @@ void removeUnobservable(DfgGraph&, V3DfgContext&) VL_MT_DISABLED;
void synthesize(DfgGraph&, V3DfgContext&) VL_MT_DISABLED; void synthesize(DfgGraph&, V3DfgContext&) VL_MT_DISABLED;
// Remove redundant selects // Remove redundant selects
void removeSelects(DfgGraph& dfg, V3DfgRemoveSelectsContext& ctx) VL_MT_DISABLED; void removeSelects(DfgGraph& dfg, V3DfgRemoveSelectsContext& ctx) VL_MT_DISABLED;
// Attempt to make the given cyclic graph into an acyclic, or "less cyclic" // Make the given cyclic graph into an acyclic one, by tracing drivers, and
// equivalent. Genuine combinational cycles can exist, so this might be // if that is unsuccessful, by inserting Prev vertices. The given graph will
// unsuccessful. Returns true if the graph became acyclic, false otherwise. // always become acyclic after this pass.
bool breakCycles(DfgGraph&, V3DfgContext&) VL_MT_DISABLED; void breakCycles(DfgGraph&, V3DfgContext&) VL_MT_DISABLED;
// Construct binary to oneHot decoders // Construct binary to oneHot decoders
void binToOneHot(DfgGraph&, V3DfgBinToOneHotContext&) VL_MT_DISABLED; void binToOneHot(DfgGraph&, V3DfgBinToOneHotContext&) VL_MT_DISABLED;
// Common subexpression elimination // Common subexpression elimination

View File

@ -266,6 +266,8 @@ class V3DfgPeephole final : public DfgVisitor {
void deleteVertex(DfgVertex* vtxp) { void deleteVertex(DfgVertex* vtxp) {
UASSERT_OBJ(!m_vInfo[vtxp].m_workListIndex, vtxp, "Deleted Vertex is in work list"); UASSERT_OBJ(!m_vInfo[vtxp].m_workListIndex, vtxp, "Deleted Vertex is in work list");
UASSERT_OBJ(!vtxp->hasSinks(), vtxp, "Should not delete used vertex"); UASSERT_OBJ(!vtxp->hasSinks(), vtxp, "Should not delete used vertex");
const DfgVertexVar* const varp = vtxp->cast<DfgVertexVar>();
UASSERT_OBJ(!varp || !varp->hasPrev(), vtxp, "Deleting variable consumed via DfgPrev");
// Invalidate cache entry // Invalidate cache entry
m_cache.invalidate(vtxp); m_cache.invalidate(vtxp);
@ -293,7 +295,6 @@ class V3DfgPeephole final : public DfgVisitor {
// This pass only removes variables that are either not driven in this graph, // This pass only removes variables that are either not driven in this graph,
// or are not observable outside the graph. If there is also no external write // or are not observable outside the graph. If there is also no external write
// to the variable and no references in other graph then delete the Ast var too. // to the variable and no references in other graph then delete the Ast var too.
const DfgVertexVar* const varp = vtxp->cast<DfgVertexVar>();
if (varp && !varp->isVolatile() && !varp->hasDfgRefs()) { if (varp && !varp->isVolatile() && !varp->hasDfgRefs()) {
m_ctx.m_deleteps.push_back(varp->vscp()); m_ctx.m_deleteps.push_back(varp->vscp());
VL_DO_DANGLING(vtxp->unlinkDelete(m_dfg), vtxp); VL_DO_DANGLING(vtxp->unlinkDelete(m_dfg), vtxp);

View File

@ -56,16 +56,6 @@ class DfgRegularize final {
} }
} }
std::unordered_set<const DfgVertexVar*> gatherCyclicVariables() {
DfgUserMap<uint64_t> vtx2Scc = m_dfg.makeUserMap<uint64_t>();
V3DfgPasses::colorStronglyConnectedComponents(m_dfg, vtx2Scc);
std::unordered_set<const DfgVertexVar*> circularVariables;
for (const DfgVertexVar& vtx : m_dfg.varVertices()) {
if (vtx2Scc[vtx]) circularVariables.emplace(&vtx);
}
return circularVariables;
}
static bool isUnused(const DfgVertex& vtx) { static bool isUnused(const DfgVertex& vtx) {
if (vtx.hasSinks()) return false; if (vtx.hasSinks()) return false;
if (const DfgVertexVar* const varp = vtx.cast<DfgVertexVar>()) { if (const DfgVertexVar* const varp = vtx.cast<DfgVertexVar>()) {
@ -73,6 +63,7 @@ class DfgRegularize final {
UASSERT_OBJ(!varp->hasDfgRefs(), varp, "Should not have refs in other DfgGraph"); UASSERT_OBJ(!varp->hasDfgRefs(), varp, "Should not have refs in other DfgGraph");
if (varp->hasModWrRefs()) return false; if (varp->hasModWrRefs()) return false;
if (varp->hasExtRefs()) return false; if (varp->hasExtRefs()) return false;
if (varp->hasPrev()) return false;
} }
return true; return true;
} }
@ -87,6 +78,9 @@ class DfgRegularize final {
&aVtx, "Mismatched vertices"); &aVtx, "Mismatched vertices");
UASSERT_OBJ(!aVtx.is<DfgVertexVar>(), &aVtx, "Should be an operation vertex"); UASSERT_OBJ(!aVtx.is<DfgVertexVar>(), &aVtx, "Should be an operation vertex");
// Prev is just a variable reference
if (aVtx.is<DfgPrev>()) return false;
if (bVtx.hasMultipleSinks()) { if (bVtx.hasMultipleSinks()) {
// Add a temporary if it's cheaper to store and load from memory than recompute // Add a temporary if it's cheaper to store and load from memory than recompute
if (!aVtx.isCheaperThanLoad()) return true; if (!aVtx.isCheaperThanLoad()) return true;
@ -119,10 +113,6 @@ class DfgRegularize final {
} }
void eliminateVars() { void eliminateVars() {
// Although we could eliminate some circular variables, doing so would
// make UNOPTFLAT traces fairly usesless, so we will not do so.
const std::unordered_set<const DfgVertexVar*> circularVariables = gatherCyclicVariables();
// Worklist based algoritm // Worklist based algoritm
DfgWorklist workList{m_dfg}; DfgWorklist workList{m_dfg};
@ -141,7 +131,7 @@ class DfgRegularize final {
}); });
// Delete corresponsing Ast variable at the end // Delete corresponsing Ast variable at the end
if (const DfgVertexVar* const varp = vtx.cast<DfgVertexVar>()) { if (const DfgVertexVar* const varp = vtx.cast<DfgVertexVar>()) {
m_ctx.m_deleteps.push_back(varp->vscp()); if (!varp->hasPrev()) m_ctx.m_deleteps.push_back(varp->vscp());
} }
// Remove the unused vertex // Remove the unused vertex
vtx.unlinkDelete(m_dfg); vtx.unlinkDelete(m_dfg);
@ -173,7 +163,7 @@ class DfgRegularize final {
UASSERT_OBJ(!varp->hasDfgRefs(), varp, "Should not have refs in other DfgGraph"); UASSERT_OBJ(!varp->hasDfgRefs(), varp, "Should not have refs in other DfgGraph");
// Do not eliminate circular variables - need to preserve UNOPTFLAT traces // Do not eliminate circular variables - need to preserve UNOPTFLAT traces
if (circularVariables.count(varp)) return; if (varp->hasPrev()) return;
// Do not inline if partially driven (the partial driver network can't be fed into // Do not inline if partially driven (the partial driver network can't be fed into
// arbitrary logic. TODO: we should peeophole these away entirely) // arbitrary logic. TODO: we should peeophole these away entirely)

View File

@ -62,8 +62,8 @@ protected:
*DfgDataType::fromAst(vscp->varp()->dtypep())} *DfgDataType::fromAst(vscp->varp()->dtypep())}
, m_vscp{vscp} { , m_vscp{vscp} {
// Increment reference count // Increment reference count
m_vscp->user1(m_vscp->user1() + 0x20); m_vscp->user1(m_vscp->user1() + 0x40);
UASSERT_OBJ((m_vscp->user1() >> 5) > 0, m_vscp, "Reference count overflow"); UASSERT_OBJ((m_vscp->user1() >> 6) > 0, m_vscp, "Reference count overflow");
// Allocate sources // Allocate sources
newInput(); newInput();
newInput(); newInput();
@ -72,8 +72,8 @@ protected:
public: public:
~DfgVertexVar() { ~DfgVertexVar() {
// Decrement reference count // Decrement reference count
m_vscp->user1(m_vscp->user1() - 0x20); m_vscp->user1(m_vscp->user1() - 0x40);
UASSERT_OBJ((m_vscp->user1() >> 5) >= 0, m_vscp, "Reference count underflow"); UASSERT_OBJ((m_vscp->user1() >> 6) >= 0, m_vscp, "Reference count underflow");
} }
ASTGEN_MEMBERS_DfgVertexVar; ASTGEN_MEMBERS_DfgVertexVar;
@ -87,7 +87,6 @@ public:
std::string srcName(size_t idx) const override final { return idx ? "defaultp" : "srcp"; } std::string srcName(size_t idx) const override final { return idx ? "defaultp" : "srcp"; }
// The Ast variable this vertex representess // The Ast variable this vertex representess
// AstVar* varp() const { return m_varp; }
AstVarScope* vscp() const { return m_vscp; } AstVarScope* vscp() const { return m_vscp; }
// If this is a temporary, the Ast variable it stands for, or same as // If this is a temporary, the Ast variable it stands for, or same as
@ -100,7 +99,7 @@ public:
void driverFileLine(FileLine* flp) { m_driverFileLine = flp; } void driverFileLine(FileLine* flp) { m_driverFileLine = flp; }
// Variable referenced from other DFG in the same module/netlist // Variable referenced from other DFG in the same module/netlist
bool hasDfgRefs() const { return m_vscp->user1() >> 6; } // I.e.: (nodep()->user1() >> 5) > 1 bool hasDfgRefs() const { return m_vscp->user1() >> 7; } // I.e.: (nodep()->user1() >> 6) > 1
// Variable referenced from Ast code in the same module/netlist // Variable referenced from Ast code in the same module/netlist
static bool hasModWrRefs(const AstVarScope* nodep) { return nodep->user1() & 0x08; } static bool hasModWrRefs(const AstVarScope* nodep) { return nodep->user1() & 0x08; }
@ -121,8 +120,15 @@ public:
static bool hasRWRefs(const AstVarScope* nodep) { return nodep->user1() & 0x10; } static bool hasRWRefs(const AstVarScope* nodep) { return nodep->user1() & 0x10; }
static void setHasRWRefs(AstVarScope* nodep) { nodep->user1(nodep->user1() | 0x10); } static void setHasRWRefs(AstVarScope* nodep) { nodep->user1(nodep->user1() | 0x10); }
// True iff the value of this variable is read outside this DfgGraph // There exists a DfgPrev vertex for this variable
static bool hasPrev(const AstVarScope* nodep) { return nodep->user1() & 0x20; }
bool hasPrev() const { return hasPrev(m_vscp); }
// True iff this variable is consumed without an explicit sink: its value is read outside
// this DfgGraph, or a DfgPrev reads it within this graph.
bool isObserved() const { bool isObserved() const {
// A DfgPrev reads this variable
if (hasPrev()) return true;
// A DfgVarVertex is written in exactly one DfgGraph, and might be read in an arbitrary // A DfgVarVertex is written in exactly one DfgGraph, and might be read in an arbitrary
// number of other DfgGraphs. If it's driven in this DfgGraph, it's read in others. // number of other DfgGraphs. If it's driven in this DfgGraph, it's read in others.
if (hasDfgRefs()) return srcp() || defaultp(); if (hasDfgRefs()) return srcp() || defaultp();
@ -162,6 +168,33 @@ public:
ASTGEN_MEMBERS_DfgVarPacked; ASTGEN_MEMBERS_DfgVarPacked;
}; };
class DfgPrev final : public DfgVertex {
// Previous value of variable, before any updates made in this graph.
// Used to break combinational cycles.
friend class DfgVertex;
friend class DfgVisitor;
AstVarScope* const m_vscp; // The AstVarScope associated with this vertex (not owned)
public:
DfgPrev(DfgGraph& dfg, AstVarScope* vscp)
: DfgVertex{dfg, dfgType(), vscp->varp()->fileline(),
*DfgDataType::fromAst(vscp->varp()->dtypep())}
, m_vscp{vscp} {
UASSERT_OBJ(!DfgVertexVar::hasPrev(vscp), vscp, "Variable already has a DfgPrev");
m_vscp->user1(m_vscp->user1() | 0x20); // Mark having a DfgPrev
}
~DfgPrev() {
m_vscp->user1(m_vscp->user1() & ~0x20); // Unmark having a DfgPrev
}
ASTGEN_MEMBERS_DfgPrev;
// The Ast variable this vertex representess
AstVarScope* vscp() const { return m_vscp; }
std::string srcName(size_t) const override final { return ""; }
};
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Ast reference vertices // Ast reference vertices

View File

@ -1466,7 +1466,9 @@ void V3Options::parseOptsList(FileLine* fl, const string& optdir, int argc,
DECL_OPTION("-fdead-cells", FOnOff, &m_fDeadCells); DECL_OPTION("-fdead-cells", FOnOff, &m_fDeadCells);
DECL_OPTION("-fdedup", FOnOff, &m_fDedupe); DECL_OPTION("-fdedup", FOnOff, &m_fDedupe);
DECL_OPTION("-fdfg", CbFOnOff, [this](bool flag) { m_fDfg = flag; }); DECL_OPTION("-fdfg", CbFOnOff, [this](bool flag) { m_fDfg = flag; });
DECL_OPTION("-fdfg-break-cycles", FOnOff, &m_fDfgBreakCycles); DECL_OPTION("-fdfg-break-cycles", CbFOnOff, [fl](bool) {
fl->v3warn(DEPRECATED, "Option '-fdfg-break-cycles' is deprecated and has no effect");
});
DECL_OPTION("-fdfg-peephole", FOnOff, &m_fDfgPeephole); DECL_OPTION("-fdfg-peephole", FOnOff, &m_fDfgPeephole);
DECL_OPTION("-fdfg-peephole-", CbPartialMatch, [this](const char* optp) { // DECL_OPTION("-fdfg-peephole-", CbPartialMatch, [this](const char* optp) { //
m_fDfgPeepholeDisabled.erase(optp); m_fDfgPeepholeDisabled.erase(optp);

View File

@ -402,7 +402,6 @@ private:
bool m_fConstBitOpTree; // main switch: -fno-const-bit-op-tree constant bit op tree bool m_fConstBitOpTree; // main switch: -fno-const-bit-op-tree constant bit op tree
bool m_fConstEager = true; // main switch: -fno-const-eagerly run V3Const during passes bool m_fConstEager = true; // main switch: -fno-const-eagerly run V3Const during passes
bool m_fDedupe; // main switch: -fno-dedupe: logic deduplication bool m_fDedupe; // main switch: -fno-dedupe: logic deduplication
bool m_fDfgBreakCycles = true; // main switch: -fno-dfg-break-cycles
bool m_fDfgPeephole = true; // main switch: -fno-dfg-peephole bool m_fDfgPeephole = true; // main switch: -fno-dfg-peephole
bool m_fDfgPushDownSels = true; // main switch: -fno-dfg-push-down-sels bool m_fDfgPushDownSels = true; // main switch: -fno-dfg-push-down-sels
bool m_fDfg; // main switch: -fno-dfg bool m_fDfg; // main switch: -fno-dfg
@ -743,7 +742,6 @@ public:
bool fConstEager() const { return m_fConstEager; } bool fConstEager() const { return m_fConstEager; }
bool fDedupe() const { return m_fDedupe; } bool fDedupe() const { return m_fDedupe; }
bool fDfg() const { return m_fDfg; } bool fDfg() const { return m_fDfg; }
bool fDfgBreakCycles() const { return m_fDfgBreakCycles; }
bool fDfgPeephole() const { return m_fDfgPeephole; } bool fDfgPeephole() const { return m_fDfgPeephole; }
bool fDfgPushDownSels() const { return m_fDfgPushDownSels; } bool fDfgPushDownSels() const { return m_fDfgPushDownSels; }
bool fDfgSynthesizeAll() const { return m_fDfgSynthesizeAll; } bool fDfgSynthesizeAll() const { return m_fDfgSynthesizeAll; }

View File

@ -67,7 +67,7 @@ with open(rdFile, 'r', encoding="utf8") as rdFh, \
test.compile(verilator_flags2=[ test.compile(verilator_flags2=[
"--stats", "--stats",
"--build", "--build",
"-fno-dfg" if test.name == "t_dfg_break_cycles" else "-fno-dfg-break-cycles", "-fno-dfg",
"-fno-gate", "-fno-gate",
"+incdir+" + test.obj_dir, "+incdir+" + test.obj_dir,
"-Mdir", test.obj_dir + "/obj_ref", "-Mdir", test.obj_dir + "/obj_ref",
@ -76,9 +76,8 @@ test.compile(verilator_flags2=[
]) # yapf:disable ]) # yapf:disable
# Check we got the expected number of circular logic warnings # Check we got the expected number of circular logic warnings
if test.name == "t_dfg_break_cycles": test.file_grep(test.obj_dir + "/obj_ref/Vref__stats.txt",
test.file_grep(test.obj_dir + "/obj_ref/Vref__stats.txt", r'Warnings, Suppressed UNOPTFLAT\s+(\d+)', nExpectedCycles)
r'Warnings, Suppressed UNOPTFLAT\s+(\d+)', nExpectedCycles)
# Compile optimized - also builds executable # Compile optimized - also builds executable
test.compile(verilator_flags2=[ test.compile(verilator_flags2=[

View File

@ -1,16 +0,0 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of either the GNU Lesser General Public License Version 3
# or the Perl Artistic License Version 2.0.
# SPDX-FileCopyrightText: 2025 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
import runpy
test.scenarios('vlt_all')
runpy.run_path("t/t_dfg_break_cycles.py", globals())

View File

@ -11,7 +11,9 @@ module t (
rand_a, rand_b, srand_a, srand_b, arand_a, arand_b rand_a, rand_b, srand_a, srand_b, arand_a, arand_b
); );
// verilator lint_off UNOPTFLAT
`include "portdecl.vh" // Boilerplate generated by t_dfg_peephole.py `include "portdecl.vh" // Boilerplate generated by t_dfg_peephole.py
// verilator lint_on UNOPTFLAT
input rand_a; input rand_a;
input rand_b; input rand_b;
@ -429,6 +431,17 @@ module t (
`signal(NARROW_CONCAT_B, narrow_concat[8:4]); `signal(NARROW_CONCAT_B, narrow_concat[8:4]);
`signal(NARROW_CONCAT_C, narrow_concat[5:4]); `signal(NARROW_CONCAT_C, narrow_concat[5:4]);
// Cycle needing a DfgPrev (variable shift). Peephole must keep the internal
// 'break_lo' driver, as the DfgPrev reads its stored value. Shift forced non-zero
// for a unique fixed point.
// verilator lint_off UNOPTFLAT
wire [63:0] break_lo;
wire [63:0] break_hi;
assign break_hi = (break_lo >> (rand_b[2:0] | 3'b1)) | rand_a;
assign break_lo = break_hi & rand_b;
// verilator lint_on UNOPTFLAT
`signal(DFG_PREV_KEEPS_DRIVER, break_hi);
// Assigned at the end to avoid inlining by other passes // Assigned at the end to avoid inlining by other passes
assign const_a = 64'h0123456789abcdef; assign const_a = 64'h0123456789abcdef;
assign const_b = 64'h98badefc10325647; assign const_b = 64'h98badefc10325647;

View File

@ -8,4 +8,5 @@
%Warning-DEPRECATED: Option '-fno-dfg-pre-inline' is deprecated and has no effect %Warning-DEPRECATED: Option '-fno-dfg-pre-inline' is deprecated and has no effect
%Warning-DEPRECATED: Option '-fno-dfg-post-inline' is deprecated and has no effect %Warning-DEPRECATED: Option '-fno-dfg-post-inline' is deprecated and has no effect
%Warning-DEPRECATED: Option '-fno-dfg-scoped' is deprecated, use '-fno-dfg' instead. %Warning-DEPRECATED: Option '-fno-dfg-scoped' is deprecated, use '-fno-dfg' instead.
%Warning-DEPRECATED: Option '-fdfg-break-cycles' is deprecated and has no effect
%Error: Exiting due to %Error: Exiting due to

View File

@ -12,7 +12,7 @@ import vltest_bootstrap
test.scenarios('vlt') test.scenarios('vlt')
test.lint(verilator_flags2=[ test.lint(verilator_flags2=[
"--trace-fst-thread --trace-threads 2 --order-clock-delay --clk foo --no-clk bar -fno-dfg-pre-inline -fno-dfg-post-inline -fno-dfg-scoped" "--trace-fst-thread --trace-threads 2 --order-clock-delay --clk foo --no-clk bar -fno-dfg-pre-inline -fno-dfg-post-inline -fno-dfg-scoped -fno-dfg-break-cycles"
], ],
fails=True, fails=True,
expect_filename=test.golden_filename) expect_filename=test.golden_filename)