verilator/src/V3Life.cpp

518 lines
20 KiB
C++
Raw Normal View History

// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Lifelicate variable assignment elimination
//
2019-11-08 04:33:59 +01:00
// Code available from: https://verilator.org
//
//*************************************************************************
//
2025-01-01 14:30:25 +01:00
// 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
//
//*************************************************************************
// LIFE TRANSFORMATIONS:
// Build control-flow graph with assignments and var usages
// All modules:
// ASSIGN(x,...), ASSIGN(x,...) => delete first one
// We also track across if statements:
// ASSIGN(X,...) IF( ..., ASSIGN(X,...), ASSIGN(X,...)) => deletes first
// We don't do the opposite yet though (remove assigns in if followed by outside if)
//
//*************************************************************************
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
#include "V3Life.h"
#include "V3Const.h"
#include "V3Stats.h"
#include <vector>
VL_DEFINE_DEBUG_FUNCTIONS;
//######################################################################
// Structure for global state
class LifeState final {
// NODE STATE
// See below
const VNUser1InUse m_inuser1;
// STATE
public:
VDouble0 m_statAssnDel; // Statistic tracking
VDouble0 m_statAssnCon; // Statistic tracking
VDouble0 m_statCResetDel; // Statistic tracking
// CONSTRUCTORS
LifeState() = default;
~LifeState() {
V3Stats::addStatSum("Optimizations, Lifetime assign deletions", m_statAssnDel);
V3Stats::addStatSum("Optimizations, Lifetime creset deletions", m_statCResetDel);
V3Stats::addStatSum("Optimizations, Lifetime constant prop", m_statAssnCon);
}
};
//######################################################################
// Structure for each variable encountered
class LifeVarEntry final {
// Last assignment to this varscope, nullptr if no longer relevant
AstNodeStmt* m_assignp = nullptr;
AstConst* m_constp = nullptr; // Known constant value
bool m_isNew = true; // Is just created
// First access was a set (and thus block above may have a set that can be deleted
bool m_setBeforeUse = false;
// Was ever assigned (and thus above block may not preserve constant propagation)
bool m_everSet = false;
public:
LifeVarEntry() = default;
~LifeVarEntry() = default;
void init(bool setBeforeUse) {
UASSERT(m_isNew, "Not a new entry");
m_isNew = false;
m_setBeforeUse = setBeforeUse;
}
void simpleAssign(AstNodeAssign* nodep) { // New simple A=.... assignment
UASSERT_OBJ(!m_isNew, nodep, "Uninitialzized new entry");
m_assignp = nodep;
m_constp = nullptr;
m_everSet = true;
if (VN_IS(nodep->rhsp(), Const)) m_constp = VN_AS(nodep->rhsp(), Const);
}
void resetStatement(AstCReset* nodep) { // New CReset(A) assignment
UASSERT_OBJ(!m_isNew, nodep, "Uninitialzized new entry");
m_assignp = nodep;
m_constp = nullptr;
m_everSet = true;
}
void complexAssign() { // A[x]=... or some complicated assignment
UASSERT(!m_isNew, "Uninitialzized new entry");
m_assignp = nullptr;
m_constp = nullptr;
m_everSet = true;
}
void consumed() { // Rvalue read of A
UASSERT(!m_isNew, "Uninitialzized new entry");
m_assignp = nullptr;
}
AstNodeStmt* assignp() const { return m_assignp; }
AstConst* constNodep() const { return m_constp; }
bool isNew() const { return m_isNew; }
bool setBeforeUse() const { return m_setBeforeUse; }
bool everSet() const { return m_everSet; }
};
//######################################################################
// Structure for all variables under a given meta-basic block
class LifeBlock final {
// NODE STATE
// Cleared each AstIf:
// AstVarScope::user1() -> int. Used in combining to detect duplicates
// LIFE MAP
// For each basic block, we'll make a new map of what variables that if/else is changing
// Current active lifetime map for current scope
std::unordered_map<AstVarScope*, LifeVarEntry> m_map;
LifeBlock* const m_aboveLifep; // Upper life, or nullptr
LifeState* const m_statep; // Current global state
bool m_replacedVref = false; // Replaced a variable reference since last clearing
VNDeleter m_deleter; // Used to delay deletion of nodes
public:
LifeBlock(LifeBlock* aboveLifep, LifeState* statep)
: m_aboveLifep{aboveLifep} // Null if top
, m_statep{statep} {}
~LifeBlock() = default;
// METHODS
void checkRemoveAssign(AstVarScope* vscp, LifeVarEntry& entr) {
const AstVar* const varp = vscp->varp();
if (!varp->isSigPublic() && !varp->sensIfacep()) {
// Rather than track what sigs AstUCFunc/AstUCStmt may change,
// we just don't optimize any public sigs
// Check the var entry, and remove if appropriate
if (AstNodeStmt* const oldassp = entr.assignp()) {
UINFO(7, " PREV: " << oldassp);
// Redundant assignment, in same level block
// Don't delete it now as it will confuse iteration since it maybe WAY
// above our current iteration point.
UINFOTREE(7, oldassp, "", "REMOVE/SAMEBLK");
entr.complexAssign();
oldassp->unlinkFrBack();
if (VN_IS(oldassp, CReset)) {
++m_statep->m_statCResetDel;
} else {
++m_statep->m_statAssnDel;
}
VL_DO_DANGLING(m_deleter.pushDeletep(oldassp), oldassp);
}
}
}
void resetStatement(AstVarScope* nodep, AstCReset* rstp) {
// Do we have a old assignment we can nuke?
UINFO(4, " CRESETof: " << nodep);
UINFO(7, " new: " << rstp);
LifeVarEntry& entr = m_map[nodep];
if (entr.isNew()) {
entr.init(true);
} else {
checkRemoveAssign(nodep, entr);
}
entr.resetStatement(rstp);
// lifeDump();
}
void simpleAssign(AstVarScope* nodep, AstNodeAssign* assp) {
// Do we have a old assignment we can nuke?
UINFO(4, " ASSIGNof: " << nodep);
UINFO(7, " new: " << assp);
LifeVarEntry& entr = m_map[nodep];
if (entr.isNew()) {
entr.init(true);
} else {
checkRemoveAssign(nodep, entr);
}
entr.simpleAssign(assp);
// lifeDump();
}
void complexAssign(AstVarScope* nodep) {
UINFO(4, " clearof: " << nodep);
LifeVarEntry& entr = m_map[nodep];
if (entr.isNew()) entr.init(false);
entr.complexAssign();
}
void clearReplaced() { m_replacedVref = false; }
bool replaced() const { return m_replacedVref; }
void varUsageReplace(AstVarScope* nodep, AstVarRef* varrefp) {
2022-03-31 02:17:59 +02:00
// Variable rvalue. If it references a constant, we can replace it
LifeVarEntry& entr = m_map[nodep];
if (entr.isNew()) {
entr.init(false);
} else {
if (AstConst* const constp = entr.constNodep()) {
if (!varrefp->varp()->isSigPublic() && !varrefp->varp()->sensIfacep()) {
// Aha, variable is constant; substitute in.
// We'll later constant propagate
UINFO(4, " replaceconst: " << varrefp);
varrefp->replaceWith(constp->cloneTree(false));
m_replacedVref = true;
VL_DO_DANGLING(varrefp->deleteTree(), varrefp);
++m_statep->m_statAssnCon;
return; // **DONE, no longer a var reference**
}
}
UINFO(4, " usage: " << nodep);
}
entr.consumed();
}
void complexAssignFind(AstVarScope* nodep) {
UINFO(4, " casfind: " << nodep);
LifeVarEntry& entr = m_map[nodep];
if (entr.isNew()) entr.init(false);
entr.complexAssign();
}
void consumedFind(AstVarScope* nodep) {
LifeVarEntry& entr = m_map[nodep];
if (entr.isNew()) entr.init(false);
entr.consumed();
}
void lifeToAbove() {
// Any varrefs under a if/else branch affect statements outside and after the if/else
UASSERT(m_aboveLifep, "Pushing life when already at the top level");
for (auto& itr : m_map) {
AstVarScope* const nodep = itr.first;
m_aboveLifep->complexAssignFind(nodep);
if (itr.second.everSet()) {
// Record there may be an assignment, so we don't constant propagate across the if.
complexAssignFind(nodep);
} else {
// Record consumption, so we don't eliminate earlier assignments
consumedFind(nodep);
}
}
}
void dualBranch(LifeBlock* life1p, LifeBlock* life2p) {
// Find any common sets on both branches of IF and propagate upwards
// life1p->lifeDump();
// life2p->lifeDump();
AstNode::user1ClearTree(); // user1p() used on entire tree
for (auto& itr : life1p->m_map) {
// When the if branch sets a var before it's used, mark that variable
if (itr.second.setBeforeUse()) itr.first->user1(1);
}
for (auto& itr : life2p->m_map) {
// When the else branch sets a var before it's used
AstVarScope* const nodep = itr.first;
if (itr.second.setBeforeUse() && nodep->user1()) {
// Both branches set the var, we can remove the assignment before the IF.
UINFO(4, "DUALBRANCH " << nodep);
const auto itab = m_map.find(nodep);
if (itab != m_map.end()) checkRemoveAssign(nodep, itab->second);
}
}
// this->lifeDump();
}
void clear() { m_map.clear(); }
// DEBUG
void lifeDump() {
UINFO(5, " LifeMap:");
for (const auto& itr : m_map) {
UINFO(5,
" Ent: " << (itr.second.setBeforeUse() ? "[F] " : " ") << itr.first);
if (itr.second.assignp()) { //
UINFO(5, " Ass: " << itr.second.assignp());
}
}
}
};
//######################################################################
// Life state, as a visitor of each AstNode
class LifeVisitor final : public VNVisitor {
// STATE
LifeState* const m_statep; // Current state
bool m_sideEffect = false; // Side effects discovered in assign RHS
bool m_noopt = false; // Disable optimization of variables in this block
bool m_tracingCall = false; // Iterating into a CCall to a CFunc
LifeBlock* m_lifep = nullptr; // Current active lifetime map for current scope
// METHODS
void setNoopt() {
m_noopt = true;
m_lifep->clear();
}
2009-01-21 22:56:50 +01:00
// VISITORS
void visit(AstVarRef* nodep) override {
// Consumption/generation of a variable,
// it's used so can't elim assignment before this use.
UASSERT_OBJ(nodep->varScopep(), nodep, "nullptr");
//
AstVarScope* const vscp = nodep->varScopep();
UASSERT_OBJ(vscp, nodep, "Scope not assigned");
if (nodep->access().isWriteOrRW()) {
2019-07-06 02:09:56 +02:00
m_sideEffect = true; // $sscanf etc may have RHS vars that are lvalues
m_lifep->complexAssign(vscp);
} else {
VL_DO_DANGLING(m_lifep->varUsageReplace(vscp, nodep), nodep);
}
}
void visit(AstNodeAssign* nodep) override {
if (nodep->isTimingControl() || VN_IS(nodep, AssignForce)) {
// V3Life doesn't understand time sense nor force assigns - don't optimize
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 14:26:32 +02:00
setNoopt();
iterateChildren(nodep);
return;
}
// Collect any used variables first, as lhs may also be on rhs
// Similar code in V3Dead
VL_RESTORER(m_sideEffect);
m_sideEffect = false;
m_lifep->clearReplaced();
iterateAndNextNull(nodep->rhsp());
if (m_lifep->replaced()) {
// We changed something, try to constant propagate, but don't delete the
// assignment as we still need nodep to remain.
V3Const::constifyEdit(nodep->rhsp()); // rhsp may change
}
// Has to be direct assignment without any EXTRACTing.
if (VN_IS(nodep->lhsp(), VarRef) && !m_sideEffect && !m_noopt) {
AstVarScope* const vscp = VN_AS(nodep->lhsp(), VarRef)->varScopep();
UASSERT_OBJ(vscp, nodep, "Scope lost on variable");
m_lifep->simpleAssign(vscp, nodep);
} else {
iterateAndNextNull(nodep->lhsp());
}
}
void visit(AstCReset* nodep) override {
if (!m_noopt) {
AstVarScope* const vscp = nodep->varrefp()->varScopep();
UASSERT_OBJ(vscp, nodep, "Scope lost on variable");
m_lifep->resetStatement(vscp, nodep);
} else {
iterateAndNextNull(nodep->varrefp());
}
}
void visit(AstAssignDly* nodep) override {
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 14:26:32 +02:00
// V3Life doesn't understand time sense
if (nodep->isTimingControl()) {
// Don't optimize
setNoopt();
}
// Don't treat as normal assign
iterateChildren(nodep);
}
//---- Track control flow changes
void visit(AstNodeIf* nodep) override {
UINFO(4, " IF " << nodep);
// Condition is part of PREVIOUS block
iterateAndNextNull(nodep->condp());
LifeBlock* const prevLifep = m_lifep;
2022-11-20 19:11:01 +01:00
LifeBlock* const ifLifep = new LifeBlock{prevLifep, m_statep};
LifeBlock* const elseLifep = new LifeBlock{prevLifep, m_statep};
{
m_lifep = ifLifep;
iterateAndNextNull(nodep->thensp());
}
{
m_lifep = elseLifep;
iterateAndNextNull(nodep->elsesp());
}
2019-10-25 03:48:45 +02:00
m_lifep = prevLifep;
UINFO(4, " join ");
// Find sets on both flows
m_lifep->dualBranch(ifLifep, elseLifep);
// For the next assignments, clear any variables that were read or written in the block
ifLifep->lifeToAbove();
elseLifep->lifeToAbove();
VL_DO_DANGLING(delete ifLifep, ifLifep);
VL_DO_DANGLING(delete elseLifep, elseLifep);
}
void visit(AstLoop* nodep) override {
// Similar problem to AstJumpBlock, don't optimize loop bodies - most are unrolled
UASSERT_OBJ(!nodep->contsp(), nodep, "'contsp' only used before LinkJump");
LifeBlock* const prevLifep = m_lifep;
2022-11-20 19:11:01 +01:00
LifeBlock* const bodyLifep = new LifeBlock{prevLifep, m_statep};
{
VL_RESTORER(m_noopt);
m_lifep = bodyLifep;
setNoopt();
iterateAndNextNull(nodep->stmtsp());
m_lifep = prevLifep;
}
UINFO(4, " joinloop");
// For the next assignments, clear any variables that were read or written in the block
bodyLifep->lifeToAbove();
VL_DO_DANGLING(delete bodyLifep, bodyLifep);
}
void visit(AstJumpBlock* nodep) override {
// As with Loop's we can't predict if a JumpGo will kill us or not
// It's worse though as an IF(..., JUMPGO) may change the control flow.
// Just don't optimize blocks with labels; they're rare - so far.
LifeBlock* const prevLifep = m_lifep;
2022-11-20 19:11:01 +01:00
LifeBlock* const bodyLifep = new LifeBlock{prevLifep, m_statep};
{
VL_RESTORER(m_noopt);
m_lifep = bodyLifep;
setNoopt();
iterateAndNextNull(nodep->stmtsp());
m_lifep = prevLifep;
}
UINFO(4, " joinjump");
// For the next assignments, clear any variables that were read or written in the block
bodyLifep->lifeToAbove();
VL_DO_DANGLING(delete bodyLifep, bodyLifep);
2010-02-14 16:01:21 +01:00
}
void visit(AstNodeCCall* nodep) override {
// UINFO(4, " CCALL " << nodep);
iterateChildren(nodep);
// Enter the function and trace it
// else is non-inline or public function we optimize separately
if (nodep->funcp()->entryPoint()) {
setNoopt();
} else {
m_tracingCall = true;
iterate(nodep->funcp());
}
}
void visit(AstCFunc* nodep) override {
// UINFO(4, " CFUNC " << nodep);
if (!m_tracingCall && !nodep->entryPoint()) return;
m_tracingCall = false;
if (nodep->recursive()) setNoopt();
if (nodep->dpiImportPrototype() && !nodep->dpiPure()) {
m_sideEffect = true; // If appears on assign RHS, don't ever delete the assignment
}
iterateChildren(nodep);
}
void visit(AstUCFunc* nodep) override {
m_sideEffect = true; // If appears on assign RHS, don't ever delete the assignment
iterateChildren(nodep);
}
void visit(AstCExpr* nodep) override {
m_sideEffect = true; // If appears on assign RHS, don't ever delete the assignment
iterateChildren(nodep);
}
void visit(AstVar*) override {} // Don't want varrefs under it
2022-09-16 17:15:10 +02:00
void visit(AstNode* nodep) override {
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 14:26:32 +02:00
if (nodep->isTimingControl()) {
// V3Life doesn't understand time sense - don't optimize
setNoopt();
}
iterateChildren(nodep);
}
public:
// CONSTRUCTORS
LifeVisitor(AstNode* nodep, LifeState* statep)
: m_statep{statep} {
UINFO(4, " LifeVisitor on " << nodep);
{
2022-11-20 19:11:01 +01:00
m_lifep = new LifeBlock{nullptr, m_statep};
iterate(nodep);
if (m_lifep) VL_DO_CLEAR(delete m_lifep, m_lifep = nullptr);
}
}
~LifeVisitor() override {
if (m_lifep) VL_DO_CLEAR(delete m_lifep, m_lifep = nullptr);
2015-10-04 04:33:06 +02:00
}
2020-01-09 01:33:47 +01:00
VL_UNCOPYABLE(LifeVisitor);
};
//######################################################################
class LifeTopVisitor final : public VNVisitor {
// Visit all top nodes searching for functions that are entry points we want to start
// finding code within.
private:
// STATE
LifeState* const m_statep; // Current state
// VISITORS
void visit(AstCFunc* nodep) override {
if (nodep->entryPoint()) {
// Usage model 1: Simulate all C code, doing lifetime analysis
LifeVisitor{nodep, m_statep};
}
}
void visit(AstNodeProcedure* nodep) override {
// Usage model 2: Cleanup basic blocks
LifeVisitor{nodep, m_statep};
}
void visit(AstVar*) override {} // Accelerate
void visit(AstNodeStmt*) override {} // Accelerate
void visit(AstNodeExpr*) override {} // Accelerate
void visit(AstNode* nodep) override { iterateChildren(nodep); }
public:
// CONSTRUCTORS
LifeTopVisitor(AstNetlist* nodep, LifeState* statep)
: m_statep{statep} {
iterate(nodep);
}
~LifeTopVisitor() override = default;
};
//######################################################################
// Life class functions
void V3Life::lifeAll(AstNetlist* nodep) {
UINFO(2, __FUNCTION__ << ":");
{
LifeState state;
LifeTopVisitor{nodep, &state};
} // Destruct before checking
VIsCached::clearCacheTree(); // Removing assignments may affect isPure
V3Global::dumpCheckGlobalTree("life", 0, dumpTreeEitherLevel() >= 3);
}