Optimize logic and variable removal early in Dfg (#7081)

After conversion of Ast to Dfg, but before synthesizing AstAlways into
primitives, run a pass to remove variables that are not observable, and
all logic that only computes such variables. This can get rid of a lot
of content early so we don't build redundant Dfgs, and also enables
synthesizing always blocks that use temporaries only in some branches,
which will come in a follow up.
This commit is contained in:
Geza Lore 2026-02-17 08:28:06 +00:00 committed by GitHub
parent ed2f018729
commit 5834f22944
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 118 additions and 2 deletions

View File

@ -216,6 +216,27 @@ private:
addStat("temporaries introduced", m_temporariesIntroduced);
}
};
class V3DfgRemoveUnobservableContext final : public V3DfgSubContext {
// Only V3DfgContext can create an instance
friend class V3DfgContext;
public:
// STATE
VDouble0 m_varsRemoved; // Number of variables removed from the Dfg
VDouble0 m_varsDeleted; // Number of variables removed from the Dfg and the Ast
VDouble0 m_logicRemoved; // Number of logic blocks removed from the Dfg
VDouble0 m_logicDeleted; // Number of logic blocks removed from the Dfg and the Ast
private:
V3DfgRemoveUnobservableContext(V3DfgContext& ctx, const std::string& label)
: V3DfgSubContext{ctx, label, "RemoveUnobservable"} {}
~V3DfgRemoveUnobservableContext() {
addStat("variables removed", m_varsRemoved);
addStat("variables deleted", m_varsDeleted);
addStat("logic removed", m_logicRemoved);
addStat("logic deleted", m_logicDeleted);
}
};
class V3DfgSynthesisContext final : public V3DfgSubContext {
// Only V3DfgContext can create an instance
friend class V3DfgContext;
@ -366,6 +387,7 @@ public:
V3DfgPeepholeContext m_peepholeContext{*this, m_label};
V3DfgPushDownSelsContext m_pushDownSelsContext{*this, m_label};
V3DfgRegularizeContext m_regularizeContext{*this, m_label};
V3DfgRemoveUnobservableContext m_removeUnobservableContext{*this, m_label};
V3DfgSynthesisContext m_synthContext{*this, m_label};
// Node pattern collector

View File

@ -314,6 +314,10 @@ class DataflowOptimize final {
// Dump the initial graph for debugging
if (dumpDfgLevel() >= 8) dfg.dumpDotFilePrefixed(m_ctx.prefix() + "dfg-in");
// Remove unobservable variabels and logic that drives only such variables
V3DfgPasses::removeUnobservable(dfg, m_ctx);
if (dumpDfgLevel() >= 8) dfg.dumpDotFilePrefixed(m_ctx.prefix() + "pruned");
// Synthesize DfgLogic vertices
V3DfgPasses::synthesize(dfg, m_ctx);
if (dumpDfgLevel() >= 8) dfg.dumpDotFilePrefixed(m_ctx.prefix() + "synth");

View File

@ -25,6 +25,82 @@
VL_DEFINE_DEBUG_FUNCTIONS;
void V3DfgPasses::removeUnobservable(DfgGraph& dfg, V3DfgContext& dfgCtx) {
V3DfgRemoveUnobservableContext& ctx = dfgCtx.m_removeUnobservableContext;
// Enqueue all DfgLogic vertices to work list
DfgWorklist workList{dfg};
for (DfgVertex& vtx : dfg.opVertices()) {
if (vtx.is<DfgLogic>()) workList.push_front(vtx);
}
// Remove all logic that only drives unobservable variables
workList.foreach([&](DfgVertex& vtx) {
DfgLogic* const logicp = vtx.as<DfgLogic>();
// Check all variables driven by this logic are removable
bool used = logicp->foreachSink([&](DfgVertex& snk) {
DfgUnresolved* const uVtxp = snk.as<DfgUnresolved>();
DfgVertexVar* const vVtxp = uVtxp->firtsSinkp()->as<DfgVertexVar>();
if (vVtxp->hasSinks()) return true;
if (vVtxp->isObserved()) return true;
return false;
});
// If some are used, the logic must stay in the Ast
if (used) return;
// If impure, it must stay in the Ast
if (!logicp->isPure()) return;
// Enqueue logic driving the inputs of this logic we are about to delete
logicp->foreachSource([&](DfgVertex& src) {
DfgVertexVar* const varp = src.as<DfgVertexVar>();
if (!varp->srcp()) return false;
varp->srcp()->as<DfgUnresolved>()->foreachSource([&](DfgVertex& driver) {
workList.push_front(*driver.as<DfgLogic>());
return false;
});
return false;
});
// Delete this logic both from the Dfg and the Ast
AstNode* const nodep = logicp->nodep();
VL_DO_DANGLING(logicp->unlinkDelete(dfg), logicp);
VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep);
++ctx.m_logicDeleted;
});
// Remove unobservable variables
for (DfgVertexVar* const vVtxp : dfg.varVertices().unlinkable()) {
if (vVtxp->hasSinks()) continue;
if (vVtxp->isObserved()) continue;
DfgVertex* const srcp = vVtxp->srcp(); // Must be a DfgUnresolved or nullptr
AstNode* const varp = vVtxp->nodep();
// Can delete the Ast variable too if it has no other references
const bool delAst = (!srcp || !srcp->nInputs()) //
&& !vVtxp->hasExtWrRefs() //
&& !vVtxp->hasModWrRefs();
VL_DO_DANGLING(vVtxp->unlinkDelete(dfg), vVtxp);
if (srcp) VL_DO_DANGLING(srcp->unlinkDelete(dfg), srcp);
if (delAst) {
VL_DO_DANGLING(varp->unlinkFrBack()->deleteTree(), varp);
++ctx.m_varsDeleted;
} else {
++ctx.m_varsRemoved;
}
}
// Finally remove logic from the Dfg if it drives no variables in the graph.
// These should only be those with side effects.
for (DfgVertex* const vtxp : dfg.opVertices().unlinkable()) {
if (!vtxp->is<DfgLogic>()) continue;
if (vtxp->hasSinks()) continue;
// Input variables will be read in Ast code, mark as such
vtxp->foreachSource([](DfgVertex& src) {
src.as<DfgVertexVar>()->setHasModRdRefs();
return false;
});
VL_DO_DANGLING(vtxp->unlinkDelete(dfg), vtxp);
++ctx.m_logicRemoved;
}
}
void V3DfgPasses::inlineVars(DfgGraph& dfg) {
for (DfgVertexVar& vtx : dfg.varVertices()) {
// Nothing to inline it into

View File

@ -38,6 +38,9 @@ std::unique_ptr<DfgGraph> astToDfg(AstModule&, V3DfgContext&) VL_MT_DISABLED;
// Same as above, but for the entire netlist, after V3Scope
std::unique_ptr<DfgGraph> astToDfg(AstNetlist&, V3DfgContext&) VL_MT_DISABLED;
// Remove unobservable variabels and logic that drives only such variables
void removeUnobservable(DfgGraph&, V3DfgContext&) VL_MT_DISABLED;
// Synthesize DfgLogic vertices into primitive operations.
// Removes all DfgLogic (even those that were not synthesized).
void synthesize(DfgGraph&, V3DfgContext&) VL_MT_DISABLED;

View File

@ -465,6 +465,7 @@ class DfgLogic final : public DfgVertexVariadic {
bool m_selectedForSynthesis = false; // Logic selected for synthesis
bool m_nonSynthesizable = false; // Logic is not synthesizeable (by DfgSynthesis)
bool m_reverted = false; // Logic was synthesized (in part if non synthesizable) then reverted
mutable uint8_t m_cachedPure = 0; // Cached purity of the logic
public:
DfgLogic(DfgGraph& dfg, AstAlways* nodep, AstScope* scopep, std::unique_ptr<CfgGraph> cfgp)
@ -493,6 +494,16 @@ public:
void setNonSynthesizable() { m_nonSynthesizable = true; }
bool reverted() const { return m_reverted; }
void setReverted() { m_reverted = true; }
// Logic has no side-effect, just computes its output variables based on its input variables
bool isPure() const {
if (!m_cachedPure) {
// This is a sledgehamer, but AstNodeStmts don't compute their 'purity' properly,
// not that 'purity' makes sense for statements... We don't call this often and cached.
const bool pure = m_nodep->forall([](AstNode* nodep) { return nodep->isPure(); });
m_cachedPure = static_cast<uint8_t>(pure) | 0x2;
}
return m_cachedPure & 0x01;
}
};
class DfgUnresolved final : public DfgVertexVariadic {

View File

@ -19,7 +19,7 @@ test.execute()
if test.vlt:
test.file_grep(test.stats, r'Optimizations, DFG scoped PushDownSels, sels pushed down\s+(\d+)',
50)
49)
test.file_grep(test.stats, r'Optimizations, DFG scoped PushDownSels, would be cyclic\s+(\d+)',
1)

View File

@ -18,7 +18,7 @@ test.compile(verilator_flags2=["-Wno-UNOPTTHREADS", "--stats", test.pli_filename
test.execute()
if test.vlt:
test.file_grep(test.stats, r'Optimizations, Const bit op reduction\s+(\d+)', 42)
test.file_grep(test.stats, r'Optimizations, Const bit op reduction\s+(\d+)', 43)
test.file_grep(test.stats, r'SplitVar, packed variables split automatically\s+(\d+)', 1)
test.passes()