verilator/src/V3SchedTiming.cpp

362 lines
17 KiB
C++
Raw Normal View History

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
// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Code scheduling
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// Copyright 2003-2022 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
//
//*************************************************************************
//
// Functions defined in this file are used by V3Sched.cpp to properly integrate
// static scheduling with timing features. They create external domains for
// variables, remap them to trigger vectors, and create timing resume/commit
// calls for the global eval loop. There is also a function that transforms
// forks into emittable constructs.
//
// See the internals documentation docs/internals.rst for more details.
//
//*************************************************************************
#include "config_build.h"
#include "verilatedos.h"
#include "V3EmitCBase.h"
#include "V3Sched.h"
#include <unordered_map>
namespace V3Sched {
//============================================================================
// Remaps external domains using the specified trigger map
std::map<const AstVarScope*, std::vector<AstSenTree*>>
TimingKit::remapDomains(const std::unordered_map<const AstSenTree*, AstSenTree*>& trigMap) const {
std::map<const AstVarScope*, std::vector<AstSenTree*>> remappedDomainMap;
for (const auto& vscpDomains : m_externalDomains) {
const AstVarScope* const vscp = vscpDomains.first;
const auto& domains = vscpDomains.second;
auto& remappedDomains = remappedDomainMap[vscp];
remappedDomains.reserve(domains.size());
for (AstSenTree* const domainp : domains) {
remappedDomains.push_back(trigMap.at(domainp));
}
}
return remappedDomainMap;
}
//============================================================================
// Creates a timing resume call (if needed, else returns null)
AstCCall* TimingKit::createResume(AstNetlist* const netlistp) {
if (!m_resumeFuncp) {
if (m_lbs.empty()) return nullptr;
// Create global resume function
AstScope* const scopeTopp = netlistp->topScopep()->scopep();
m_resumeFuncp = new AstCFunc{netlistp->fileline(), "_timing_resume", scopeTopp, ""};
m_resumeFuncp->dontCombine(true);
m_resumeFuncp->isLoose(true);
m_resumeFuncp->isConst(false);
m_resumeFuncp->declPrivate(true);
scopeTopp->addActivep(m_resumeFuncp);
for (auto& p : m_lbs) {
// Put all the timing actives in the resume function
AstActive* const activep = p.second;
m_resumeFuncp->addStmtsp(activep);
}
}
return new AstCCall{m_resumeFuncp->fileline(), m_resumeFuncp};
}
//============================================================================
// Creates a timing commit call (if needed, else returns null)
AstCCall* TimingKit::createCommit(AstNetlist* const netlistp) {
if (!m_commitFuncp) {
for (auto& p : m_lbs) {
AstActive* const activep = p.second;
auto* const resumep = VN_AS(activep->stmtsp(), CMethodHard);
UASSERT_OBJ(!resumep->nextp(), resumep, "Should be the only statement here");
AstVarScope* const schedulerp = VN_AS(resumep->fromp(), VarRef)->varScopep();
UASSERT_OBJ(schedulerp->dtypep()->basicp()->isDelayScheduler()
|| schedulerp->dtypep()->basicp()->isTriggerScheduler(),
schedulerp, "Unexpected type");
if (!schedulerp->dtypep()->basicp()->isTriggerScheduler()) continue;
// Create the global commit function only if we have trigger schedulers
if (!m_commitFuncp) {
AstScope* const scopeTopp = netlistp->topScopep()->scopep();
m_commitFuncp
= new AstCFunc{netlistp->fileline(), "_timing_commit", scopeTopp, ""};
m_commitFuncp->dontCombine(true);
m_commitFuncp->isLoose(true);
m_commitFuncp->isConst(false);
m_commitFuncp->declPrivate(true);
scopeTopp->addActivep(m_commitFuncp);
}
AstSenTree* const sensesp = activep->sensesp();
FileLine* const flp = sensesp->fileline();
// Negate the sensitivity. We will commit only if the event wasn't triggered on the
// current iteration
auto* const negSensesp = sensesp->cloneTree(false);
negSensesp->sensesp()->sensp(
new AstLogNot{flp, negSensesp->sensesp()->sensp()->unlinkFrBack()});
sensesp->addNextHere(negSensesp);
auto* const newactp = new AstActive{flp, "", negSensesp};
// Create the commit call and put it in the commit function
auto* const commitp = new AstCMethodHard{
flp, new AstVarRef{flp, schedulerp, VAccess::READWRITE}, "commit"};
commitp->addPinsp(resumep->pinsp()->cloneTree(false));
commitp->statement(true);
commitp->dtypeSetVoid();
newactp->addStmtsp(commitp);
m_commitFuncp->addStmtsp(newactp);
}
// We still haven't created a commit function (no trigger schedulers), return null
if (!m_commitFuncp) return nullptr;
}
return new AstCCall{m_commitFuncp->fileline(), m_commitFuncp};
}
//============================================================================
// Creates the timing kit and marks variables written by suspendables
TimingKit prepareTiming(AstNetlist* const netlistp) {
if (!v3Global.usesTiming()) return {};
class AwaitVisitor final : public VNVisitor {
private:
// NODE STATE
// AstSenTree::user1() -> bool. Set true if the sentree has been visited.
const VNUser1InUse m_inuser1;
// STATE
bool m_inProcess = false; // Are we in a process?
bool m_gatherVars = false; // Should we gather vars in m_writtenBySuspendable?
AstScope* const m_scopeTopp; // Scope at the top
LogicByScope& m_lbs; // Timing resume actives
// Additional var sensitivities
std::map<const AstVarScope*, std::set<AstSenTree*>>& m_externalDomains;
std::set<AstSenTree*> m_processDomains; // Sentrees from the current process
// Variables written by suspendable processes
std::vector<AstVarScope*> m_writtenBySuspendable;
// METHODS
// Create an active with a timing scheduler resume() call
void createResumeActive(AstCAwait* const awaitp) {
auto* const methodp = VN_AS(awaitp->exprp(), CMethodHard);
AstVarScope* const schedulerp = VN_AS(methodp->fromp(), VarRef)->varScopep();
AstSenTree* const sensesp = awaitp->sensesp();
FileLine* const flp = sensesp->fileline();
// Create a resume() call on the timing scheduler
auto* const resumep = new AstCMethodHard{
flp, new AstVarRef{flp, schedulerp, VAccess::READWRITE}, "resume"};
if (schedulerp->dtypep()->basicp()->isTriggerScheduler()) {
resumep->addPinsp(methodp->pinsp()->cloneTree(false));
}
resumep->statement(true);
resumep->dtypeSetVoid();
// Put it in an active and put that in the global resume function
auto* const activep = new AstActive{flp, "_timing", sensesp};
activep->addStmtsp(resumep);
m_lbs.emplace_back(m_scopeTopp, activep);
}
// VISITORS
virtual void visit(AstNodeProcedure* const nodep) override {
UASSERT_OBJ(!m_inProcess && !m_gatherVars && m_processDomains.empty()
&& m_writtenBySuspendable.empty(),
nodep, "Process in process?");
m_inProcess = true;
m_gatherVars = nodep->isSuspendable(); // Only gather vars in a suspendable
const VNUser2InUse user2InUse; // AstVarScope -> bool: Set true if var has been added
// to m_writtenBySuspendable
iterateChildren(nodep);
for (AstVarScope* const vscp : m_writtenBySuspendable) {
m_externalDomains[vscp].insert(m_processDomains.begin(), m_processDomains.end());
vscp->varp()->setWrittenBySuspendable();
}
m_processDomains.clear();
m_writtenBySuspendable.clear();
m_inProcess = false;
m_gatherVars = false;
}
virtual void visit(AstFork* nodep) override {
VL_RESTORER(m_gatherVars);
if (m_inProcess) m_gatherVars = true;
// If not in a process, we don't need to gather variables or domains
iterateChildren(nodep);
}
virtual void visit(AstCAwait* nodep) override {
if (AstSenTree* const sensesp = nodep->sensesp()) {
if (!sensesp->user1SetOnce()) createResumeActive(nodep);
nodep->clearSensesp(); // Clear as these sentrees will get deleted later
if (m_inProcess) m_processDomains.insert(sensesp);
}
}
virtual void visit(AstNodeVarRef* nodep) override {
if (m_gatherVars && nodep->access().isWriteOrRW()
&& !nodep->varScopep()->user2SetOnce()) {
m_writtenBySuspendable.push_back(nodep->varScopep());
}
}
//--------------------
virtual void visit(AstNodeMath*) override {} // Accelerate
virtual void visit(AstNode* nodep) override { iterateChildren(nodep); }
public:
// CONSTRUCTORS
explicit AwaitVisitor(AstNetlist* nodep, LogicByScope& lbs,
std::map<const AstVarScope*, std::set<AstSenTree*>>& externalDomains)
: m_scopeTopp{nodep->topScopep()->scopep()}
, m_lbs{lbs}
, m_externalDomains{externalDomains} {
iterate(nodep);
}
virtual ~AwaitVisitor() override = default;
};
LogicByScope lbs;
std::map<const AstVarScope*, std::set<AstSenTree*>> externalDomains;
AwaitVisitor{netlistp, lbs, externalDomains};
return {std::move(lbs), std::move(externalDomains)};
}
//============================================================================
// Visits all forks and transforms their sub-statements into separate functions.
void transformForks(AstNetlist* const netlistp) {
if (!v3Global.usesTiming()) return;
// Transform all forked processes into functions
class ForkVisitor final : public VNVisitor {
private:
// NODE STATE
// AstVar::user1() -> bool. Set true if the variable was declared before the current
// fork.
const VNUser1InUse m_inuser1;
// STATE
bool m_inClass = false; // Are we in a class?
AstFork* m_forkp = nullptr; // Current fork
AstCFunc* m_funcp = nullptr; // Current function
// METHODS
// Remap local vars referenced by the given fork function
// TODO: We should only pass variables to the fork that are
// live in the fork body, but for that we need a proper data
// flow analysis framework which we don't have at the moment
void remapLocals(AstCFunc* const funcp, AstCCall* const callp) {
const VNUser2InUse user2InUse; // AstVarScope -> AstVarScope: var to remap to
funcp->foreach<AstNodeVarRef>([&](AstNodeVarRef* refp) {
AstVar* const varp = refp->varp();
AstBasicDType* const dtypep = varp->dtypep()->basicp();
// If it a fork sync or an intra-assignment variable, pass it by value
const bool passByValue = (dtypep && dtypep->isForkSync())
|| VString::startsWith(varp->name(), "__Vintra");
// Only handle vars passed by value or locals declared before the fork
if (!passByValue && (!varp->user1() || !varp->isFuncLocal())) return;
if (passByValue) {
// We can just pass it to the new function
} else if (m_forkp->joinType().join()) {
// If it's fork..join, we can refer to variables from the parent process
if (!m_funcp->user1SetOnce()) { // Only do this once per function
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
// Move all locals to the heap before the fork
auto* const awaitp = new AstCAwait{
m_forkp->fileline(), new AstCStmt{m_forkp->fileline(), "VlNow{}"}};
awaitp->statement(true);
m_forkp->addHereThisAsNext(awaitp);
}
} else {
refp->v3warn(E_UNSUPPORTED, "Unsupported: variable local to a forking process "
"accessed in a fork..join_any or fork..join_none");
return;
}
// Remap the reference
AstVarScope* const vscp = refp->varScopep();
if (!vscp->user2p()) {
// Clone the var to the new function
AstVar* const varp = refp->varp();
AstVar* const newvarp
= new AstVar{varp->fileline(), VVarType::BLOCKTEMP, varp->name(), varp};
newvarp->funcLocal(true);
newvarp->direction(passByValue ? VDirection::INPUT : VDirection::REF);
funcp->addArgsp(newvarp);
AstVarScope* const newvscp
= new AstVarScope{newvarp->fileline(), funcp->scopep(), newvarp};
funcp->scopep()->addVarp(newvscp);
vscp->user2p(newvscp);
callp->addArgsp(new AstVarRef{refp->fileline(), vscp, VAccess::READ});
}
auto* const newvscp = VN_AS(vscp->user2p(), VarScope);
refp->varScopep(newvscp);
refp->varp(newvscp->varp());
});
}
// VISITORS
virtual void visit(AstNodeModule* nodep) override {
VL_RESTORER(m_inClass);
m_inClass = VN_IS(nodep, Class);
iterateChildren(nodep);
}
virtual void visit(AstCFunc* nodep) override {
m_funcp = nodep;
iterateChildren(nodep);
m_funcp = nullptr;
}
virtual void visit(AstVar* nodep) override { nodep->user1(true); }
virtual void visit(AstFork* nodep) override {
VL_RESTORER(m_forkp);
m_forkp = nodep;
iterateChildrenConst(nodep); // Const, so we don't iterate the calls twice
// Replace self with the function calls (no co_await, as we don't want the main
// process to suspend whenever any of the children do)
nodep->replaceWith(nodep->stmtsp()->unlinkFrBackWithNext());
VL_DO_DANGLING(nodep->deleteTree(), nodep);
}
virtual void visit(AstBegin* nodep) override {
UASSERT_OBJ(m_forkp, nodep, "Begin outside of a fork");
UASSERT_OBJ(!nodep->name().empty(), nodep, "Begin needs a name");
FileLine* const flp = nodep->fileline();
// Create a function to put this begin's statements in
AstCFunc* const newfuncp
= new AstCFunc{flp, nodep->name(), m_funcp->scopep(), "VlCoroutine"};
m_funcp->addNextHere(newfuncp);
newfuncp->isLoose(m_funcp->isLoose());
newfuncp->slow(m_funcp->slow());
newfuncp->isConst(m_funcp->isConst());
newfuncp->declPrivate(true);
// Replace the begin with a call to the newly created function
auto* const callp = new AstCCall{flp, newfuncp};
nodep->replaceWith(callp);
// If we're in a class, add a vlSymsp arg
if (m_inClass) {
newfuncp->argTypes(EmitCBaseVisitor::symClassVar());
callp->argTypes("vlSymsp");
}
// Put the begin's statements in the function, delete the begin
newfuncp->addStmtsp(nodep->stmtsp()->unlinkFrBackWithNext());
VL_DO_DANGLING(nodep->deleteTree(), nodep);
remapLocals(newfuncp, callp);
}
//--------------------
virtual void visit(AstNodeMath*) override {} // Accelerate
virtual void visit(AstNode* nodep) override { iterateChildren(nodep); }
public:
// CONSTRUCTORS
explicit ForkVisitor(AstNetlist* nodep) { iterate(nodep); }
virtual ~ForkVisitor() override = default;
};
ForkVisitor{netlistp};
V3Global::dumpCheckGlobalTree("sched_forks", 0, v3Global.opt.dumpTreeLevel(__FILE__) >= 6);
}
} // namespace V3Sched