Improve Loop unrolling (#6480) (#6493)

This patch implements #6480. All loop statements are represented using
AstLoop and AstLoopTest.

This necessitates rework of the loop unroller to handle loops of
arbitrary form. To enable this, I have split the old unroller used for
'generate for' statements and moved it into V3Param, and subsequently
rewrote V3Unroll to handle the new representation. V3Unroll can now
unroll more complex loops, including with loop conditions containing
multiple variable references or inlined functions.

Handling the more generic code also requires some restrictions. If a
loop contains any of the following, it cannot be unrolled:
- A timing control that might suspend the loop
- A non-inlined call to a non-pure function

These constructs can change the values of variables in the loop, so are
generally not safe to unroll if they are present. (We could still unroll
if all the variables needed for unrolling are automatic, however we
don't do that right now.)

These restrictions seem ok in the benchmark suite, where the new
unroller can generally unroll many more loops than before.
This commit is contained in:
Geza Lore
2025-09-29 15:25:25 +01:00
committed by GitHub
parent 5c72b45975
commit 603f4c615a
59 changed files with 3151 additions and 2891 deletions
+324 -445
View File
@@ -1,6 +1,6 @@
// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Add temporaries, such as for unroll nodes
// DESCRIPTION: Verilator: Loop unrolling
//
// Code available from: https://verilator.org
//
@@ -13,14 +13,8 @@
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//*************************************************************************
// V3Unroll's Transformations:
// Note is called twice. Once on modules for GenFor unrolling,
// Again after V3Scope for normal for loop unrolling.
//
// Each module:
// Look for "FOR" loops and unroll them if <= 32 loops.
// (Eventually, a better way would be to simulate the entire loop; ala V3Table.)
// Convert remaining FORs to WHILEs
// Unroll AstLoopStmts
//
//*************************************************************************
@@ -29,489 +23,374 @@
#include "V3Unroll.h"
#include "V3Const.h"
#include "V3Simulate.h"
#include "V3Stats.h"
VL_DEFINE_DEBUG_FUNCTIONS;
//######################################################################
// Unroll state, as a visitor of each AstNode
// Statistics tracking
class UnrollVisitor final : public VNVisitor {
// STATE - across all visitors
AstVar* m_forVarp; // Iterator variable
const AstVarScope* m_forVscp; // Iterator variable scope (nullptr for generate pass)
const AstNode* m_ignoreIncp; // Increment node to ignore
bool m_varModeCheck; // Just checking RHS assignments
bool m_varAssignHit; // Assign var hit
bool m_forkHit; // Fork hit
bool m_generate; // Expand single generate For loop
string m_beginName; // What name to give begin iterations
// STATE - Statistic tracking
VDouble0 m_statLoops; // Statistic tracking
VDouble0 m_statIters; // Statistic tracking
struct UnrollStats final {
class Stat final {
size_t m_value = 0; // Statistics value
const char* const m_name; // Name for stats file and UDEBUG
public:
Stat(const char* const name)
: m_name{name} {}
~Stat() { V3Stats::addStat("Optimizations, Loop unrolling, "s + m_name, m_value); }
const char* name() const { return m_name; }
Stat& operator++() {
++m_value;
return *this;
}
Stat& operator+=(size_t v) {
m_value += v;
return *this;
}
};
// STATE - statistics
Stat m_nFailCall{"Failed - non-inlined call"};
Stat m_nFailCondition{"Failed - unknown loop condition"};
Stat m_nFailFork{"Failed - contains fork"};
Stat m_nFailInfinite{"Failed - infinite loop"};
Stat m_nFailNestedLoopTest{"Failed - loop test in sub-statement"};
Stat m_nFailTimingControl{"Failed - contains timing control"};
Stat m_nFailUnrollCount{"Failed - reached --unroll-count"};
Stat m_nFailUnrollStmts{"Failed - reached --unroll-stmts"};
Stat m_nPragmaDisabled{"Pragma unroll_disable"};
Stat m_nUnrolledLoops{"Unrolled loops"};
Stat m_nUnrolledIters{"Unrolled iterations"};
};
//######################################################################
// Unroll one AstLoop
class UnrollOneVisitor final : VNVisitor {
// NODE STATE
// AstVarScope::user1p() AstConst: Value of this AstVarScope
const VNUser1InUse m_user1InUse;
// STATE
UnrollStats& m_stats; // Statistics tracking
AstLoop* const m_loopp; // The loop we are trying to unroll
AstNode* m_stmtsp = nullptr; // Resulting unrolled statement
size_t m_unrolledSize = 0; // Number of nodes in unrolled loop
// Temporary block needed for iteration of cloned statements
AstBegin* const m_wrapp = new AstBegin{m_loopp->fileline(), "[EditWrapper]", nullptr, false};
const bool m_unrollFull = m_loopp->unroll().isSetTrue(); // Completely unroll the loop?
bool m_ok = true; // Unrolling successful so far, gave up if false
// METHODS
void replaceVarRef(AstNode* bodyp, AstNode* varValuep) {
// Replace all occurances of loop variable in bodyp and next
bodyp->foreachAndNext([this, varValuep](AstVarRef* refp) {
if (refp->varp() == m_forVarp && refp->varScopep() == m_forVscp
&& refp->access().isReadOnly()) {
AstNode* const newconstp = varValuep->cloneTree(false);
refp->replaceWith(newconstp);
VL_DO_DANGLING(pushDeletep(refp), refp);
}
});
void cantUnroll(AstNode* nodep, UnrollStats::Stat& stat) {
m_ok = false;
++stat;
UINFO(4, " Can't Unroll: " << stat.name() << " :" << nodep);
}
bool cantUnroll(AstNode* nodep, const char* reason) const {
if (m_generate)
nodep->v3warn(E_UNSUPPORTED, "Unsupported: Can't unroll generate for; " << reason);
UINFO(4, " Can't Unroll: " << reason << " :" << nodep);
// UINFOTREE(9, nodep, "", "cant");
V3Stats::addStatSum("Unrolling gave up, "s + reason, 1);
return false;
}
bool bodySizeOverRecurse(AstNode* nodep, int& bodySize, int bodyLimit) {
if (!nodep) return false;
bodySize++;
// Exit once exceeds limits, rather than always total
// so don't go O(n^2) when can't unroll
if (bodySize > bodyLimit) return true;
if (bodySizeOverRecurse(nodep->op1p(), bodySize, bodyLimit)) return true;
if (bodySizeOverRecurse(nodep->op2p(), bodySize, bodyLimit)) return true;
if (bodySizeOverRecurse(nodep->op3p(), bodySize, bodyLimit)) return true;
if (bodySizeOverRecurse(nodep->op4p(), bodySize, bodyLimit)) return true;
// Tail recurse.
return bodySizeOverRecurse(nodep->nextp(), bodySize, bodyLimit);
}
bool forUnrollCheck(
AstNode* const nodep,
const VOptionBool& unrollFull, // Pragma unroll_full, unroll_disable
AstNode* const initp, // Maybe under nodep (no nextp), or standalone (ignore nextp)
AstNode* condp,
AstNode* const incp, // Maybe under nodep or in bodysp
AstNode* bodysp) {
// To keep the IF levels low, we return as each test fails.
UINFO(4, " FOR Check " << nodep);
if (initp) UINFO(6, " Init " << initp);
if (condp) UINFO(6, " Cond " << condp);
if (incp) UINFO(6, " Inc " << incp);
if (unrollFull.isSetFalse()) return cantUnroll(nodep, "pragma unroll_disable");
// Initial value check
AstAssign* const initAssp = VN_CAST(initp, Assign);
if (!initAssp) return cantUnroll(nodep, "no initial assignment");
UASSERT_OBJ(!(initp->nextp() && initp->nextp() != nodep), nodep,
"initial assignment shouldn't be a list");
if (!VN_IS(initAssp->lhsp(), VarRef)) {
return cantUnroll(nodep, "no initial assignment to simple variable");
}
//
// Condition check
UASSERT_OBJ(!condp->nextp(), nodep, "conditional shouldn't be a list");
//
// Assignment of next value check
const AstAssign* const incAssp = VN_CAST(incp, Assign);
if (!incAssp) return cantUnroll(nodep, "no increment assignment");
if (incAssp->nextp()) return cantUnroll(nodep, "multiple increments");
m_forVarp = VN_AS(initAssp->lhsp(), VarRef)->varp();
m_forVscp = VN_AS(initAssp->lhsp(), VarRef)->varScopep();
if (VN_IS(nodep, GenFor) && !m_forVarp->isGenVar()) {
nodep->v3error("Non-genvar used in generate for: " << m_forVarp->prettyNameQ());
} else if (!VN_IS(nodep, GenFor) && m_forVarp->isGenVar()) {
// Likely impossible as V3LinkResolve will earlier throw bad genvar use error
nodep->v3error("Genvar not legal in non-generate for" // LCOV_EXCL_LINE
" (IEEE 1800-2023 27.4): "
<< m_forVarp->prettyNameQ());
}
if (m_generate) V3Const::constifyParamsEdit(initAssp->rhsp()); // rhsp may change
// This check shouldn't be needed when using V3Simulate
// however, for repeat loops, the loop variable is auto-generated
// and the initp statements will reference a variable outside of the initp scope
// alas, failing to simulate.
const AstConst* const constInitp = VN_CAST(initAssp->rhsp(), Const);
if (!constInitp) return cantUnroll(nodep, "non-constant initializer");
//
// Now, make sure there's no assignment to this variable in the loop
m_varModeCheck = true;
m_varAssignHit = false;
m_forkHit = false;
m_ignoreIncp = incp;
iterateAndNextNull(bodysp);
iterateAndNextNull(incp);
m_varModeCheck = false;
m_ignoreIncp = nullptr;
if (m_varAssignHit) return cantUnroll(nodep, "genvar assigned *inside* loop");
if (m_forkHit) return cantUnroll(nodep, "fork inside loop");
//
if (m_forVscp) {
UINFO(8, " Loop Variable: " << m_forVscp);
} else {
UINFO(8, " Loop Variable: " << m_forVarp);
}
UINFOTREE(9, nodep, "", "for");
if (!m_generate) {
const AstAssign* const incpAssign = VN_AS(incp, Assign);
if (!canSimulate(incpAssign->rhsp())) {
return cantUnroll(incp, "Unable to simulate increment");
}
if (!canSimulate(condp)) return cantUnroll(condp, "Unable to simulate condition");
// Check whether to we actually want to try and unroll.
int loops;
const int limit = v3Global.opt.unrollCountAdjusted(unrollFull, m_generate, false);
if (!countLoops(initAssp, condp, incp, limit, loops)) {
return cantUnroll(nodep, "Unable to simulate loop");
}
// Less than 10 statements in the body?
if (!unrollFull.isSetTrue()) {
int bodySize = 0;
int bodyLimit = v3Global.opt.unrollStmts();
if (loops > 0) bodyLimit = v3Global.opt.unrollStmts() / loops;
if (bodySizeOverRecurse(bodysp, bodySize /*ref*/, bodyLimit)
|| bodySizeOverRecurse(incp, bodySize /*ref*/, bodyLimit)) {
return cantUnroll(nodep, "too many statements");
}
}
}
// Finally, we can do it
if (!forUnroller(nodep, unrollFull, initAssp, condp, incp, bodysp)) {
return cantUnroll(nodep, "Unable to unroll loop");
}
VL_DANGLING(nodep);
// Cleanup
return true;
}
bool canSimulate(AstNode* nodep) {
SimulateVisitor simvis;
AstNode* clonep = nodep->cloneTree(true);
simvis.mainCheckTree(clonep);
VL_DO_CLEAR(pushDeletep(clonep), clonep = nullptr);
return simvis.optimizable();
}
bool simulateTree(AstNode* nodep, const V3Number* loopValue, AstNode* dtypep,
V3Number& outNum) {
AstNode* clonep = nodep->cloneTree(true);
UASSERT_OBJ(clonep, nodep, "Failed to clone tree");
if (loopValue) {
AstConst* varValuep = new AstConst{nodep->fileline(), *loopValue};
// Iteration requires a back, so put under temporary node
AstBegin* tempp = new AstBegin{nodep->fileline(), "[EditWrapper]", clonep, false};
replaceVarRef(tempp->stmtsp(), varValuep);
clonep = tempp->stmtsp()->unlinkFrBackWithNext();
VL_DO_CLEAR(tempp->deleteTree(), tempp = nullptr);
VL_DO_DANGLING(pushDeletep(varValuep), varValuep);
}
SimulateVisitor simvis;
simvis.mainParamEmulate(clonep);
if (!simvis.optimizable()) {
UINFO(4, "Unable to simulate");
UINFOTREE(9, nodep, "", "_simtree");
VL_DO_DANGLING(clonep->deleteTree(), clonep);
return false;
}
// Fetch the result
V3Number* resp = simvis.fetchNumberNull(clonep);
if (!resp) {
UINFO(3, "No number returned from simulation");
VL_DO_DANGLING(clonep->deleteTree(), clonep);
return false;
}
// Patch up datatype
if (dtypep) {
AstConst new_con{clonep->fileline(), *resp};
new_con.dtypeFrom(dtypep);
outNum = new_con.num();
outNum.isSigned(dtypep->isSigned());
VL_DO_DANGLING(clonep->deleteTree(), clonep);
return true;
}
outNum = *resp;
VL_DO_DANGLING(clonep->deleteTree(), clonep);
return true;
}
bool countLoops(AstAssign* initp, AstNode* condp, AstNode* incp, int max, int& outLoopsr) {
outLoopsr = 0;
V3Number loopValue{initp};
if (!simulateTree(initp->rhsp(), nullptr, initp, loopValue)) { //
return false;
}
while (true) {
V3Number res{initp};
if (!simulateTree(condp, &loopValue, nullptr, res)) { //
return false;
}
if (!res.isEqOne()) break;
outLoopsr++;
// Run inc
AstAssign* const incpass = VN_AS(incp, Assign);
V3Number newLoopValue{initp};
if (!simulateTree(incpass->rhsp(), &loopValue, incpass, newLoopValue)) {
return false;
}
loopValue.opAssign(newLoopValue);
if (outLoopsr > max) return false;
}
return true;
}
bool forUnroller(AstNode* nodep, const VOptionBool& unrollFull, AstAssign* initp,
AstNode* condp, AstNode* incp, AstNode* bodysp) {
UINFO(9, "forUnroller " << nodep);
V3Number loopValue{nodep};
if (!simulateTree(initp->rhsp(), nullptr, initp, loopValue)) { //
return false;
}
AstNode* stmtsp = nullptr;
if (initp) {
initp->unlinkFrBack(); // Always a single statement; nextp() may be nodep
// Don't add to list, we do it once, and setting loop index isn't
// needed if we have > 1 loop, as we're constant propagating it
pushDeletep(initp); // Always cloned below.
}
if (bodysp) {
bodysp->unlinkFrBackWithNext();
stmtsp = AstNode::addNext(stmtsp, bodysp); // Maybe null if no body
}
if (incp && !VN_IS(nodep, GenFor)) { // Generates don't need to increment loop index
incp->unlinkFrBackWithNext();
stmtsp = AstNode::addNext(stmtsp, incp); // Maybe null if no body
}
// Mark variable to disable some later warnings
m_forVarp->usedLoopIdx(true);
++m_statLoops;
AstNode* newbodysp = nullptr;
if (initp && !m_generate) { // Set variable to initial value (may optimize away later)
AstNode* clonep = initp->cloneTree(true);
AstConst* varValuep = new AstConst{nodep->fileline(), loopValue};
// Iteration requires a back, so put under temporary node
AstBegin* tempp = new AstBegin{nodep->fileline(), "[EditWrapper]", clonep, false};
replaceVarRef(clonep, varValuep);
clonep = tempp->stmtsp()->unlinkFrBackWithNext();
VL_DO_CLEAR(tempp->deleteTree(), tempp = nullptr);
VL_DO_DANGLING(pushDeletep(varValuep), varValuep);
newbodysp = clonep;
}
if (stmtsp) {
pushDeletep(stmtsp); // Always cloned below.
int times = 0;
while (true) {
UINFO(8, " Looping " << loopValue);
V3Number res{nodep};
if (!simulateTree(condp, &loopValue, nullptr, res)) {
nodep->v3error("Loop unrolling failed.");
// Returns false if the loop terminated (or we gave up)
bool unrollOneIteration(AstNode* stmtsp) {
// True if the loop contains at least one dependent AstLoopTest
bool foundLoopTest = false;
// Process one body statement at a time
for (AstNode* stmtp = stmtsp; stmtp; stmtp = stmtp->nextp()) {
// Check if this is a loop test - before substitution
if (AstLoopTest* const testp = VN_CAST(stmtp, LoopTest)) {
AstNode* const condp = V3Const::constifyEdit(testp->condp());
if (condp->isZero()) {
// Loop terminates
return false;
} else if (condp->isNeqZero()) {
// Loop test is unconditionally true, ignore
continue;
}
if (!res.isEqOne()) {
break; // Done with the loop
} else {
// Replace iterator values with constant
AstNode* oneloopp = stmtsp->cloneTree(true);
AstConst* varValuep = new AstConst{nodep->fileline(), loopValue};
if (oneloopp) {
// Iteration requires a back, so put under temporary node
AstBegin* const tempp
= new AstBegin{oneloopp->fileline(), "[EditWrapper]", oneloopp, false};
replaceVarRef(tempp->stmtsp(), varValuep);
oneloopp = tempp->stmtsp()->unlinkFrBackWithNext();
VL_DO_DANGLING(tempp->deleteTree(), tempp);
}
if (m_generate) {
const string index = AstNode::encodeNumber(varValuep->toSInt());
const string nname = m_beginName + "__BRA__" + index + "__KET__";
oneloopp = new AstGenBlock{oneloopp->fileline(), nname, oneloopp, false};
}
VL_DO_DANGLING(pushDeletep(varValuep), varValuep);
if (newbodysp) {
newbodysp->addNext(oneloopp);
} else {
newbodysp = oneloopp;
}
}
++m_statIters;
const int limit
= v3Global.opt.unrollCountAdjusted(unrollFull, m_generate, false);
if (++times / 3 > limit) {
nodep->v3error(
"Loop unrolling took too long;"
" probably this is an infinite loop, "
" or use /*verilator unroll_full*/, or set --unroll-count above "
<< times);
break;
}
// Clone and iterate one body statement
m_wrapp->addStmtsp(stmtp->cloneTree(false));
iterateAndNextNull(m_wrapp->stmtsp());
// Give up if failed
if (!m_ok) return false;
// loopValue += valInc
AstAssign* const incpass = VN_AS(incp, Assign);
V3Number newLoopValue{nodep};
if (!simulateTree(incpass->rhsp(), &loopValue, incpass, newLoopValue)) {
nodep->v3error("Loop unrolling failed");
// Add statements to unrolled body
while (AstNode* const nodep = m_wrapp->stmtsp()) {
// Check if we reached the size limit, unless full unrolling is requested
if (!m_loopp->unroll().isSetTrue()) {
m_unrolledSize += nodep->nodeCount();
if (m_unrolledSize > static_cast<size_t>(v3Global.opt.unrollStmts())) {
cantUnroll(m_loopp, m_stats.m_nFailUnrollStmts);
return false;
}
loopValue.opAssign(newLoopValue);
}
// Will be adding to results (or deleting)
nodep->unlinkFrBack();
// If a LoopTest, check how it resolved
if (AstLoopTest* const testp = VN_CAST(nodep, LoopTest)) {
foundLoopTest = true;
// Will not actually need it, nor any subsequent
pushDeletep(testp);
// Loop continues - add rest of statements
if (testp->condp()->isNeqZero()) continue;
// Won't need any of the trailing statements
if (m_wrapp->stmtsp()) pushDeletep(m_wrapp->stmtsp()->unlinkFrBackWithNext());
// Loop terminates
if (testp->condp()->isZero()) return false;
// Loop condition unknown - cannot unroll
cantUnroll(testp->condp(), m_stats.m_nFailCondition);
return false;
}
// Add this statement to the result list
m_stmtsp = AstNode::addNext(m_stmtsp, nodep);
// Check if terminated via JumpGo
if (VN_IS(nodep, JumpGo)) {
UASSERT_OBJ(!m_wrapp->stmtsp(), nodep, "Statements after JumpGo");
// This JumpGo is going directly into the body of the unrolled loop.
// A JumpGo always redirects to the end of an enclosing JumpBlock,
// so this JumpGo must go outside the loop. The loop terminates.
return false;
}
}
}
if (!newbodysp) { // initp might have effects after the loop
if (m_generate && initp) { // GENFOR(ASSIGN(...)) need to move under a new Initial
newbodysp = new AstInitial{initp->fileline(), initp->cloneTree(true)};
} else {
newbodysp = initp ? initp->cloneTree(true) : nullptr;
}
// If there is no loop test in the body, give up, it's an infinite loop
if (!foundLoopTest) {
cantUnroll(m_loopp, m_stats.m_nFailInfinite);
return false;
}
// Replace the FOR()
if (newbodysp) {
nodep->replaceWith(newbodysp);
} else {
nodep->unlinkFrBack();
}
if (newbodysp) UINFOTREE(9, newbodysp, "", "_new");
// One iteration done, loop continues
return true;
}
// VISITORS
void visit(AstWhile* nodep) override {
iterateChildren(nodep);
if (!m_varModeCheck) {
// Constify before unroll call, as it may change what is underneath.
if (nodep->condp()) V3Const::constifyEdit(nodep->condp()); // condp may change
// Grab initial value
AstNode* initp = nullptr; // Should be statement before the while.
if (nodep->backp()->nextp() == nodep) initp = nodep->backp();
if (initp) VL_DO_DANGLING(V3Const::constifyEdit(initp), initp);
if (nodep->backp()->nextp() == nodep) initp = nodep->backp();
// Grab assignment
AstNode* incp = nullptr; // Should be last statement
AstNode* stmtsp = nodep->stmtsp();
if (nodep->incsp()) V3Const::constifyEdit(nodep->incsp());
// cppcheck-suppress duplicateCondition
if (nodep->incsp()) {
incp = nodep->incsp();
} else {
for (incp = nodep->stmtsp(); incp && incp->nextp(); incp = incp->nextp()) {}
if (incp) VL_DO_DANGLING(V3Const::constifyEdit(incp), incp);
// Again, as may have changed
stmtsp = nodep->stmtsp();
for (incp = nodep->stmtsp(); incp && incp->nextp(); incp = incp->nextp()) {}
if (incp == stmtsp) stmtsp = nullptr;
// Substitute all reads of bound variables with their value. If a write is
// encountered, remove the binding and don't substitute that variable.
// Returns false if we can't unroll
bool process(AstNode* nodep) {
UASSERT_OBJ(m_ok, nodep, "Should not call 'substituteCondVscp' if we gave up");
if (!nodep) return true;
// Variable references we should try to substitute
std::vector<AstVarRef*> toSubstitute;
// Iterate subtree
nodep->foreach([&](AstNode* np) {
// Failed earlier
if (!m_ok) return;
// Check for AstLoopTest
if (AstLoopTest* const testp = VN_CAST(np, LoopTest)) {
// Nested loop is OK, bail only if the nested LoopTest is for the current loop
if (testp->loopp() == m_loopp) cantUnroll(np, m_stats.m_nFailNestedLoopTest);
return;
}
// And check it
if (forUnrollCheck(nodep, nodep->unrollFull(), initp, nodep->condp(), incp, stmtsp)) {
VL_DO_DANGLING(pushDeletep(nodep), nodep); // Did replacement
}
}
}
void visit(AstGenFor* nodep) override {
UASSERT_OBJ(m_generate, nodep, "There should be no GenFor left when unrolling all");
if (!m_varModeCheck) {
// Constify before unroll call, as it may change what is underneath.
if (nodep->initsp()) V3Const::constifyEdit(nodep->initsp()); // initsp may change
if (nodep->condp()) V3Const::constifyEdit(nodep->condp()); // condp may change
if (nodep->incsp()) V3Const::constifyEdit(nodep->incsp()); // incsp may change
if (nodep->condp()->isZero()) {
// We don't need to do any loops. Remove the GenFor,
// Genvar's don't care about any initial assignments.
//
// Note normal For's can't do exactly this deletion, as
// we'd need to initialize the variable to the initial
// condition, but they'll become while's which can be
// deleted by V3Const.
VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
} else if (forUnrollCheck(nodep, VOptionBool{}, nodep->initsp(), nodep->condp(),
nodep->incsp(), nodep->itemsp())) {
VL_DO_DANGLING(pushDeletep(nodep), nodep); // Did replacement
} else {
nodep->v3error("For loop doesn't have genvar index, or is malformed");
// We will die, do it gracefully
VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
// Check for AstFork - can't unroll
if (VN_IS(np, Fork)) {
cantUnroll(np, m_stats.m_nFailFork);
return;
}
// Check for calls - can't unroll if not pure might modify bindings
if (AstNodeCCall* const callp = VN_CAST(np, NodeCCall)) {
if (!callp->isPure()) {
cantUnroll(np, m_stats.m_nFailCall);
return;
}
}
// Check for timing control - can't unroll, might modify bindings
if (np->isTimingControl()) {
cantUnroll(np, m_stats.m_nFailTimingControl);
return;
}
// Process variable references
AstVarRef* const refp = VN_CAST(np, VarRef);
if (!refp) return;
// Ignore if the referenced variable has no binding
AstConst* const valp = VN_AS(refp->varScopep()->user1p(), Const);
if (!valp) return;
// If writen, remove the binding
if (refp->access().isWriteOrRW()) {
refp->varScopep()->user1p(nullptr);
return;
}
// Otherwise add it to the list of variables to substitute
toSubstitute.push_back(refp);
});
// Give up if we have to
if (!m_ok) return false;
// Actually substitute the variables that still have bindings
for (AstVarRef* const refp : toSubstitute) {
// Pick up bound value
AstConst* const valp = VN_AS(refp->varScopep()->user1p(), Const);
// Binding might have been removed after adding to 'toSubstitute'
if (!valp) continue;
// Substitute it
refp->replaceWith(valp->cloneTree(false));
VL_DO_DANGLING(pushDeletep(refp), refp);
}
return true;
}
void visit(AstVarRef* nodep) override {
if (m_varModeCheck && nodep->varp() == m_forVarp && nodep->varScopep() == m_forVscp
&& nodep->access().isWriteOrRW()) {
UINFO(8, " Itervar assigned to: " << nodep);
m_varAssignHit = true;
// CONSTRUCTOR
UnrollOneVisitor(UnrollStats& stats, AstLoop* loopp)
: m_stats{stats}
, m_loopp{loopp} {
UASSERT_OBJ(!loopp->contsp(), loopp, "'contsp' only used before LinkJump");
// Do not unroll if we are told not to
if (loopp->unroll().isSetFalse()) {
cantUnroll(loopp, m_stats.m_nPragmaDisabled);
return;
}
}
void visit(AstFork* nodep) override {
if (m_varModeCheck) {
if (nodep->joinType().joinNone() || nodep->joinType().joinAny()) {
// Forks are not allowed to unroll for loops, so we just set a flag
m_forkHit = true;
// Gather variable bindings from the preceding statements
for (AstNode *succp = loopp, *currp = loopp->backp(); currp->nextp() == succp;
succp = currp, currp = currp->backp()) {
AstAssign* const assignp = VN_CAST(currp, Assign);
if (!assignp) break;
AstConst* const valp = VN_CAST(V3Const::constifyEdit(assignp->rhsp()), Const);
if (!valp) break;
AstVarRef* const lhsp = VN_CAST(assignp->lhsp(), VarRef);
if (!lhsp) break;
// Don't bind if volatile
if (lhsp->varp()->isForced() || lhsp->varp()->isSigUserRWPublic()) continue;
// Don't overwrite a later binding
if (lhsp->varScopep()->user1p()) continue;
// Set up the binding
lhsp->varScopep()->user1p(valp);
}
// Attempt to unroll the loop
const size_t iterLimit = v3Global.opt.unrollCountAdjusted(loopp->unroll(), false, false);
size_t iterCount = 0;
do {
// Allow iterLimit + 1 iterations, which is consistent with the old behaviour
// where 'do' loops used to be unrolled at least once, and while/for loops are
// tested at the front on the last entry to the loop body
if (iterCount > iterLimit) {
cantUnroll(m_loopp, m_stats.m_nFailUnrollCount);
return;
}
} else {
iterateChildren(nodep);
++iterCount;
} while (unrollOneIteration(loopp->stmtsp()));
if (m_ok) {
++m_stats.m_nUnrolledLoops;
m_stats.m_nUnrolledIters += iterCount;
}
}
~UnrollOneVisitor() { VL_DO_DANGLING(m_wrapp->deleteTree(), m_wrapp); }
// VISIT - these are called for the statements directly in the loop body
void visit(AstNode* nodep) override {
if (m_varModeCheck && nodep == m_ignoreIncp) {
// Ignore subtree that is the increment
} else {
iterateChildren(nodep);
if (!m_ok) return;
// Generic body statement, just substitute
process(nodep);
}
void visit(AstLoopTest* nodep) override {
if (!m_ok) return;
// If the condition is a ExprStmt, move it before the LoopTest
if (AstExprStmt* const exprp = VN_CAST(nodep->condp(), ExprStmt)) {
AstNode* const stmtsp = exprp->stmtsp()->unlinkFrBackWithNext();
exprp->replaceWith(exprp->resultp()->unlinkFrBack());
VL_DO_DANGLING(pushDeletep(exprp), exprp);
VNRelinker relinker;
nodep->unlinkFrBack(&relinker);
stmtsp->addNext(nodep);
relinker.relink(stmtsp);
return;
}
// Substitute the condition only, this is not a nested AstLoopTest
process(nodep->condp());
// Also simplify it, it will be checked later
V3Const::constifyEdit(nodep->condp());
}
void visit(AstAssign* nodep) override {
if (!m_ok) return;
// Can't do it if delayed
if (nodep->timingControlp()) {
cantUnroll(nodep, m_stats.m_nFailTimingControl);
return;
}
if (!process(nodep->rhsp())) return;
// If a simple variable assignment, update the binding
AstVarRef* const lhsp = VN_CAST(nodep->lhsp(), VarRef);
AstConst* const valp = VN_CAST(V3Const::constifyEdit(nodep->rhsp()), Const);
if (lhsp && valp && !lhsp->varp()->isForced() && !lhsp->varp()->isSigUserRWPublic()) {
lhsp->varScopep()->user1p(valp);
return;
}
// Otherwise just like a generic statement
process(nodep->lhsp());
}
void visit(AstIf* nodep) override {
if (!m_ok) return;
if (!process(nodep->condp())) return;
// If condition is constant, replce with the relevant branch
if (AstConst* const condp = VN_CAST(V3Const::constifyEdit(nodep->condp()), Const)) {
if (AstNode* const bodyp = condp->isNeqZero() ? nodep->thensp() : nodep->elsesp()) {
nodep->addNextHere(bodyp->unlinkFrBackWithNext()); // This will be iterated next
}
VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
return;
}
// Otherwise just like a generic statement
process(nodep);
}
void visit(AstJumpGo* nodep) override {
// Remove trailing dead code
if (nodep->nextp()) pushDeletep(nodep->nextp()->unlinkFrBackWithNext());
}
public:
// CONSTRUCTORS
UnrollVisitor() { init(false, ""); }
~UnrollVisitor() override {
V3Stats::addStatSum("Optimizations, Unrolled Loops", m_statLoops);
V3Stats::addStatSum("Optimizations, Unrolled Iterations", m_statIters);
}
// METHODS
void init(bool generate, const string& beginName) {
m_forVarp = nullptr;
m_forVscp = nullptr;
m_ignoreIncp = nullptr;
m_varModeCheck = false;
m_varAssignHit = false;
m_forkHit = false;
m_generate = generate;
m_beginName = beginName;
}
void process(AstNode* nodep, bool generate, const string& beginName) {
init(generate, beginName);
iterate(nodep);
// Unroll the given loop. Returns the resulting statements and the number of
// iterations unrolled (0 if unrolling failed);
static std::pair<AstNode*, bool> apply(UnrollStats& stats, AstLoop* loopp) {
UnrollOneVisitor visitor{stats, loopp};
// If successfully unrolled, return the resulting list of statements - might be empty
if (visitor.m_ok) return {visitor.m_stmtsp, true};
// Otherwise delete intermediate results
if (visitor.m_stmtsp) VL_DO_DANGLING(visitor.m_stmtsp->deleteTree(), visitor.m_stmtsp);
return {nullptr, false};
}
};
//######################################################################
// Unroll class functions
// Unroll all AstLoop statements
UnrollStateful::UnrollStateful()
: m_unrollerp{new UnrollVisitor} {}
UnrollStateful::~UnrollStateful() { delete m_unrollerp; }
class UnrollAllVisitor final : VNVisitor {
// STATE - Statistic tracking
UnrollStats m_stats;
void UnrollStateful::unrollGen(AstGenFor* nodep, const string& beginName) {
UINFO(5, __FUNCTION__ << ": ");
m_unrollerp->process(nodep, true, beginName);
}
// VISIT
void visit(AstLoop* nodep) override {
// Attempt to unroll this loop
const std::pair<AstNode*, bool> pair = UnrollOneVisitor::apply(m_stats, nodep);
// If failed, carry on with nested loop
if (!pair.second) {
iterateChildren(nodep);
return;
}
// Otherwise replace the loop with the unrolled code - might be empty
if (pair.first) {
nodep->replaceWith(pair.first);
VL_DO_DANGLING(pushDeletep(nodep), nodep);
} else {
VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
}
// Iteration continues with the unrolled body
}
void visit(AstNode* nodep) override { iterateChildren(nodep); }
// CONSTRUCTOR
UnrollAllVisitor(AstNetlist* netlistp) { iterate(netlistp); }
public:
static void apply(AstNetlist* netlistp) { UnrollAllVisitor{netlistp}; }
};
//######################################################################
// V3Unroll class functions
void V3Unroll::unrollAll(AstNetlist* nodep) {
UINFO(2, __FUNCTION__ << ":");
{
UnrollVisitor visitor;
visitor.process(nodep, false, "");
} // Destruct before checking
UnrollAllVisitor::apply(nodep);
V3Global::dumpCheckGlobalTree("unroll", 0, dumpTreeEitherLevel() >= 3);
}