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 <[email protected]>
This commit is contained in:
Krzysztof Bieganski
2022-08-22 13:26:32 +01:00
committed by GitHub
parent 7cc89b8b42
commit 39af5d020e
201 changed files with 8216 additions and 538 deletions
+41 -5
View File
@@ -42,6 +42,7 @@ private:
const AstNode* const m_startNodep; // Start node of count
bool m_tracingCall = false; // Iterating into a CCall to a CFunc
bool m_inCFunc = false; // Inside AstCFunc
bool m_ignoreRemaining = false; // Ignore remaining statements in the block
const bool m_assertNoDups; // Check for duplicates
const std::ostream* const m_osp; // Dump file
@@ -81,7 +82,12 @@ public:
uint32_t instrCount() const { return m_instrCount; }
private:
void reset() {
m_instrCount = 0;
m_ignoreRemaining = false;
}
uint32_t startVisitBase(AstNode* nodep) {
UASSERT_OBJ(!m_ignoreRemaining, nodep, "Should not reach here if ignoring");
if (m_assertNoDups && !m_inCFunc) {
// Ensure we don't count the same node twice
//
@@ -110,7 +116,7 @@ private:
void endVisitBase(uint32_t savedCount, AstNode* nodep) {
UINFO(8, "cost " << std::setw(6) << std::left << m_instrCount << " " << nodep << endl);
markCost(nodep);
m_instrCount += savedCount;
if (!m_ignoreRemaining) m_instrCount += savedCount;
}
void markCost(AstNode* nodep) {
if (m_osp) nodep->user4(m_instrCount + 1); // Else don't mark to avoid writeback
@@ -118,6 +124,7 @@ private:
// VISITORS
virtual void visit(AstNodeSel* nodep) override {
if (m_ignoreRemaining) return;
// This covers both AstArraySel and AstWordSel
//
// If some vector is a bazillion dwords long, and we're selecting 1
@@ -129,6 +136,7 @@ private:
iterateAndNextNull(nodep->bitp());
}
virtual void visit(AstSel* nodep) override {
if (m_ignoreRemaining) return;
// Similar to AstNodeSel above, a small select into a large vector
// is not expensive. Count the cost of the AstSel itself (scales with
// its width) and the cost of the lsbp() and widthp() nodes, but not
@@ -144,6 +152,7 @@ private:
nodep->v3fatalSrc("AstMemberSel unhandled");
}
virtual void visit(AstConcat* nodep) override {
if (m_ignoreRemaining) return;
// Nop.
//
// Ignore concat. The problem with counting concat is that when we
@@ -164,22 +173,24 @@ private:
markCost(nodep);
}
virtual void visit(AstNodeIf* nodep) override {
if (m_ignoreRemaining) return;
const VisitBase vb{this, nodep};
iterateAndNextNull(nodep->condp());
const uint32_t savedCount = m_instrCount;
UINFO(8, "ifsp:\n");
m_instrCount = 0;
reset();
iterateAndNextNull(nodep->ifsp());
uint32_t ifCount = m_instrCount;
if (nodep->branchPred().unlikely()) ifCount = 0;
UINFO(8, "elsesp:\n");
m_instrCount = 0;
reset();
iterateAndNextNull(nodep->elsesp());
uint32_t elseCount = m_instrCount;
if (nodep->branchPred().likely()) elseCount = 0;
reset();
if (ifCount >= elseCount) {
m_instrCount = savedCount + ifCount;
if (nodep->elsesp()) nodep->elsesp()->user4(0); // Don't dump it
@@ -189,6 +200,7 @@ private:
}
}
virtual void visit(AstNodeCond* nodep) override {
if (m_ignoreRemaining) return;
// Just like if/else above, the ternary operator only evaluates
// one of the two expressions, so only count the max.
const VisitBase vb{this, nodep};
@@ -196,15 +208,16 @@ private:
const uint32_t savedCount = m_instrCount;
UINFO(8, "?\n");
m_instrCount = 0;
reset();
iterateAndNextNull(nodep->expr1p());
const uint32_t ifCount = m_instrCount;
UINFO(8, ":\n");
m_instrCount = 0;
reset();
iterateAndNextNull(nodep->expr2p());
const uint32_t elseCount = m_instrCount;
reset();
if (ifCount < elseCount) {
m_instrCount = savedCount + elseCount;
if (nodep->expr1p()) nodep->expr1p()->user4(0); // Don't dump it
@@ -213,6 +226,25 @@ private:
if (nodep->expr2p()) nodep->expr2p()->user4(0); // Don't dump it
}
}
virtual void visit(AstCAwait* nodep) override {
if (m_ignoreRemaining) return;
iterateChildren(nodep);
// Anything past a co_await is irrelevant
m_ignoreRemaining = true;
}
virtual void visit(AstFork* nodep) override {
if (m_ignoreRemaining) return;
const VisitBase vb{this, nodep};
uint32_t totalCount = m_instrCount;
// Sum counts in each statement until the first await
for (AstNode* stmtp = nodep->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
reset();
iterate(stmtp);
totalCount += m_instrCount;
}
m_instrCount = totalCount;
m_ignoreRemaining = false;
}
virtual void visit(AstActive* nodep) override {
// You'd think that the OrderLogicVertex's would be disjoint trees
// of stuff in the AST, but it isn't so: V3Order makes an
@@ -230,6 +262,7 @@ private:
UASSERT_OBJ(nodep == m_startNodep, nodep, "Multiple actives, or not start node");
}
virtual void visit(AstNodeCCall* nodep) override {
if (m_ignoreRemaining) return;
const VisitBase vb{this, nodep};
iterateChildren(nodep);
m_tracingCall = true;
@@ -241,6 +274,7 @@ private:
// from the root
UASSERT_OBJ(m_tracingCall || nodep == m_startNodep, nodep,
"AstCFunc not under AstCCall, or not start node");
UASSERT_OBJ(!m_ignoreRemaining, nodep, "Should not be ignoring at the start of a CFunc");
m_tracingCall = false;
VL_RESTORER(m_inCFunc);
{
@@ -248,8 +282,10 @@ private:
const VisitBase vb{this, nodep};
iterateChildren(nodep);
}
m_ignoreRemaining = false;
}
virtual void visit(AstNode* nodep) override {
if (m_ignoreRemaining) return;
const VisitBase vb{this, nodep};
iterateChildren(nodep);
}