verilator/src/V3DfgOptimizer.cpp

382 lines
16 KiB
C++

// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Dataflow based optimization of combinational logic
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// Copyright 2003-2025 by Wilson Snyder. 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-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 "V3Dfg.h"
#include "V3DfgPasses.h"
#include "V3Graph.h"
#include "V3UniqueNames.h"
#include <vector>
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<std::pair<AstNodeExpr*, std::vector<const AstVar*>>>;
// Expressions considered for extraction. All the candidates are pure expressions.
AstUser4Allocator<AstNodeModule, Candidates> 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<const AstVar*> 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
modp->addStmtsp(
new AstAssignW{flp, new AstVarRef{flp, varp, VAccess::WRITE}, cnodep});
}
}
}
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(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. This covers all AstNodeFTaskRef
// as AstNodeFTaskRef are sadly not AstNodeExpr.
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
// - bit31-4: 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 optimize(DfgGraph& dfg) {
if (dumpDfgLevel() >= 8) dfg.dumpDotFilePrefixed(m_ctx.prefix() + "dfg-in");
// Synthesize DfgLogic vertices
V3DfgPasses::synthesize(dfg, m_ctx);
if (dumpDfgLevel() >= 8) dfg.dumpDotFilePrefixed(m_ctx.prefix() + "synth");
// 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<std::unique_ptr<DfgGraph>> cyclicComponents
= dfg.extractCyclicComponents("cyclic");
// Attempt to convert cyclic components into acyclic ones
std::vector<std::unique_ptr<DfgGraph>> madeAcyclicComponents;
if (v3Global.opt.fDfgBreakCycles()) {
for (auto it = cyclicComponents.begin(); it != cyclicComponents.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 = cyclicComponents.erase(it);
}
}
}
// Merge those that were made acyclic back to the graph, this enables optimizing more
dfg.mergeGraphs(std::move(madeAcyclicComponents));
// Split the acyclic DFG into [weakly] connected components
std::vector<std::unique_ptr<DfgGraph>> acyclicComponents
= dfg.splitIntoComponents("acyclic");
// Quick sanity check
UASSERT(dfg.size() == 0, "DfgGraph should have become empty");
// For each acyclic component
for (const std::unique_ptr<DfgGraph>& component : acyclicComponents) {
// Optimize the component
V3DfgPasses::optimize(*component, m_ctx);
}
// Merge back under the main DFG (we will convert everything back in one go)
dfg.mergeGraphs(std::move(acyclicComponents));
// Eliminate redundant variables. Run this on the whole acyclic DFG. It needs to traverse
// the module/netlist to perform variable substitutions. Doing this by component would do
// redundant traversals and can be extremely slow when we have many components.
V3DfgPasses::eliminateVars(dfg, m_ctx.m_eliminateVarsContext);
// For each cyclic component
for (const std::unique_ptr<DfgGraph>& component : cyclicComponents) {
// Converting back to Ast assumes the 'regularize' pass was run, so we must run it
V3DfgPasses::regularize(*component, m_ctx.m_regularizeContext);
}
// Merge back under the main DFG (we will convert everything back in one go)
dfg.mergeGraphs(std::move(cyclicComponents));
if (dumpDfgLevel() >= 8) dfg.dumpDotFilePrefixed(m_ctx.prefix() + "dfg-out");
}
DataflowOptimize(AstNetlist* netlistp, const string& label)
: m_ctx{label} {
// Mark interfaces that might be referenced by a virtual interface
if (v3Global.hasVirtIfaces()) {
netlistp->typeTablep()->foreach([](AstIfaceRefDType* nodep) {
if (!nodep->isVirtual()) return;
nodep->ifaceViaCellp()->setHasVirtualRef();
});
}
if (!netlistp->topScopep()) {
// Pre V3Scope application. Run on each module separately.
// Mark cross-referenced variables
netlistp->foreach([](const AstVarXRef* xrefp) {
AstVar* const tgtp = xrefp->varp();
if (xrefp->access().isReadOrRW()) DfgVertexVar::setHasRdXRefs(tgtp);
if (xrefp->access().isWriteOrRW()) DfgVertexVar::setHasWrXRefs(tgtp);
});
// Run the optimization
for (AstNode* nodep = netlistp->modulesp(); nodep; nodep = nodep->nextp()) {
// Only optimize proper modules
AstModule* const modp = VN_CAST(nodep, Module);
if (!modp) continue;
// Pre V3Scope application. Run on module.
UINFO(4, "Applying DFG optimization to module '" << modp->name() << "'");
++m_ctx.m_modules;
// Build the DFG of this module or netlist
const std::unique_ptr<DfgGraph> dfgp = V3DfgPasses::astToDfg(*modp, m_ctx);
// Actually process the graph
optimize(*dfgp);
// Convert back to Ast
V3DfgPasses::dfgToAst(*dfgp, m_ctx);
}
} else {
// 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<DfgGraph> dfgp = V3DfgPasses::astToDfg(*netlistp, m_ctx);
// Actually process the graph
optimize(*dfgp);
// Convert back to Ast
V3DfgPasses::dfgToAst(*dfgp, m_ctx);
}
}
public:
static void apply(AstNetlist* netlistp, const string& label) {
DataflowOptimize{netlistp, label};
}
};
void V3DfgOptimizer::optimize(AstNetlist* netlistp, const string& label) {
UINFO(2, __FUNCTION__ << ":");
DataflowOptimize::apply(netlistp, label);
V3Global::dumpCheckGlobalTree("dfg-optimize", 0, dumpTreeEitherLevel() >= 3);
}