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
+323
View File
@@ -0,0 +1,323 @@
// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Generate loop unrolling
//
// 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
//
//*************************************************************************
//
// Unroll AstLoopStmts
//
//*************************************************************************
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
#include "V3Const.h"
#include "V3Simulate.h"
#include "V3Unroll.h"
VL_DEFINE_DEBUG_FUNCTIONS;
//######################################################################
// Unroll AstGenFor
class UnrollGenVisitor final : public VNVisitor {
// STATE - across all visitors
AstVar* m_forVarp = nullptr; // Iterator variable
const AstNode* m_ignoreIncp = nullptr; // Increment node to ignore
bool m_varModeCheck = false; // Just checking RHS assignments
bool m_varAssignHit = false; // Assign var hit
bool m_forkHit = false; // Fork hit
std::string m_beginName; // What name to give begin iterations
// 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->access().isReadOnly()) {
AstNode* const newconstp = varValuep->cloneTree(false);
refp->replaceWith(newconstp);
VL_DO_DANGLING(pushDeletep(refp), refp);
}
});
}
bool cantUnroll(AstNode* nodep, const char* reason) const {
UINFO(4, " Can't Unroll: " << reason << " :" << nodep);
nodep->v3warn(E_UNSUPPORTED, "Unsupported: Can't unroll generate for; " << reason);
return false;
}
bool forUnrollCheck(
AstGenFor* const nodep,
AstNode* const initp, // Maybe under nodep (no nextp), or standalone (ignore nextp)
AstNodeExpr* 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);
// 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();
if (!m_forVarp->isGenVar()) {
nodep->v3error("Non-genvar used in generate for: " << m_forVarp->prettyNameQ());
}
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");
//
UINFO(8, " Loop Variable: " << m_forVarp);
UINFOTREE(9, nodep, "", "for");
// Finally, we can do it
if (!forUnroller(nodep, initAssp, condp, incp, bodysp)) {
return cantUnroll(nodep, "Unable to unroll loop");
}
VL_DANGLING(nodep);
// Cleanup
return true;
}
bool simulateTree(AstNodeExpr* 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 forUnroller(AstGenFor* nodep, AstAssign* initp, AstNodeExpr* 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
}
// Mark variable to disable some later warnings
m_forVarp->usedLoopIdx(true);
AstNode* newbodysp = nullptr;
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.");
return false;
}
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);
}
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;
}
const int limit = v3Global.opt.unrollCountAdjusted(VOptionBool{}, true, 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;
}
// loopValue += valInc
AstAssign* const incpass = VN_AS(incp, Assign);
V3Number newLoopValue{nodep};
if (!simulateTree(incpass->rhsp(), &loopValue, incpass, newLoopValue)) {
nodep->v3error("Loop unrolling failed");
return false;
}
loopValue.opAssign(newLoopValue);
}
}
}
if (!newbodysp) { // initp might have effects after the loop
if (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;
}
}
// Replace the FOR()
if (newbodysp) {
nodep->replaceWith(newbodysp);
} else {
nodep->unlinkFrBack();
}
if (newbodysp) UINFOTREE(9, newbodysp, "", "_new");
return true;
}
// VISITORS
void visit(AstVarRef* nodep) override {
if (m_varModeCheck && nodep->varp() == m_forVarp && nodep->access().isWriteOrRW()) {
UINFO(8, " Itervar assigned to: " << nodep);
m_varAssignHit = true;
}
}
void visit(AstNode* nodep) override {
if (m_varModeCheck && nodep == m_ignoreIncp) {
// Ignore subtree that is the increment
} else {
iterateChildren(nodep);
}
}
public:
// CONSTRUCTORS
UnrollGenVisitor() = default;
void unroll(AstGenFor* nodep, const std::string& beginName) {
m_forVarp = nullptr; // Iterator variable
m_ignoreIncp = nullptr; // Increment node to ignore
m_varModeCheck = false; // Just checking RHS assignments
m_varAssignHit = false; // Assign var hit
m_forkHit = false; // Fork hit
m_beginName = beginName;
// 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, 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);
}
}
};
//######################################################################
// GenForUnroller members
GenForUnroller::GenForUnroller()
: m_unrollerp{new UnrollGenVisitor{}} {}
GenForUnroller::~GenForUnroller() { VL_DO_DANGLING(delete m_unrollerp, m_unrollerp); }
void GenForUnroller::unroll(AstGenFor* nodep, const std::string& beginName) {
m_unrollerp->unroll(nodep, beginName);
}