Optimize multiplexers in Dfg synthesis (#6331)

The previous algorithm was designed to handle the general case where a
full control flow path predicate is required to select which value to
use when synthesizing control flow join point in an always block.

Here we add a better algorithm that tries to use the predicate of
the closest dominating branch if the branch paths dominate the joining
paths. This is almost universally true in synthesizable logic (RTLMeter
has no exceptions), however there are cases where this is not
applicable, for which we fall back on the previous generic algorithm.

Overall this significantly simplifies the synthesized Dfg graphs and
enables further optimization.
This commit is contained in:
Geza Lore
2025-08-25 13:47:45 +01:00
committed by GitHub
parent c2cac8a7fd
commit 02e64f0795
12 changed files with 1269 additions and 407 deletions
+204 -111
View File
@@ -508,15 +508,23 @@ class AstToDfgSynthesize final {
bool operator<=(const Driver& other) const { return !(other < *this); }
};
// STATE
// STATE - Persistent
DfgGraph& m_dfg; // The graph being built
V3DfgSynthesisContext& m_ctx; // The context for stats
AstToDfgConverter<T_Scoped> m_converter; // The convert instance to use for each construct
DfgLogic* m_logicp = nullptr; // Current logic vertex we are synthesizing
// Some debug aid: We stop after synthesizing s_dfgSynthDebugLimit vertices (if non-zero).
// This is the problematic logic (last one we synthesize), assuming a bisection search
// over s_dfgSynthDebugLimit.
// STATE - for current DfgLogic being synthesized
DfgLogic* m_logicp = nullptr; // Current logic vertex we are synthesizing
CfgBlockMap<SymTab> m_bbToISymTab; // Map from CfgBlock -> input symbol table
CfgBlockMap<SymTab> m_bbToOSymTab; // Map from CfgBlock -> output symbol table
CfgBlockMap<DfgVertex*> m_bbToCondp; // Map from CfgBlock -> terminating branch condition
CfgEdgeMap<DfgVertex*> m_edgeToPredicatep; // Map CfgGraphEdge -> path predicate to get there
CfgDominatorTree m_domTree; // The dominator tree of the current CFG
// STATE - Some debug aid
// We stop after synthesizing s_dfgSynthDebugLimit vertices (if non-zero).
// This is the problematic logic (last one we synthesize), assuming a
// bisection search over s_dfgSynthDebugLimit.
DfgLogic* m_debugLogicp = nullptr;
// Source (upstream) cone of outputs of m_debugLogicp
std::unique_ptr<std::unordered_set<const DfgVertex*>> m_debugOSrcConep{nullptr};
@@ -869,7 +877,7 @@ class AstToDfgSynthesize final {
});
}
// Initialzie input symbol table of entry BasicBlock
// Initialzie input symbol table of entry CfgBlock
void initializeEntrySymbolTable(SymTab& iSymTab) {
m_logicp->forEachSource([&](DfgVertex& src) {
DfgVertexVar* const vvp = src.as<DfgVertexVar>();
@@ -991,75 +999,152 @@ class AstToDfgSynthesize final {
return joinp;
}
// Merge 'thenSymTab' into 'elseSymTab' using the given predicate to join values
bool joinSymbolTables(SymTab& elseSymTab, DfgVertex* predicatep, const SymTab& thenSymTab) {
// Give up if something is not assigned on all paths ... Latch?
if (thenSymTab.size() != elseSymTab.size()) {
++m_ctx.m_synt.nonSynLatch;
return false;
}
// Join each symbol
for (std::pair<Variable* const, DfgVertexVar*>& pair : elseSymTab) {
Variable* const varp = pair.first;
// Find same variable on the else path
auto it = thenSymTab.find(varp);
// Give up if something is not assigned on all paths ... Latch?
if (it == thenSymTab.end()) {
++m_ctx.m_synt.nonSynLatch;
return false;
}
// Join paths with the predicate
DfgVertexVar* const thenp = it->second;
DfgVertexVar* const elsep = pair.second;
DfgVertexVar* const newp = joinDrivers(varp, predicatep, thenp, elsep);
if (!newp) return false;
pair.second = newp;
}
// Done
return true;
}
// Given two joining control flow edges, compute how to join their symbols.
// Returns the predicaete to join over, and the 'then' and 'else' blocks.
std::tuple<DfgVertex*, const CfgBlock*, const CfgBlock*> //
howToJoin(const CfgEdge* const ap, const CfgEdge* const bp) {
// Find the closest common dominator of the two paths
const CfgBlock* const domp = m_domTree.closestCommonDominator(ap->srcp(), bp->srcp());
// These paths join here, so 'domp' must be a branch, otherwise it's not the closest
UASSERT_OBJ(domp->isBranch(), domp, "closestCommonDominator is not a branch");
// The branches of the common dominator
const CfgEdge* const takenEdgep = domp->takenEdgep();
const CfgEdge* const untknEdgep = domp->untknEdgep();
// We check if the taken branch dominates the path to either blocks,
// and if the untaken branch dominates the path to the other block.
// If so, we can use the branch condition as predicate, otherwise
// we must use the path predicate as there are ways to get from one
// branch of the dominator to the other. We need to be careful if
// either branches are directly to the join block. This is fine,
// it's as if there was an empty block on that critical edge which
// is dominated by that path.
if (takenEdgep == ap || m_domTree.dominates(takenEdgep->dstp(), ap->srcp())) {
if (untknEdgep == bp || m_domTree.dominates(untknEdgep->dstp(), bp->srcp())) {
// Taken path dominates 'ap' and untaken dominates 'bp', use the branch condition
++m_ctx.m_synt.joinUsingBranchCondition;
return std::make_tuple(m_bbToCondp[domp], ap->srcp(), bp->srcp());
}
} else if (takenEdgep == bp || m_domTree.dominates(takenEdgep->dstp(), bp->srcp())) {
if (untknEdgep == ap || m_domTree.dominates(untknEdgep->dstp(), ap->srcp())) {
// Taken path dominates 'bp' and untaken dominates 'ap', use the branch condition
++m_ctx.m_synt.joinUsingBranchCondition;
return std::make_tuple(m_bbToCondp[domp], bp->srcp(), ap->srcp());
}
}
// The branches don't dominate the joined blocks, must use the path predicate
++m_ctx.m_synt.joinUsingPathPredicate;
// TODO: We could do better here: use the path predicate of the closest
// cominating blocks, pick the one from the lower rank, etc, but this
// generic case is very rare, most synthesizable logic has
// series-parallel CFGs which are covered by the earlier cases.
return std::make_tuple(m_edgeToPredicatep[ap], ap->srcp(), bp->srcp());
}
// Combine the output symbol tables of the predecessors of the given
// BasicBlock to compute the input symtol table for the given block.
bool createInputSymbolTable(SymTab& joined, const BasicBlock& bb,
const BasicBlockMap<SymTab>& bbToOSymTab,
const ControlFlowEdgeMap<DfgVertex*>& edgeToPredicatep) {
// Input symbol table of entry block was previously initialzied
if (bb.inEmpty()) return true;
// block to compute the input symtol table for the given block.
bool createInputSymbolTable(const CfgBlock& bb) {
// The input symbol table of the given block, we are computing it now
SymTab& joined = m_bbToISymTab[bb];
// We will fill it in here
UASSERT(joined.empty(), "Unresolved input symbol table should be empty");
// Fast path if there is only one predecessor
if (bb.inSize1()) {
joined = bbToOSymTab[*(bb.inEdges().frontp()->fromp()->as<BasicBlock>())];
// Input symbol table of entry block is special
if (bb.isEnter()) {
initializeEntrySymbolTable(joined);
return true;
}
// Gather predecessors and the path predicates
// Current input symbol table should be empty, we will fill it in here
UASSERT(joined.empty(), "Unprocessed input symbol table should be empty");
// Fast path if there is only one predecessor - TODO: use less copying
if (!bb.isJoin()) {
joined = m_bbToOSymTab[bb.firstPredecessorp()];
return true;
}
// We also have a simpler job if there are 2 predecessors
if (bb.isTwoWayJoin()) {
DfgVertex* predicatep = nullptr;
const CfgBlock* thenp = nullptr;
const CfgBlock* elsep = nullptr;
std::tie(predicatep, thenp, elsep)
= howToJoin(bb.firstPredecessorEdgep(), bb.lastPredecessorEdgep());
// Copy from else
joined = m_bbToOSymTab[elsep];
// Join with then
return joinSymbolTables(joined, predicatep, m_bbToOSymTab[*thenp]);
}
// General hard way
// Gather predecessors
struct Predecessor final {
const BasicBlock* m_bbp;
DfgVertex* m_predicatep;
const CfgBlock* m_bbp; // Predeccessor block
DfgVertex* m_predicatep; // Predicate predecessor reached this block with
const SymTab* m_oSymTabp; // Output symbol table or predecessor
Predecessor() = delete;
Predecessor(const BasicBlock* bbp, DfgVertex* predicatep)
Predecessor(const CfgBlock* bbp, DfgVertex* predicatep, const SymTab* oSymTabp)
: m_bbp{bbp}
, m_predicatep{predicatep} {}
, m_predicatep{predicatep}
, m_oSymTabp{oSymTabp} {}
};
const std::vector<Predecessor> predecessors = [&]() {
std::vector<Predecessor> res;
for (const V3GraphEdge& edge : bb.inEdges()) {
const ControlFlowEdge& cfgEdge = static_cast<const ControlFlowEdge&>(edge);
res.emplace_back(&cfgEdge.src(), edgeToPredicatep[cfgEdge]);
const CfgEdge& cfgEdge = static_cast<const CfgEdge&>(edge);
const CfgBlock* const predecessorp = cfgEdge.srcp();
DfgVertex* const predicatep = m_edgeToPredicatep[cfgEdge];
const SymTab* const oSymTabp = &m_bbToOSymTab[predecessorp];
res.emplace_back(predecessorp, predicatep, oSymTabp);
}
// Sort predecessors topologically. This way later blocks will come
// after earlier blocks, and the entry block will be first if present.
// Sort predecessors reverse topologically. This way earlier blocks
// will come after later blocks, and the entry block is last if present.
std::sort(res.begin(), res.end(), [](const Predecessor& a, const Predecessor& b) { //
return a.m_bbp->id() < b.m_bbp->id();
return *a.m_bbp > *b.m_bbp;
});
return res;
}();
// Start by copying the bindings from the oldest predecessor
joined = bbToOSymTab[*predecessors[0].m_bbp];
// Start by copying the bindings from the frist predecessor
joined = *predecessors[0].m_oSymTabp;
// Join over all other predecessors
for (size_t i = 1; i < predecessors.size(); ++i) {
DfgVertex* const predicatep = predecessors[i].m_predicatep;
const SymTab& oSymTab = bbToOSymTab[*predecessors[i].m_bbp];
// Give up if something is not assigned on all paths ... Latch?
if (joined.size() != oSymTab.size()) {
++m_ctx.m_synt.nonSynLatch;
return false;
}
// Join each symbol
for (auto& pair : joined) {
Variable* const varp = pair.first;
// Find same variable on other path
auto it = oSymTab.find(varp);
// Give up if something is not assigned on all paths ... Latch?
if (it == oSymTab.end()) {
++m_ctx.m_synt.nonSynLatch;
return false;
}
// Join paths with the block predicate
DfgVertexVar* const thenp = it->second;
DfgVertexVar* const elsep = pair.second;
DfgVertexVar* const newp = joinDrivers(varp, predicatep, thenp, elsep);
if (!newp) return false;
pair.second = newp;
}
const SymTab& oSymTab = *predecessors[i].m_oSymTabp;
if (!joinSymbolTables(joined, predicatep, oSymTab)) return false;
}
return true;
@@ -1169,7 +1254,7 @@ class AstToDfgSynthesize final {
}
// Synthesize the given statements with the given input symbol table.
// Returnt true if successfolly synthesized.
// Returns true if successfolly synthesized.
// Populates the given output symbol table.
// Populates the given reference with the condition of the terminator branch, if any.
bool synthesizeBasicBlock(SymTab& oSymTab, DfgVertex*& condpr,
@@ -1259,56 +1344,53 @@ class AstToDfgSynthesize final {
return true;
}
// Given a basic block, and the condition of the terminating branch (if any),
// assign perdicates to the block's outgoing control flow edges.
void assignSuccessorPredicates(ControlFlowEdgeMap<DfgVertex*>& edgeToPredicatep,
const BasicBlock& bb, DfgVertex* condp) {
// Assign path perdicates to the outgoing control flow edges of the given block
void assignPathPredicates(const CfgBlock& bb) {
// Nothing to do for the exit block
if (bb.outEmpty()) return;
if (bb.isExit()) return;
// Get the predicate of this block
DfgVertex* const predp = [&]() -> DfgVertex* {
// Entry block has no predecessors, use constant true
if (bb.inEmpty()) return make<DfgConst>(m_logicp->fileline(), 1U, 1U);
if (bb.isEnter()) return make<DfgConst>(m_logicp->fileline(), 1U, 1U);
// For any other block, 'or' together all the incoming predicates
const auto& inEdges = bb.inEdges();
auto it = inEdges.begin();
DfgVertex* resp = edgeToPredicatep[static_cast<const ControlFlowEdge&>(*it)];
DfgVertex* resp = m_edgeToPredicatep[static_cast<const CfgEdge&>(*it)];
while (++it != inEdges.end()) {
DfgOr* const orp = make<DfgOr>(resp->fileline(), resp->dtypep());
orp->rhsp(resp);
orp->lhsp(edgeToPredicatep[static_cast<const ControlFlowEdge&>(*it)]);
orp->lhsp(m_edgeToPredicatep[static_cast<const CfgEdge&>(*it)]);
resp = orp;
}
return resp;
}();
if (!condp) {
// There should be 1 successors for a block with an unconditional terminator
UASSERT_OBJ(!bb.untknEdgep(), predp, "Expecting 1 successor for BasicBlock");
// Successor predicate edge is the same
edgeToPredicatep[*bb.takenEdgep()] = predp;
} else {
// There should be 2 successors for a block with an conditional terminator
UASSERT_OBJ(bb.untknEdgep(), predp, "Expecting 2 successors for BasicBlock");
FileLine* const flp = condp->fileline();
AstNodeDType* const dtypep = condp->dtypep(); // Single bit
// Predicate for taken branch: 'predp & condp'
DfgAnd* const takenPredp = make<DfgAnd>(flp, dtypep);
takenPredp->lhsp(predp);
takenPredp->rhsp(condp);
edgeToPredicatep[*bb.takenEdgep()] = takenPredp;
// Predicate for untaken branch: 'predp & ~condp'
DfgAnd* const untknPredp = make<DfgAnd>(flp, dtypep);
untknPredp->lhsp(predp);
DfgNot* const notp = make<DfgNot>(flp, dtypep);
notp->srcp(condp);
untknPredp->rhsp(notp);
edgeToPredicatep[*bb.untknEdgep()] = untknPredp;
// For uncondional branches, the successor predicate edge is the same
if (!bb.isBranch()) {
m_edgeToPredicatep[bb.takenEdgep()] = predp;
return;
}
// For branches, we need to factor in the branch condition
DfgVertex* const condp = m_bbToCondp[bb];
FileLine* const flp = condp->fileline();
AstNodeDType* const dtypep = condp->dtypep(); // Single bit
// Predicate for taken branch: 'predp & condp'
DfgAnd* const takenPredp = make<DfgAnd>(flp, dtypep);
takenPredp->lhsp(predp);
takenPredp->rhsp(condp);
m_edgeToPredicatep[bb.takenEdgep()] = takenPredp;
// Predicate for untaken branch: 'predp & ~condp'
DfgAnd* const untknPredp = make<DfgAnd>(flp, dtypep);
untknPredp->lhsp(predp);
DfgNot* const notp = make<DfgNot>(flp, dtypep);
notp->srcp(condp);
untknPredp->rhsp(notp);
m_edgeToPredicatep[bb.untknEdgep()] = untknPredp;
}
// Add the synthesized values as drivers to the output variables of the current DfgLogic
@@ -1359,7 +1441,7 @@ class AstToDfgSynthesize final {
// Initialzie input symbol table
initializeEntrySymbolTable(iSymTab);
// Synthesize as if it was in a single BasicBlock CFG
// Synthesize as if it was in a single CfgBlock CFG
DfgVertex* condp = nullptr;
const bool success = synthesizeBasicBlock(oSymTab, condp, {assignp}, iSymTab);
UASSERT_OBJ(!condp, nodep, "Conditional AstAssignW ???");
@@ -1372,7 +1454,7 @@ class AstToDfgSynthesize final {
}
// Synthesize the given AstAlways. Returns true on success.
bool synthesizeCfg(const ControlFlowGraph& cfg) {
bool synthesizeCfg(CfgGraph& cfg) {
++m_ctx.m_synt.inputAlways;
if (hasExternallyWrittenVariable(*m_logicp)) {
@@ -1383,37 +1465,48 @@ class AstToDfgSynthesize final {
// If there is a backward edge (loop), we can't synthesize it
if (cfg.containsLoop()) {
++m_ctx.m_synt.nonSynLoop;
++m_ctx.m_synt.cfgCyclic;
return false;
}
// Maps from BasicBlock to its input and output symbol tables
BasicBlockMap<SymTab> bbToISymTab = cfg.makeBasicBlockMap<SymTab>();
BasicBlockMap<SymTab> bbToOSymTab = cfg.makeBasicBlockMap<SymTab>();
// If it's a trivial CFG we can save on some work
if (cfg.nBlocks() == 1) {
++m_ctx.m_synt.cfgTrivial;
} else {
// Insert two-way join blocks to aid multiplexer ordering
if (cfg.insertTwoWayJoins()) {
++m_ctx.m_synt.cfgSp;
} else {
++m_ctx.m_synt.cfgDag;
}
// Initialize maps needed for non-trivial CFGs
m_domTree = CfgDominatorTree{cfg};
m_edgeToPredicatep = cfg.makeEdgeMap<DfgVertex*>();
}
// Map from ControlFlowGraphEdge to its predicate
ControlFlowEdgeMap<DfgVertex*> edgeToPredicatep = cfg.makeEdgeMap<DfgVertex*>();
// Initialzie input symbol table of entry block
initializeEntrySymbolTable(bbToISymTab[cfg.enter()]);
// Initialize CfgMaps
m_bbToISymTab = cfg.makeBlockMap<SymTab>();
m_bbToOSymTab = cfg.makeBlockMap<SymTab>();
m_bbToCondp = cfg.makeBlockMap<DfgVertex*>();
// Synthesize all blocks
for (const V3GraphVertex& cfgVtx : cfg.vertices()) {
const BasicBlock& bb = *cfgVtx.as<BasicBlock>();
// Symbol tables of the block
SymTab& iSymTab = bbToISymTab[bb];
SymTab& oSymTab = bbToOSymTab[bb];
// Join symbol tables from predecessor blocks
if (!createInputSymbolTable(iSymTab, bb, bbToOSymTab, edgeToPredicatep)) return false;
// Condition of the terminating branch, if any
DfgVertex* condp = nullptr;
// Synthesize the block
if (!synthesizeBasicBlock(oSymTab, condp, bb.stmtps(), iSymTab)) return false;
// Set the predicates on the successor edges
assignSuccessorPredicates(edgeToPredicatep, bb, condp);
for (const V3GraphVertex& vtx : cfg.vertices()) {
const CfgBlock& bb = static_cast<const CfgBlock&>(vtx);
// Prepare the input symbol table of this block (enter, or join predecessor blocks)
if (!createInputSymbolTable(bb)) return false;
// Synthesize this block
if (!synthesizeBasicBlock(m_bbToOSymTab[bb], //
m_bbToCondp[bb], //
bb.stmtps(), //
m_bbToISymTab[bb])) {
return false;
}
// Set the path predicates on the successor edges
assignPathPredicates(bb);
}
// Add resolved output variable drivers
return addSynthesizedOutput(bbToOSymTab[cfg.exit()]);
return addSynthesizedOutput(m_bbToOSymTab[cfg.exit()]);
}
// Synthesize a DfgLogic into regular vertices. Returns ture on success.
@@ -1676,7 +1769,7 @@ void V3DfgPasses::synthesize(DfgGraph& dfg, V3DfgContext& ctx) {
if (VN_IS(logicp->nodep(), AssignW)) return true;
// Synthesize always blocks with no more than 4 basic blocks and 4 edges
// These are usually simple branches (if (rst) ... else ...), or close to it
return logicp->cfg().nBasicBlocks() <= 4 && logicp->cfg().nEdges() <= 4;
return logicp->cfg().nBlocks() <= 4 && logicp->cfg().nEdges() <= 4;
});
if (doIt) varps.emplace_back(&var);
}