verilator/src/V3CfgLiveVariables.cpp

241 lines
9.3 KiB
C++
Raw Normal View History

Optimize complex combinational logic in DFG (#6298) This patch adds DfgLogic, which is a vertex that represents a whole, arbitrarily complex combinational AstAlways or AstAssignW in the DfgGraph. Implementing this requires computing the variables live at entry to the AstAlways (variables read by the block), so there is a new ControlFlowGraph data structure and a classical data-flow analysis based live variable analysis to do that at the variable level (as opposed to bit/element level). The actual CFG construction and live variable analysis is best effort, and might fail for currently unhandled constructs or data types. This can be extended later. V3DfgAstToDfg is changed to convert the Ast into an initial DfgGraph containing only DfgLogic, DfgVertexSplice and DfgVertexVar vertices. The DfgLogic are then subsequently synthesized into primitive operations by the new V3DfgSynthesize pass, which is a combination of the old V3DfgAstToDfg conversion and new code to handle AstAlways blocks with complex flow control. V3DfgSynthesize by default will synthesize roughly the same constructs as V3DfgAstToDfg used to handle before, plus any logic that is part of a combinational cycle within the DfgGraph. This enables breaking up these cycles, for which there are extensions to V3DfgBreakCycles in this patch as well. V3DfgSynthesize will then delete all non synthesized or non synthesizable DfgLogic vertices and the rest of the Dfg pipeline is identical, with minor changes to adjust for the changed representation. Because with this change we can now eliminate many more UNOPTFLAT, DFG has been disabled in all the tests that specifically target testing the scheduling and reporting of circular combinational logic.
2025-08-19 16:06:38 +02:00
// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: CFG liveness analysis
//
// 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
//
//*************************************************************************
//
// Classical data flow analysis computing live variables input to a CFG
// https://en.wikipedia.org/wiki/Live-variable_analysis
//
//*************************************************************************
#include "config_build.h"
#include "verilatedos.h"
#include "V3Ast.h"
#include "V3Cfg.h"
#include <limits>
#include <memory>
#include <unordered_map>
VL_DEFINE_DEBUG_FUNCTIONS;
template <bool T_Scoped>
class CfgLiveVariables final : VNVisitorConst {
// TYPES
using Variable = std::conditional_t<T_Scoped, AstVarScope, AstVar>;
// State associted with each basic block
struct BlockState final {
// Variables used in block, before a complete assignment in the same block
std::unordered_set<Variable*> m_gen;
// Variables that are assigned a complete value in the basic block
std::unordered_set<Variable*> m_kill;
std::unordered_set<Variable*> m_liveIn; // Variables live on entry to the block
std::unordered_set<Variable*> m_liveOut; // Variables live on exit from the block
bool m_isOnWorkList = false; // Block is on work list
bool m_wasProcessed = false; // Already processed at least once
};
// STATE
const ControlFlowGraph& m_cfg; // The CFG beign analysed
// State for each block
BasicBlockMap<BlockState> m_blockState = m_cfg.makeBasicBlockMap<BlockState>();
BlockState* m_currp = nullptr; // State of current block being analysed
bool m_abort = false; // Abort analysis - unhandled construct
// METHODS
static Variable* getTarget(const AstNodeVarRef* refp) {
// TODO: remove the useless reinterpret_casts when C++17 'if constexpr' actually works
if VL_CONSTEXPR_CXX17 (T_Scoped) {
return reinterpret_cast<Variable*>(refp->varScopep());
} else {
return reinterpret_cast<Variable*>(refp->varp());
}
}
// This is to match DFG, but can be extended independently - eventually should handle all
static bool isSupportedPackedDType(const AstNodeDType* dtypep) {
dtypep = dtypep->skipRefp();
if (const AstBasicDType* const typep = VN_CAST(dtypep, BasicDType)) {
return typep->keyword().isIntNumeric();
}
if (const AstPackArrayDType* const typep = VN_CAST(dtypep, PackArrayDType)) {
return isSupportedPackedDType(typep->subDTypep());
}
if (const AstNodeUOrStructDType* const typep = VN_CAST(dtypep, NodeUOrStructDType)) {
return typep->packed();
}
return false;
}
// Check and return if variable is incompatible
bool incompatible(Variable* varp) {
if (!isSupportedPackedDType(varp->dtypep())) return true;
AstVar* astVarp = nullptr;
// TODO: remove the useless reinterpret_casts when C++17 'if constexpr' actually works
if VL_CONSTEXPR_CXX17 (T_Scoped) {
astVarp = reinterpret_cast<AstVarScope*>(varp)->varp();
} else {
astVarp = reinterpret_cast<AstVar*>(varp);
}
if (astVarp->ignoreSchedWrite()) return true;
return false;
}
void updateGen(AstNode* nodep) {
UASSERT_OBJ(!m_abort, nodep, "Doing useless work");
m_abort = nodep->exists([&](const AstNodeVarRef* refp) {
// Cross reference is ambiguous
if (VN_IS(refp, VarXRef)) return true;
// Only care about reads
if (refp->access().isWriteOnly()) return false;
// Grab referenced variable
Variable* const tgtp = getTarget(refp);
// Bail if not a compatible type
if (incompatible(tgtp)) return true;
// Add to gen set, if not killed - assume whole variable read
if (m_currp->m_kill.count(tgtp)) return false;
m_currp->m_gen.emplace(tgtp);
return false;
});
}
void updateKill(AstNode* nodep) {
UASSERT_OBJ(!m_abort, nodep, "Doing useless work");
m_abort = nodep->exists([&](const AstNodeVarRef* refp) {
// Cross reference is ambiguous
if (VN_IS(refp, VarXRef)) return true;
// Only care about writes
if (refp->access().isReadOnly()) return false;
// Grab referenced variable
Variable* const tgtp = getTarget(refp);
// Bail if not a compatible type
if (incompatible(tgtp)) return true;
// If whole written, add to kill set
if (refp->nextp()) return false;
if (VN_IS(refp->abovep(), Sel)) return false;
m_currp->m_kill.emplace(tgtp);
return false;
});
}
void single(AstNode* nodep) {
// Assume all reads hapen before any writes
if (m_abort) return;
updateGen(nodep);
if (m_abort) return;
updateKill(nodep);
}
// Apply transfer function of block, return true if changed
bool transfer(const BasicBlock& bb) {
BlockState& state = m_blockState[bb];
// liveIn = gen union (liveOut - kill)
std::unordered_set<Variable*> liveIn = state.m_gen;
for (Variable* const varp : state.m_liveOut) {
if (state.m_kill.count(varp)) continue;
liveIn.insert(varp);
}
if (liveIn == state.m_liveIn) return false;
std::swap(liveIn, state.m_liveIn);
return true;
}
// VISIT
void visit(AstNode* nodep) override { //
UASSERT_OBJ(!m_abort, nodep, "Repeat traversal after abort");
m_abort = true;
UINFO(9, "Unhandled AstNode type " << nodep->typeName());
}
void visit(AstAssign* nodep) override { single(nodep); }
void visit(AstDisplay* nodep) override { single(nodep); }
void visit(AstFinish* nodep) override { single(nodep); }
void visit(AstStmtExpr* nodep) override { single(nodep); }
void visit(AstStop* nodep) override { single(nodep); }
// Only the condition check belongs to the terminated basic block
void visit(AstIf* nodep) override { single(nodep->condp()); }
void visit(AstWhile* nodep) override { single(nodep->condp()); }
// CONSTRUCTOR
explicit CfgLiveVariables(const ControlFlowGraph& cfg)
: m_cfg{cfg} {
// For each basic block, compute the gen and kill set via visit
cfg.foreach([&](const BasicBlock& bb) {
if (m_abort) return;
VL_RESTORER(m_currp);
m_currp = &m_blockState[bb];
for (AstNode* const stmtp : bb.stmtps()) iterateConst(stmtp);
});
if (m_abort) return;
// Perform the flow analysis
std::deque<const BasicBlock*> workList;
const auto enqueue = [&](const BasicBlock& bb) {
BlockState& state = m_blockState[bb];
if (state.m_isOnWorkList) return;
state.m_isOnWorkList = true;
workList.emplace_back(&bb);
};
enqueue(cfg.exit());
while (!workList.empty()) {
const BasicBlock* const currp = workList.front();
workList.pop_front();
BlockState& state = m_blockState[*currp];
state.m_isOnWorkList = false;
// Compute meet (liveOut = union liveIn of successors)
currp->forEachSuccessor([&](const BasicBlock& bb) {
auto& liveIn = m_blockState[bb].m_liveIn;
state.m_liveOut.insert(liveIn.begin(), liveIn.end());
});
// Apply transfer function of block
const bool changed = transfer(*currp);
// Enqueue predecessors
if (changed || !state.m_wasProcessed) currp->forEachPredecessor(enqueue);
// Mark as done with first visit
state.m_wasProcessed = true;
}
}
public:
static std::unique_ptr<std::vector<Variable*>> apply(const ControlFlowGraph& cfg) {
CfgLiveVariables analysis{cfg};
// If failed, return nullptr
if (analysis.m_abort) return nullptr;
// Gather variables live in to the entry blockstd::unique_ptr<std::vector<Variable*>>
const std::unordered_set<Variable*>& lin = analysis.m_blockState[cfg.enter()].m_liveIn;
std::vector<Variable*>* const resultp = new std::vector<Variable*>{lin.begin(), lin.end()};
// Sort for stability
std::stable_sort(resultp->begin(), resultp->end(), [](Variable* ap, Variable* bp) { //
return ap->name() < bp->name();
});
return std::unique_ptr<std::vector<Variable*>>{resultp};
}
};
std::unique_ptr<std::vector<AstVar*>> V3Cfg::liveVars(const ControlFlowGraph& cfg) {
return CfgLiveVariables</* T_Scoped: */ false>::apply(cfg);
}
std::unique_ptr<std::vector<AstVarScope*>> V3Cfg::liveVarScopes(const ControlFlowGraph& cfg) {
return CfgLiveVariables</* T_Scoped: */ true>::apply(cfg);
}