// -*- mode: C++; c-file-style: "cc-mode" -*- //************************************************************************* // DESCRIPTION: Verilator: Dataflow based optimization of combinational logic // // Code available from: https://verilator.org // //************************************************************************* // // 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: 2003-2026 Wilson Snyder // SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 // //************************************************************************* // // High level entry points from Ast world to the DFG optimizer. // //************************************************************************* #include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT #include "V3DfgOptimizer.h" #include "V3AstUserAllocator.h" #include "V3Const.h" #include "V3Dfg.h" #include "V3DfgPasses.h" #include "V3Error.h" #include "V3Graph.h" #include "V3UniqueNames.h" #include VL_DEFINE_DEBUG_FUNCTIONS; // Extract more combinational logic equations from procedures for better optimization opportunities class DataflowExtractVisitor final : public VNVisitor { // NODE STATE // AstVar::user3 -> bool: Flag indicating variable is subject of force or release // statement AstVar::user4 -> bool: Flag indicating variable is combinationally driven // AstNodeModule::user4 -> Extraction candidates (via m_extractionCandidates) const VNUser3InUse m_user3InUse; const VNUser4InUse m_user4InUse; // Expressions considered for extraction as separate assignment to gain more opportunities for // optimization, together with the list of variables they read. using Candidates = std::vector>>; // Expressions considered for extraction. All the candidates are pure expressions. AstUser4Allocator m_extractionCandidates; // STATE AstNodeModule* m_modp = nullptr; // The module being visited Candidates* m_candidatesp = nullptr; bool m_impure = false; // True if the visited tree has a side effect bool m_inForceReleaseLhs = false; // Iterating LHS of force/release // List of AstVar nodes read by the visited tree. 'vector' rather than 'set' as duplicates are // somewhat unlikely and we can handle them later. std::vector m_readVars; // METHODS // Node considered for extraction as a combinational equation. Trace variable usage/purity. void iterateExtractionCandidate(AstNode* nodep) { UASSERT_OBJ(!VN_IS(nodep->backp(), NodeExpr), nodep, "Should not try to extract nested expressions (only root expressions)"); // Simple VarRefs should not be extracted, as they only yield trivial assignments. // Similarly, don't extract anything if no candidate map is set up (for non-modules). // We still need to visit them though, to mark hierarchical references. if (VN_IS(nodep, NodeVarRef) || !m_candidatesp) { iterate(nodep); return; } // Don't extract plain constants if (VN_IS(nodep, Const)) return; // Candidates can't nest, so no need for VL_RESTORER, just initialize iteration state m_impure = false; m_readVars.clear(); // Trace variable usage iterate(nodep); // We only extract pure expressions if (m_impure) return; // Do not extract expressions without any variable references if (m_readVars.empty()) return; // Add to candidate list m_candidatesp->emplace_back(VN_AS(nodep, NodeExpr), std::move(m_readVars)); } // VISIT methods void visit(AstNetlist* nodep) override { // Analyze the whole design iterateChildrenConst(nodep); // Replace candidate expressions only reading combinationally driven signals with variables V3UniqueNames names{"__VdfgExtracted"}; for (AstNodeModule* modp = nodep->modulesp(); modp; modp = VN_AS(modp->nextp(), NodeModule)) { // Only extract from proper modules if (!VN_IS(modp, Module)) continue; for (const auto& pair : m_extractionCandidates(modp)) { AstNodeExpr* const cnodep = pair.first; // Do not extract expressions without any variable references if (pair.second.empty()) continue; // Check if all variables read by this expression are driven combinationally, // and move on if not. Also don't extract it if one of the variables is subject // to a force/release, as releasing nets must have immediate effect, but adding // extra combinational logic can change semantics (see t_force_release_net*). { bool hasBadVar = false; for (const AstVar* const readVarp : pair.second) { // variable is target of force/release or not combinationally driven if (readVarp->user3() || !readVarp->user4()) { hasBadVar = true; break; } } if (hasBadVar) continue; } // Create temporary variable FileLine* const flp = cnodep->fileline(); const string name = names.get(cnodep); AstVar* const varp = new AstVar{flp, VVarType::MODULETEMP, name, cnodep->dtypep()}; varp->trace(false); modp->addStmtsp(varp); // Replace expression with temporary variable cnodep->replaceWith(new AstVarRef{flp, varp, VAccess::READ}); // Add assignment driving temporary variable AstAssignW* const ap = new AstAssignW{flp, new AstVarRef{flp, varp, VAccess::WRITE}, cnodep}; modp->addStmtsp(new AstAlways{ap}); } } } void visit(AstNodeModule* nodep) override { VL_RESTORER(m_modp); m_modp = nodep; iterateChildrenConst(nodep); } void visit(AstAlways* nodep) override { VL_RESTORER(m_candidatesp); // Only extract from combinational logic under proper modules const bool isComb = !nodep->sentreep() && (nodep->keyword() == VAlwaysKwd::ALWAYS || nodep->keyword() == VAlwaysKwd::ALWAYS_COMB || nodep->keyword() == VAlwaysKwd::ALWAYS_LATCH); m_candidatesp = isComb && VN_IS(m_modp, Module) ? &m_extractionCandidates(m_modp) : nullptr; iterateChildrenConst(nodep); } void visit(AstAssignW* nodep) override { // Mark LHS variable as combinationally driven if (const AstVarRef* const vrefp = VN_CAST(nodep->lhsp(), VarRef)) { vrefp->varp()->user4(true); } // iterateChildrenConst(nodep); } void visit(AstAssign* nodep) override { iterateExtractionCandidate(nodep->rhsp()); iterate(nodep->lhsp()); } void visit(AstAssignDly* nodep) override { iterateExtractionCandidate(nodep->rhsp()); iterate(nodep->lhsp()); } void visit(AstIf* nodep) override { iterateExtractionCandidate(nodep->condp()); iterateAndNextConstNull(nodep->thensp()); iterateAndNextConstNull(nodep->elsesp()); } void visit(AstAssignForce* nodep) override { VL_RESTORER(m_inForceReleaseLhs); iterate(nodep->rhsp()); UASSERT_OBJ(!m_inForceReleaseLhs, nodep, "Should not nest"); m_inForceReleaseLhs = true; iterate(nodep->lhsp()); } void visit(AstRelease* nodep) override { VL_RESTORER(m_inForceReleaseLhs); UASSERT_OBJ(!m_inForceReleaseLhs, nodep, "Should not nest"); m_inForceReleaseLhs = true; iterate(nodep->lhsp()); } void visit(AstNodeExpr* nodep) override { iterateChildrenConst(nodep); } void visit(AstArg* nodep) override { iterateChildrenConst(nodep); } void visit(AstNodeVarRef* nodep) override { if (nodep->access().isWriteOrRW()) { // If it writes a variable, mark as impure m_impure = true; // Mark target of force/release if (m_inForceReleaseLhs) nodep->varp()->user3(true); } else { // Otherwise, add read reference m_readVars.push_back(nodep->varp()); } } void visit(AstNode* nodep) override { // Conservatively assume unhandled nodes are impure. m_impure = true; // Still need to gather all references/force/release, etc. iterateChildrenConst(nodep); } // CONSTRUCTOR explicit DataflowExtractVisitor(AstNetlist* netlistp) { iterate(netlistp); } public: static void apply(AstNetlist* netlistp) { DataflowExtractVisitor{netlistp}; } }; void V3DfgOptimizer::extract(AstNetlist* netlistp) { UINFO(2, __FUNCTION__ << ":"); // Extract more optimization candidates DataflowExtractVisitor::apply(netlistp); V3Global::dumpCheckGlobalTree("dfg-extract", 0, dumpTreeEitherLevel() >= 3); } class DataflowOptimize final { // NODE STATE // AstVar::user1, AstVarScope::user1 -> int, used as a bit-field // - bit0: Read via AstVarXRef (hierarchical reference) // - bit1: Written via AstVarXRef (hierarchical reference) // - bit2: Read 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 // - bit31-5: Reference count, how many DfgVertexVar represent this variable // // AstNode::user2/user3/user4 can be used by various DFG algorithms const VNUser1InUse m_user1InUse; // STATE V3DfgContext m_ctx; // The context holding values that need to persist across multiple graphs void endOfStage(const std::string& name) { if (VL_UNLIKELY(v3Global.opt.stats())) V3Stats::statsStage("dfg-optimize-" + name); } void endOfStage(const std::string& name, const DfgGraph& dfg, const std::vector>& componentps) { // Dump the graphs for debugging if (VL_UNLIKELY(dumpDfgLevel() >= 5)) { if (dfg.size() > 0) dfg.dumpDotFilePrefixed(name); for (const std::unique_ptr& componentp : componentps) { if (componentp->size() > 0) componentp->dumpDotFilePrefixed(name); } } // Type check the graphs if (VL_UNLIKELY(v3Global.opt.debugCheck())) { V3DfgPasses::typeCheck(dfg); for (const std::unique_ptr& componentp : componentps) { V3DfgPasses::typeCheck(*componentp); } } // Dump stage stats endOfStage(name); } // Mark variables with external references void markExternallyReferencedVariables(AstNetlist* netlistp) { netlistp->foreach([](AstNode* nodep) { // Check variable flags if (AstVarScope* const vscp = VN_CAST(nodep, VarScope)) { const AstVar* const varp = vscp->varp(); // Force and trace have already been processed const bool hasExtRd = varp->isPrimaryIO() || varp->isSigUserRdPublic(); const bool hasExtWr = (varp->isPrimaryIO() && varp->isNonOutput()) || varp->isSigUserRWPublic(); if (hasExtRd) DfgVertexVar::setHasExtRdRefs(vscp); if (hasExtWr) DfgVertexVar::setHasExtWrRefs(vscp); return; } // Check references if (const AstVarRef* const refp = VN_CAST(nodep, VarRef)) { if (refp->access().isRW()) DfgVertexVar::setHasRWRefs(refp->varScopep()); UASSERT_OBJ(!refp->classOrPackagep(), refp, "V3Scope should have removed"); return; } UASSERT_OBJ(!VN_IS(nodep, VarXRef), nodep, "V3Scope should have removed"); // Check cell ports if (const AstCell* const cellp = VN_CAST(nodep, Cell)) { // Why does this not hold? UASSERT_OBJ(true || !cellp->pinsp(), cellp, "Pins should have been lowered"); return; } }); } void optimize(DfgGraph& dfg) { // Remove unobservable variabels and logic that drives only such variables V3DfgPasses::removeUnobservable(dfg, m_ctx); endOfStage("removeUnobservable", dfg, {}); // Synthesize DfgLogic vertices V3DfgPasses::synthesize(dfg, m_ctx); endOfStage("synthesize", dfg, {}); // Extract the cyclic sub-graphs. We do this because a lot of the optimizations assume a // DAG, and large, mostly acyclic graphs could not be optimized due to the presence of // small cycles. std::vector> cyclicComps = dfg.extractCyclicComponents("cyclic"); endOfStage("extractCyclic", dfg, cyclicComps); // Attempt to convert cyclic components into acyclic ones std::vector> madeAcyclicComponents; if (v3Global.opt.fDfgBreakCycles()) { for (auto it = cyclicComps.begin(); it != cyclicComps.end();) { auto result = V3DfgPasses::breakCycles(**it, m_ctx); if (!result.first) { // No improvement, moving on. ++it; } else if (!result.second) { // Improved, but still cyclic. Replace the original cyclic component. *it = std::move(result.first); ++it; } else { // Result became acyclic. Move to madeAcyclicComponents, delete original. madeAcyclicComponents.emplace_back(std::move(result.first)); 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); // Split the acyclic DFG into [weakly] connected components std::vector> acyclicComps = dfg.splitIntoComponents("acyclic"); UASSERT(dfg.size() == 0, "DfgGraph should have become empty"); endOfStage("splitAcyclic", dfg, acyclicComps); // Optimize each acyclic component for (auto& cp : acyclicComps) V3DfgPasses::inlineVars(*cp); endOfStage("inlineVars", dfg, acyclicComps); for (auto& cp : acyclicComps) V3DfgPasses::cse(*cp, m_ctx.m_cseContext0); endOfStage("cse0", dfg, acyclicComps); for (auto& cp : acyclicComps) V3DfgPasses::binToOneHot(*cp, m_ctx.m_binToOneHotContext); endOfStage("binToOneHot", dfg, acyclicComps); for (auto& cp : acyclicComps) V3DfgPasses::peephole(*cp, m_ctx.m_peepholeContext); endOfStage("peephole", dfg, acyclicComps); 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); // Accumulate patterns for reporting if (v3Global.opt.stats()) { { V3DfgPatternStats patternStats; for (auto& cp : acyclicComps) patternStats.accumulate(*cp); } endOfStage("patterns"); } // Merge everything back under the main DFG dfg.mergeGraphs(std::move(acyclicComps)); dfg.mergeGraphs(std::move(cyclicComps)); endOfStage("optimized", dfg, {}); // Regularize the graph after merging it all back together so all // references are known and we only need to iterate the Ast once // to replace redundant variables. V3DfgPasses::regularize(dfg, m_ctx.m_regularizeContext); endOfStage("regularize", dfg, {}); } void removeNeverActives(AstNetlist* netlistp) { std::vector neverActiveps; netlistp->foreach([&](AstActive* activep) { AstSenTree* const senTreep = activep->sentreep(); if (!senTreep) return; const AstNode* const nodep = V3Const::constifyEdit(senTreep); UASSERT_OBJ(nodep == senTreep, nodep, "Should not have been repalced"); if (senTreep->sensesp()->isNever()) { UASSERT_OBJ(!senTreep->sensesp()->nextp(), nodep, "Never senitem should be alone, else the never should be eliminated."); neverActiveps.emplace_back(activep); } }); for (AstActive* const activep : neverActiveps) { VL_DO_DANGLING(activep->unlinkFrBack()->deleteTree(), activep); } } DataflowOptimize(AstNetlist* netlistp) { // Mark interfaces that might be referenced by a virtual interface if (v3Global.hasVirtIfaces()) { netlistp->typeTablep()->foreach([](const AstIfaceRefDType* nodep) { if (!nodep->isVirtual()) return; nodep->ifaceViaCellp()->setHasVirtualRef(); }); } // Mark variables with external references markExternallyReferencedVariables(netlistp); // Dump stage stats endOfStage("init"); // Post V3Scope application. Run on whole netlist. UINFO(4, "Applying DFG optimization to entire netlist"); // Build the DFG of the entire netlist const std::unique_ptr dfgp = V3DfgPasses::astToDfg(*netlistp, m_ctx); endOfStage("astToDfg", *dfgp, {}); // Actually process the graph optimize(*dfgp); // Convert back to Ast V3DfgPasses::dfgToAst(*dfgp, m_ctx); endOfStage("dfgToAst", *dfgp, {}); // Some sentrees might have become constant, remove them removeNeverActives(netlistp); // Reset interned types so the corresponding Ast types can be garbage collected DfgDataType::reset(); // Dump stage stats endOfStage("fini"); } public: static void apply(AstNetlist* netlistp) { DataflowOptimize{netlistp}; } }; void V3DfgOptimizer::optimize(AstNetlist* netlistp) { UINFO(2, __FUNCTION__ << ":"); DataflowOptimize::apply(netlistp); V3Global::dumpCheckGlobalTree("dfg-optimize", 0, dumpTreeEitherLevel() >= 3); }