verilator/src/V3Localize.cpp
Geza Lore 80b08b71aa
Support NBAs to arrays inside loops (#5092)
For NBAs that might execute a dynamic number of times in a single
evaluation (specifically: those that assign to array elements inside
loops), we introduce a new run-time VlNBACommitQueue data-structure
(currently a vector), which stores all pending updates and the necessary
info to reconstruct the LHS reference of the AstAssignDly at run-time.

All variables needing a commit queue has their corresponding unique
commit queue.

All NBAs to a variable that requires a commit queue go through the
commit queue. This is necessary to preserve update order in sequential
code, e.g.:
 a[7] <= 10
 for (int i = 1 ; i < 10; ++i) a[i] <= i;
 a[2] <= 10
needs to end with array elements 1..9 being 1, 10, 3, 4, 5, 6, 7, 8, 9.

This enables supporting common forms of NBAs to arrays on the left hand
side of <= in non-suspendable/non-fork code. (Suspendable/fork
implementation is unclear to me so I left it unchanged, see #5084).

Any NBA that does not need a commit queue (i.e.: those that were
supported before), use the same scheme as before, and this patch should
have no effect on the generated code for those NBAs.
2024-05-03 07:45:49 -04:00

229 lines
9.6 KiB
C++

// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Convert BLOCKTEMPs to local variables
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// Copyright 2003-2024 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
//
//*************************************************************************
// LOCALIZE TRANSFORMATIONS:
// All modules:
// VARSCOPE(BLOCKTEMP...
// if only referenced in one CFUNC, make it local
// VARSCOPE
// if non-public, always written before used, make it local
//
//*************************************************************************
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
#include "V3Localize.h"
#include "V3AstUserAllocator.h"
#include "V3Stats.h"
#include <vector>
VL_DEFINE_DEBUG_FUNCTIONS;
//######################################################################
// LocalizeVisitor
class LocalizeVisitor final : public VNVisitor {
// NODE STATE
// AstVarScope::user1() -> Bool indicating VarScope is not optimizable.
// AstCFunc::user1() -> Bool indicating CFunc is not a leaf function.
// AstVarScope::user2() -> Bool indicating VarScope was fully assigned in the current
// function.
// AstVarScope::user3p() -> Set of CFuncs referencing this VarScope. (via m_accessors)
// AstCFunc::user4p() -> Multimap of 'VarScope -> VarRefs that reference that VarScope'
// in this function. (via m_references)
const VNUser1InUse m_user1InUse;
const VNUser3InUse m_user3InUse;
const VNUser4InUse m_user4InUse;
AstUser3Allocator<AstVarScope, std::unordered_set<AstCFunc*>> m_accessors;
AstUser4Allocator<AstCFunc, std::unordered_multimap<const AstVarScope*, AstVarRef*>>
m_references;
// STATE - across all visitors
std::vector<AstVarScope*> m_varScopeps; // List of variables to consider for localization
VDouble0 m_statLocVars; // Statistic tracking
// STATE - for current visit position (use VL_RESTORER)
AstCFunc* m_cfuncp = nullptr; // Current active function
uint32_t m_nodeDepth = 0; // Node depth under m_cfuncp
// METHODS
bool isOptimizable(AstVarScope* nodep) {
// Don't want to malloc/free the backing store all the time
if (VN_IS(nodep->dtypep(), NBACommitQueueDType)) return false;
return ((!nodep->user1() // Not marked as not optimizable, or ...
// .. a block temp used in a single CFunc
|| (nodep->varp()->varType() == VVarType::BLOCKTEMP
&& m_accessors(nodep).size() == 1))
// and under size limit
&& nodep->varp()->dtypep()->widthTotalBytes() <= v3Global.opt.localizeMaxSize());
}
static bool existsNonLeaf(const std::unordered_set<AstCFunc*>& funcps) {
for (const AstCFunc* const funcp : funcps) {
if (funcp->user1()) return true;
}
return false;
}
void moveVarScopes() {
for (AstVarScope* const nodep : m_varScopeps) {
if (!isOptimizable(nodep)) continue; // Not optimizable
const std::unordered_set<AstCFunc*>& funcps = m_accessors(nodep);
if (funcps.empty()) continue; // No referencing functions at all
// If more than one referencing function, but not all are leaf
// functions, then don't localize, as one of the referencing
// functions might be calling another, which the current analysis
// cannot cope with. This should be rare (introduced by V3Depth).
if (funcps.size() > 1 && existsNonLeaf(funcps)) continue;
UINFO(4, "Localizing " << nodep << endl);
++m_statLocVars;
// Yank the VarScope from it's parent and schedule them for deletion. Leave the Var
// for now, as not all VarScopes referencing this Var might be localized.
pushDeletep(nodep->unlinkFrBack());
// In each referencing function, create a replacement local variable
AstVar* const oldVarp = nodep->varp();
for (AstCFunc* const funcp : funcps) {
// Create the new local variable.
const string newName
= nodep->scopep() == funcp->scopep()
? oldVarp->name()
: nodep->scopep()->nameDotless() + "__DOT__" + oldVarp->name();
AstVar* const newVarp
= new AstVar{oldVarp->fileline(), oldVarp->varType(), newName, oldVarp};
newVarp->funcLocal(true);
newVarp->noReset(oldVarp->noReset());
funcp->addInitsp(newVarp);
// Fix up all the references within this function
const auto er = m_references(funcp).equal_range(nodep);
for (auto it = er.first; it != er.second; ++it) {
AstVarRef* const refp = it->second;
refp->varScopep(nullptr);
refp->varp(newVarp);
}
}
}
m_varScopeps.clear();
}
// VISITORS
void visit(AstNetlist* nodep) override {
iterateChildrenConst(nodep);
moveVarScopes();
}
void visit(AstCFunc* nodep) override {
UINFO(4, " CFUNC " << nodep << endl);
VL_RESTORER(m_cfuncp);
VL_RESTORER(m_nodeDepth);
m_cfuncp = nodep;
m_nodeDepth = 0;
const VNUser2InUse user2InUse;
iterateChildrenConst(nodep);
}
void visit(AstCCall* nodep) override {
m_cfuncp->user1(true); // Mark caller as not a leaf function
iterateChildrenConst(nodep);
}
void visit(AstNodeAssign* nodep) override {
// Analyze RHS first so "a = a + 1" is detected as a read before write
iterate(nodep->rhsp());
// For now we only consider an assignment that is directly under the function, (in
// particular: not under an AstIf, or other kind of branch). This could be improved with
// proper data flow analysis.
if (m_nodeDepth == 0) {
// Check if simple "VARREF = ..." assignment, i.e.: this assignment sets the whole
// variable (and in particular, it is not assigned only in part).
if (const AstVarRef* const refp = VN_CAST(nodep->lhsp(), VarRef)) {
// Mark this VarScope as assigned in this function
refp->varScopep()->user2(1);
}
}
// Analyze LHS (in case it's not the above simple case
iterate(nodep->lhsp());
}
void visit(AstVarScope* nodep) override {
if (!nodep->varp()->isPrimaryIO() // Not an IO the user wants to interact with
&& !nodep->varp()->isSigPublic() // Not something the user wants to interact with
&& !nodep->varp()->isFuncLocal() // Not already a function local (e.g.: argument)
&& !nodep->varp()->isStatic() // Not a static variable
&& !nodep->varp()->isClassMember() // Statically exists in design hierarchy
&& !nodep->varp()->sensIfacep() // Not sensitive to an interface
&& !nodep->varp()->valuep() // Does not have an initializer
) {
UINFO(4, "Consider for localization: " << nodep << endl);
m_varScopeps.push_back(nodep);
}
// No iterate; Don't want varrefs under it (e.g.: in child dtype?)
}
void visit(AstVarRef* nodep) override {
UASSERT_OBJ(m_cfuncp, nodep, "AstVarRef not under function");
AstVarScope* const varScopep = nodep->varScopep();
// Remember this function accesses this VarScope (we always need this as we might optimize
// this VarScope into a local, even if it's not assigned. See 'isOptimizable')
m_accessors(varScopep).emplace(m_cfuncp);
// Remember the reference so we can fix it up later (we always need this as well)
m_references(m_cfuncp).emplace(varScopep, nodep);
// Check if already marked as not optimizable
if (!varScopep->user1()) {
// Note: we only check read variables, as it's ok to localize (and in fact discard)
// any variables that are only written but never read.
if (nodep->access().isReadOrRW() && !varScopep->user2()) {
// Variable is read, but is not known to have been assigned in this function. Mark
// as not optimizable.
UINFO(4, "Not optimizable (not written): " << nodep << endl);
varScopep->user1(1);
}
}
// No iterate; Don't want varrefs under it (e.g.: in child dtype?)
}
void visit(AstNode* nodep) override {
VL_RESTORER(m_nodeDepth);
++m_nodeDepth;
iterateChildrenConst(nodep);
}
public:
// CONSTRUCTORS
explicit LocalizeVisitor(AstNetlist* nodep) { iterate(nodep); }
~LocalizeVisitor() override {
V3Stats::addStat("Optimizations, Vars localized", m_statLocVars);
}
};
//######################################################################
// Localize class functions
void V3Localize::localizeAll(AstNetlist* nodep) {
UINFO(2, __FUNCTION__ << ": " << endl);
{ LocalizeVisitor{nodep}; } // Destruct before checking
V3Global::dumpCheckGlobalTree("localize", 0, dumpTreeEitherLevel() >= 6);
}