Support #0 delays with IEEE-1800 compliant semantics (#7079)

This patch adds IEEE-1800 compliant scheduling support for the Inactive
scheduling region used for #0 delays.

Implementing this requires that **all** IEEE-1800 active region events
are placed in the internal 'act' section. This has simulation
performance implications. It prevents some optimizations (e.g.
V3LifePost), which reduces single threaded performance. It also reduces
the available work and parallelism in the internal 'nba' section, which
reduced the effectiveness of multi-threading severely.

Performance impact on RTLMeter when using scheduling adjusted to support
proper #0 delays is ~10-20% slowdown in single-threaded mode, and ~100%
(2x slower) with --threads 4.

To avoid paying this performance penalty unconditionally, the scheduling
is only adjusted if either:
1. The input contains a statically known #0 delay
2. The input contains a variable #x delay unknown at compile time

If no #0 is present, but #x variable delays are, a ZERODLY warning is
issued advising the use of '--no-sched-zero-delay' which is a promise
by the user that none of the variable delays will evaluate to a zero
delay at run-time. This warning is turned off if '--sched-zero-delay'
is explicitly given. This is similar to the '--timing' option.

If '--no-sched-zero-delay' was used at compile time, then executing
a zero delay will fail at runtime.

A ZERODLY warning is also issued if a static #0 if found, but the user
specified '--no-sched-zero-delay'. In this case the scheduling is not
adjusted to support #0, so executing it will fail at runtime. Presumably
the user knows it won't be executed.

The intended behaviour with all this is the following:

No #0, no #var in the design (#constant is OK)
-> Same as current behaviour, scheduling not adjusted,
   same code generated as before

Has static #0 and '--no-sched-zero-delay' is NOT given:
-> No warnings, scheduling adjusted so it just works, runs slow

Has static #0 and '--no-sched-zero-delay' is given:
-> ZERODLY on the #0, scheduling not adjusted, fails at runtime if hit

No static #0, but has #var and no option is given:
-> ZERODLY on the #var advising use of '--no-sched-zero-delay' or
   '--sched-zero-delay' (similar to '--timing'), scheduling adjusted
   assuming it can be a zero delay and it just works

No static #0, but has #var and '--no-sched-zero-delay' is given:
-> No warning, scheduling not adjusted, fails at runtime if zero delay

No static #0, but has #var and '--sched-zero-delay' is given:
-> No warning, scheduling adjusted so it just works
This commit is contained in:
Geza Lore
2026-02-16 03:55:55 +00:00
committed by GitHub
parent a0b89dde8e
commit 505d33b35a
43 changed files with 672 additions and 78 deletions
+77 -14
View File
@@ -132,7 +132,8 @@ EvalLoop createEvalLoop(
const string& name, // Name of current phase
bool slow, // Should create slow functions
const TriggerKit& trigKit, // The trigger kit
AstVarScope* trigp, // The trigger vector - may be nullptr if no triggers
AstVarScope* trigp, // The trigger vector - may be nullptr if no triggers or using 'condp'
AstNodeExpr* condp, // Explicit condition that must be true to run 'phaseWorkp'
AstNodeStmt* innerp, // The inner loop, if any
AstNodeStmt* phasePrepp, // Prep statements run before checking triggers
AstNodeStmt* phaseWorkp, // The work to do if anything triggered
@@ -141,9 +142,11 @@ EvalLoop createEvalLoop(
// and must be unmodified otherwise.
std::function<AstNodeStmt*(AstVarScope*)> phaseExtra = [](AstVarScope*) { return nullptr; } //
) {
// All work is under a trigger, so if there are no triggers, there is
// nothing to do besides executing the inner loop.
if (!trigp) return {nullptr, innerp};
UASSERT(!trigp || !condp, "Cannot use both 'trigp' and 'condp' in 'createEvalLoop'");
// All work is under a trigger or condition, so if there are none,
// there is nothing to do besides executing the inner loop.
if (!trigp && !condp) return {nullptr, innerp};
const std::string varPrefix = "__V" + tag;
AstScope* const scopeTopp = netlistp->topScopep()->scopep();
@@ -161,9 +164,10 @@ EvalLoop createEvalLoop(
// If there is work in this phase, execute it if any triggers fired
if (phaseWorkp) {
// Check if any triggers are fired, save the result
AstNodeExpr* const lhsp = new AstVarRef{flp, executeFlagp, VAccess::WRITE};
AstNodeExpr* const rhsp = trigKit.newAnySetCall(trigp);
// If using explicit condition, that directly determines whether to execute,
// otherwise check if any triggers are fired
AstNodeExpr* const rhsp = condp ? condp : trigKit.newAnySetCall(trigp);
phaseFuncp->addStmtsp(new AstAssign{flp, lhsp, rhsp});
// Add the work
@@ -216,8 +220,8 @@ EvalLoop createEvalLoop(
AstLoop* const loopp = new AstLoop{flp};
stmtps->addNext(loopp);
// Check the iteration limit (aborts if exceeded)
AstNodeStmt* const dumpCallp = trigKit.newDumpCall(trigp, tag, false);
// Check the iteration limit (aborts if exceeded). Dump triggers if using triggers.
AstNodeStmt* dumpCallp = trigp ? trigKit.newDumpCall(trigp, tag, false) : nullptr;
loopp->addStmtsp(util::checkIterationLimit(netlistp, name, counterp, dumpCallp));
// Increment the iteration counter
loopp->addStmtsp(util::incrementVar(counterp));
@@ -442,7 +446,10 @@ void createSettle(AstNetlist* netlistp, AstCFunc* const initFuncp, SenExprBuilde
// Create the eval loop
const EvalLoop stlLoop = createEvalLoop( //
netlistp, "stl", "Settle", /* slow: */ true, trigKit, trigKit.vscp(),
netlistp, "stl", "Settle", /* slow: */ true, trigKit,
// Use trigger
trigKit.vscp(), nullptr,
// Explicit condition
// Inner loop statements
nullptr,
// Prep statements: Compute the current 'stl' triggers
@@ -550,7 +557,9 @@ AstNode* createInputCombLoop(AstNetlist* netlistp, AstCFunc* const initFuncp,
// Create the eval loop
const EvalLoop icoLoop = createEvalLoop( //
netlistp, "ico", "Input combinational", /* slow: */ false, trigKit, trigKit.vscp(),
netlistp, "ico", "Input combinational", /* slow: */ false, trigKit,
// Use trigger
trigKit.vscp(), nullptr,
// Inner loop statements
nullptr,
// Prep statements: Compute the current 'ico' triggers
@@ -595,13 +604,18 @@ void createEval(AstNetlist* netlistp, //
) {
FileLine* const flp = netlistp->fileline();
// Grab the delay scheduler variable, if any
AstVarScope* const delaySchedVscp = timingKit.getDelayScheduler(netlistp);
// 'createResume' consumes the contents that 'createReady' needs, so do the right order
AstCCall* const timingReadyp = timingKit.createReady(netlistp);
AstCCall* const timingResumep = timingKit.createResume(netlistp);
// Create the active eval loop
EvalLoop topLoop = createEvalLoop( //
netlistp, "act", "Active", /* slow: */ false, trigKit, actKit.m_vscp,
netlistp, "act", "Active", /* slow: */ false, trigKit,
// Use trigger
actKit.m_vscp, nullptr,
// Inner loop statements
nullptr,
// Prep statements
@@ -640,9 +654,54 @@ void createEval(AstNetlist* netlistp, //
return workp;
}());
// Create if there are any delays, so we can check at runtime if a #0 is unexpected
if (delaySchedVscp) {
topLoop = createEvalLoop( //
netlistp, "inact", "Inactive", /* slow: */ false, trigKit,
// Use explicit condition
nullptr,
[&]() {
// Run if any zero delays are pending
AstNodeExpr* const callp
= new AstCMethodHard{flp, new AstVarRef{flp, delaySchedVscp, VAccess::READ},
VCMethod::SCHED_AWAITING_ZERO_DELAY};
callp->dtypeSetBit();
return callp;
}(),
// Inner loop statements
topLoop.stmtsp,
// Prep statements
nullptr,
// Work statements
[&]() -> AstNodeStmt* {
if (v3Global.usesZeroDelay()) {
// Resume processes watiting for #0 delay
AstCMethodHard* const callp = new AstCMethodHard{
flp, new AstVarRef{flp, delaySchedVscp, VAccess::READWRITE},
VCMethod::SCHED_RESUME_ZERO_DELAY};
callp->dtypeSetVoid();
return callp->makeStmt();
} else {
// Assumption was that the design doesn't use #0 delays.
// Die at run-time if it does.
AstCStmt* const stmtp = new AstCStmt{flp};
const FileLine* const locp = netlistp->topModulep()->fileline();
const std::string& file = VIdProtect::protect(locp->filename());
const std::string& line = std::to_string(locp->lineno());
stmtp->add(
"VL_FATAL_MT(\"" + V3OutFormatter::quoteNameControls(file) + "\", " + line
+ ", \"\", \"ZERODLY: Design Verilated with '--no-sched-zero-delay', "
+ "but #0 delay executed at runtime\");");
return stmtp;
}
}());
}
// Create the NBA eval loop, which is the default top level loop.
topLoop = createEvalLoop( //
netlistp, "nba", "NBA", /* slow: */ false, trigKit, nbaKit.m_vscp,
netlistp, "nba", "NBA", /* slow: */ false, trigKit,
// Use trigger
nbaKit.m_vscp, nullptr,
// Inner loop statements
topLoop.stmtsp,
// Prep statements
@@ -687,7 +746,9 @@ void createEval(AstNetlist* netlistp, //
if (!obsKit.empty()) {
// Create the Observed eval loop, which becomes the top level loop.
topLoop = createEvalLoop( //
netlistp, "obs", "Observed", /* slow: */ false, trigKit, obsKit.m_vscp,
netlistp, "obs", "Observed", /* slow: */ false, trigKit,
// Use trigger
obsKit.m_vscp, nullptr,
// Inner loop statements
topLoop.stmtsp,
// Prep statements
@@ -711,7 +772,9 @@ void createEval(AstNetlist* netlistp, //
if (!reactKit.empty()) {
// Create the Reactive eval loop, which becomes the top level loop.
topLoop = createEvalLoop( //
netlistp, "react", "Reactive", /* slow: */ false, trigKit, reactKit.m_vscp,
netlistp, "react", "Reactive", /* slow: */ false, trigKit,
// Use trigger
reactKit.m_vscp, nullptr,
// Inner loop statements
topLoop.stmtsp,
// Prep statements