Optimize input combinational logic by change detection

When a lot of combinational logic is driven from top level inputs,
work can be wasted evaluating that logic if the top level inputs don't
change.

This change adds an optimization by performing a change detect on the
top level inputs, and evaluate 'ico' logic only if the top level input
actually changed. This especially helps with --hierarchical/--lib-create
which runs the 'ico' of each sub-model in the eval settle loop.

This was observed to yield 40%+ run-time speedup on some partitioned
designs.

The added change detection is cheap, so it is emitted even if the 'ico'
region is small, and is on by default.

The optimization is only sound if the model itself does not write to the
top level inputs (otherwise the 'previous value' variables would be out
of sync, which are not updated by internal writes.). If we can detect a
top level input is written within the design, then for that input, we
fall back on always running the relevant logic. With --vpi we cannot
prove safety statically, so --vpi will disable this optimisation unless
explicitly enabled. (In which case it's the user's responsibility to not
write to top level inputs via the VPI.)
This commit is contained in:
Geza Lore 2025-11-25 09:50:06 -06:00
parent 77e6a21224
commit 13111d0a28
17 changed files with 293 additions and 17 deletions

View File

@ -737,6 +737,21 @@ Summary:
this is not recommended as may cause additional warnings and ordering
issues.
.. option:: -fno-ico-change-detect
Rarely needed. Disable input change detection in the input combinational
('ico') region. With change detection enabled (the default, unless
:vlopt:`--vpi` is passed), the input combinational logic is evaluated only
when a top level input has actually changed, rather than unconditionally on
the first scheduling iteration.
The change detection logic assumes a top level input only ever changes
externally between evaluations. The optimization is automatically disabled
for top level input signals that are written within the design. Accesses via
the VPI cannot be analyzed at compile time, therefore :vlopt:`--vpi`
disables this optimization. It can be turned back on by explicitly passing
:vlopt:`-fico-change-detect`.
.. option:: -fno-inline
.. option:: -fno-inline-funcs

View File

@ -2151,6 +2151,7 @@ class AstVar final : public AstNode {
bool m_isDpiOpenArray : 1; // DPI import open array
bool m_isHideLocal : 1; // Verilog local
bool m_isHideProtected : 1; // Verilog protected
bool m_maybeWritten : 1; // Design might write to this signal (not very accurate)
bool m_noCReset : 1; // Do not do automated CReset creation
bool m_noReset : 1; // Do not do automated reset/randomization
bool m_noSubst : 1; // Do not substitute out references
@ -2213,6 +2214,7 @@ class AstVar final : public AstNode {
m_isDpiOpenArray = false;
m_isHideLocal = false;
m_isHideProtected = false;
m_maybeWritten = false;
m_noCReset = false;
m_noReset = false;
m_noSubst = false;
@ -2385,6 +2387,8 @@ public:
void isHideLocal(bool flag) { m_isHideLocal = flag; }
bool isHideProtected() const { return m_isHideProtected; }
void isHideProtected(bool flag) { m_isHideProtected = flag; }
void maybeWritten(bool flag) { m_maybeWritten = flag; }
bool maybeWritten() const { return m_maybeWritten; }
bool noCReset() const { return m_noCReset; }
void noCReset(bool flag) { m_noCReset = flag; }
bool noReset() const { return m_noReset; }

View File

@ -291,6 +291,7 @@ void V3LinkLevel::wrapTopCell(AstNetlist* rootp) {
varp->sigPublic(true); // User needs to be able to get to it...
oldvarp->primaryIO(false);
varp->primaryIO(true);
varp->maybeWritten(oldvarp->maybeWritten());
if (varp->isRef() || varp->isConstRef()) {
varp->v3warn(E_UNSUPPORTED,
"Unsupported: ref/const ref as primary input/output: "

View File

@ -1077,6 +1077,9 @@ void V3Options::notify() VL_MT_DISABLED {
// Preprocessor defines based on options used
if (timing().isSetTrue()) V3PreShell::defineCmdLine("VERILATOR_TIMING", "1");
// If VPI is used, and no explicit ico change detect option was passed, disable it by default
if (m_vpi && m_fIcoChangeDetect.isDefault()) m_fIcoChangeDetect.setTrueOrFalse(false);
// === Leave last
// Mark options as available
m_available = true;
@ -1483,6 +1486,9 @@ void V3Options::parseOptsList(FileLine* fl, const string& optdir, int argc,
DECL_OPTION("-ffunc-opt-balance-cat", FOnOff, &m_fFuncBalanceCat);
DECL_OPTION("-ffunc-opt-split-cat", FOnOff, &m_fFuncSplitCat);
DECL_OPTION("-fgate", FOnOff, &m_fGate);
DECL_OPTION("-fico-change-detect", CbFOnOff, [this](bool flag) { //
m_fIcoChangeDetect.setTrueOrFalse(flag);
});
DECL_OPTION("-finline", FOnOff, &m_fInline);
DECL_OPTION("-finline-funcs", FOnOff, &m_fInlineFuncs);
DECL_OPTION("-finline-funcs-eager", FOnOff, &m_fInlineFuncsEager);

View File

@ -410,6 +410,8 @@ private:
bool m_fFuncBalanceCat = true; // main switch: -fno-func-balance-cat: expansion of C macros
bool m_fFuncSplitCat = true; // main switch: -fno-func-split-cat: expansion of C macros
bool m_fGate; // main switch: -fno-gate: gate wire elimination
// main switch: -fno-ico-change-detect: input change detection optimization
VOptionBool m_fIcoChangeDetect{VOptionBool::OPT_DEFAULT_TRUE};
bool m_fInline; // main switch: -fno-inline: module inlining
bool m_fInlineFuncs = true; // main switch: -fno-inline-funcs: function inlining
bool m_fInlineFuncsEager = true; // main switch: -fno-inline-funcs-eager: don't inline eagerly
@ -745,6 +747,7 @@ public:
bool fFuncSplitCat() const { return m_fFuncSplitCat; }
bool fFunc() const { return fFuncSplitCat() || fFuncBalanceCat(); }
bool fGate() const { return m_fGate; }
VOptionBool fIcoChangeDetect() const { return m_fIcoChangeDetect; }
bool fInline() const { return m_fInline; }
bool fInlineFuncs() const { return m_fInlineFuncs; }
bool fInlineFuncsEager() const { return m_fInlineFuncsEager; }

View File

@ -529,8 +529,46 @@ AstNode* createInputCombLoop(AstNetlist* netlistp, AstCFunc* const initFuncp,
+ entry.m_memberp->name());
}
// Create the input change detect SenTrees.
// If there is a lot of combinationallogic hanging of the top level inputs, we can save
// a lot of work by only evaluating it if an input has actually changed. This in
// paticular helps hierarchical models partitioned across combinaitonal boundaries.
// The change detect itself should be fairly cheap otherwise so alway do it.
// For correctness, don't create a change detect for top level inputs also written
// by the design, as the change detect 'previous value' would get out of sync.
// Also omit a SenTree for types that don't have the required '!=' operator.
// Any signal that does not have an explicit change detect trigger will fall back to
// using the 'first iteration' trigger, same as if this optimization was disabled.
std::unordered_map<const AstVarScope*, AstSenTree*> inp2changedp;
std::vector<AstSenTree*> icoChangeSenTreeps;
if (v3Global.opt.fIcoChangeDetect().isTrue()) {
FileLine* const flp = netlistp->fileline();
AstScope* const scopep = netlistp->topScopep()->scopep();
for (AstVarScope* vscp = scopep->varsp(); vscp; vscp = VN_AS(vscp->nextp(), VarScope)) {
// Only for top level ports, assume outputs don't change externally
if (!vscp->varp()->isPrimaryInish()) continue;
// Don't do if written by the design - wouldn't update the change detect 'prev' value
if (vscp->varp()->maybeWritten()) continue;
// Don't do if forceable, as we can't see the actual value - this is belt and braces
if (vscp->varp()->isForced()) continue;
// Can't handle unpacked arrays (they have special types when primary input)
if (VN_IS(vscp->dtypep()->skipRefp(), UnpackArrayDType)) continue;
// Similarly to arrays, can't handle SystemC types
if (vscp->varp()->isSc()) continue;
// Create a sen tree triggered when this input changes
AstSenTree*& senTreepr = inp2changedp[vscp];
UASSERT_OBJ(!senTreepr, vscp, "Duplicate input change detect trigger");
AstVarRef* const refp = new AstVarRef{flp, vscp, VAccess::READ};
AstSenItem* const senItemp = new AstSenItem{flp, VEdgeType::ET_CHANGED, refp};
senTreepr = new AstSenTree{flp, senItemp};
icoChangeSenTreeps.push_back(senTreepr);
}
}
V3Stats::addStat("Scheduling, 'ico' change detect triggers", icoChangeSenTreeps.size());
// Gather the relevant sensitivity expressions and create the trigger kit
const auto& senTreeps = getSenTreesUsedBy({&logic});
std::vector<const AstSenTree*> senTreeps = getSenTreesUsedBy({&logic});
senTreeps.insert(senTreeps.end(), icoChangeSenTreeps.begin(), icoChangeSenTreeps.end());
const TriggerKit trigKit = TriggerKit::create(netlistp, initFuncp, senExprBuilder, {},
senTreeps, "ico", extraTriggers, false, false);
std::ignore = senExprBuilder.getAndClearResults();
@ -543,14 +581,14 @@ AstNode* createInputCombLoop(AstNetlist* netlistp, AstCFunc* const initFuncp,
// Remap sensitivities
remapSensitivities(logic, trigKit.mapVec());
for (auto& pair : inp2changedp) pair.second = trigKit.mapVec().at(pair.second);
// Create the inverse map from trigger ref AstSenTree to original AstSenTree
V3Order::TrigToSenMap trigToSen;
invertAndMergeSenTreeMap(trigToSen, trigKit.mapVec());
// The trigger top level inputs (first iteration)
AstSenTree* const inputChanged
= trigKit.newExtraTriggerSenTree(trigKit.vscp(), firstIterationTrigger);
// The 'first iteration' trigger for top level inputs - lazy constructed only if needed
AstSenTree* firstIterTriggerp = nullptr;
// The DPI Export trigger
AstSenTree* const dpiExportTriggered
@ -565,9 +603,19 @@ AstNode* createInputCombLoop(AstNetlist* netlistp, AstCFunc* const initFuncp,
netlistp, {&logic}, trigToSen, "ico", false, false,
[&](const AstVarScope* vscp, std::vector<AstSenTree*>& out) {
AstVar* const varp = vscp->varp();
if (varp->isPrimaryInish() || varp->isSigUserRWPublic()) {
out.push_back(inputChanged);
// If it has an explicit change detect trigger, use that,
// otherwise fall back to using the 'first iteration' trigger
auto it = inp2changedp.find(vscp);
if (it != inp2changedp.end()) {
out.push_back(it->second);
} else if (varp->isPrimaryInish() || varp->isSigUserRWPublic()) {
if (!firstIterTriggerp) {
firstIterTriggerp
= trigKit.newExtraTriggerSenTree(trigKit.vscp(), firstIterationTrigger);
}
out.push_back(firstIterTriggerp);
}
// Add other triggers
if (varp->isWrittenByDpi()) out.push_back(dpiExportTriggered);
if (vscp->varp()->sensIfacep() || vscp->varp()->isVirtIface()) {
const auto& ifaceTriggered
@ -593,8 +641,14 @@ AstNode* createInputCombLoop(AstNetlist* netlistp, AstCFunc* const initFuncp,
// Work statements: Invoke the 'ico' function
util::callVoidFunc(icoFuncp));
// Add the first iteration trigger to the trigger computation function
trigKit.addExtraTriggerAssignment(icoLoop.firstIterp, firstIterationTrigger, false);
// Add the first iteration trigger to the trigger computation function - if used
if (firstIterTriggerp) {
trigKit.addExtraTriggerAssignment(icoLoop.firstIterp, firstIterationTrigger, false);
}
// Release temporary input change detect SenTrees
for (AstSenTree* const senTreep : icoChangeSenTreeps) senTreep->deleteTree();
icoChangeSenTreeps.clear();
return icoLoop.stmtsp;
}

View File

@ -3073,6 +3073,7 @@ class WidthVisitor final : public VNVisitor {
UASSERT_OBJ(nodep->dtypep(), nodep, "LHS var should be dtype completed");
}
// UINFOTREE(9, nodep, "", "VRout");
if (nodep->access().isWriteOrRW()) nodep->varp()->maybeWritten(true);
if (nodep->access().isWriteOrRW() && nodep->varp()->direction() == VDirection::CONSTREF) {
nodep->v3error("Assigning to const ref variable: " << nodep->prettyNameQ());
} else if (nodep->access().isWriteOrRW() && nodep->varp()->isInput()

View File

@ -13,6 +13,6 @@ test.scenarios('vlt')
test.compile(verilator_flags2=['--expand-limit 1 --stats -fno-dfg'])
test.file_grep(test.stats, r'Optimizations, expand limited\s+(\d+)', 7)
test.file_grep(test.stats, r'Optimizations, expand limited\s+(\d+)', 29)
test.passes()

View File

@ -14,7 +14,7 @@ test.scenarios('vlt')
test.compile(
verilator_flags2=["--stats", "--build", "--gate-stmts", "10000", "--expand-limit", "128"])
test.file_grep(test.stats, r'Optimizations, FuncOpt concat trees balanced\s+(\d+)', 2)
test.file_grep(test.stats, r'Optimizations, FuncOpt concat trees balanced\s+(\d+)', 3)
test.file_grep(test.stats, r'Optimizations, FuncOpt concat splits\s+(\d+)', 67)
test.passes()

View File

@ -0,0 +1,45 @@
// -*- mode: C++; c-file-style: "cc-mode" -*-
//
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
#include <verilated.h>
#include <memory>
#include <string>
#include VM_PREFIX_INCLUDE
int main(int argc, char** argv) {
const std::unique_ptr<VerilatedContext> contextp{new VerilatedContext};
contextp->threads(1);
contextp->commandArgs(argc, argv);
const std::unique_ptr<VM_PREFIX> topp{new VM_PREFIX{contextp.get(), "top"}};
topp->clk = 0;
topp->i = 0;
topp->eval();
while ((contextp->time() < 10000) && !contextp->gotFinish()) {
contextp->timeInc(1);
topp->clk = !topp->clk;
// Always set to the same constant value, so change detection will think it's not changing
topp->i = 500000;
topp->eval();
if (topp->o != topp->i + 10) {
const std::string msg = "%Error: incorrect output, got: " + std::to_string(topp->o)
+ " expected: " + std::to_string(topp->i + 10);
vl_fatal(__FILE__, __LINE__, "main", msg.c_str());
break;
}
}
if (!contextp->gotFinish()) {
vl_fatal(__FILE__, __LINE__, "main", "%Error: Timeout; never got a $finish");
}
topp->final();
return 0;
}

View File

@ -0,0 +1,28 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# 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-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('vlt')
test.top_filename = "t/t_sched_ico_change_detect_input_assigned.v"
test.compile(make_top_shell=False,
make_main=False,
verilator_flags2=[
"--exe", "--stats", "-Wno-ASSIGNIN",
"t/t_sched_ico_change_detect_input_assigned.cpp"
])
test.execute()
# There should be a change detect for 'clk', but not for 'i'
test.file_grep(test.stats, r"Scheduling, 'ico' change detect triggers\s+(\d+)", 1)
test.passes()

View File

@ -0,0 +1,38 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// verilog_format: off
`define stop $stop
`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0x exp=%0x (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0);
`define checks(gotv,expv) do if ((gotv) != (expv)) begin $write("%%Error: %s:%0d: got='%s' exp='%s'\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0);
// verilog_format: on
module t(
clk, i, o, cyc
);
input clk, i;
output o, cyc;
logic clk;
int i; // Primary input that the design also drives
int o;
int cyc = 0;
// Logic dependent on primary input 'i'
always_comb o = i + 10;
always @(posedge clk) begin
cyc <= cyc + 1;
// On even cycles, assign 'i'
if (cyc % 2 == 0) i = cyc + 1000;
if (cyc == 99) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
endmodule

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# 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-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('vlt')
test.top_filename = "t/t_sched_ico_change_detect_input_assigned.v"
test.compile(make_top_shell=False,
make_main=False,
verilator_flags2=[
"--exe", "--stats", "-Wno-ASSIGNIN",
"t/t_sched_ico_change_detect_input_assigned.cpp", "-fno-ico-change-detect"
])
test.execute()
test.file_grep(test.stats, r"Scheduling, 'ico' change detect triggers\s+(\d+)", 0)
test.passes()

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# 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-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('vlt')
test.top_filename = "t/t_sched_ico_change_detect_input_assigned.v"
test.compile(make_top_shell=False,
make_main=False,
verilator_flags2=[
"--exe", "--stats", "-Wno-ASSIGNIN",
"t/t_sched_ico_change_detect_input_assigned.cpp", "--vpi"
])
test.execute()
test.file_grep(test.stats, r"Scheduling, 'ico' change detect triggers\s+(\d+)", 0)
test.passes()

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# 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-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('vlt')
test.top_filename = "t/t_sched_ico_change_detect_input_assigned.v"
test.compile(make_top_shell=False,
make_main=False,
verilator_flags2=[
"--exe", "--stats", "-Wno-ASSIGNIN",
"t/t_sched_ico_change_detect_input_assigned.cpp", "-fico-change-detect", "--vpi"
])
test.execute()
test.file_grep(test.stats, r"Scheduling, 'ico' change detect triggers\s+(\d+)", 1)
test.passes()

View File

@ -139,12 +139,12 @@ C 'ft/t_wrapper_context.vl21n3tlinepagev_line/topoblockS21-24,28,3
C 'ft/t_wrapper_context.vl35n3tlinepagev_line/topoblockS35htop0.top' 11
C 'ft/t_wrapper_context.vl36n5tbranchpagev_branch/topoifS36htop0.top' 1
C 'ft/t_wrapper_context.vl36n6tbranchpagev_branch/topoelseS37htop0.top' 10
C 'ft/t_wrapper_context.vl39n3tlinepagev_line/topoblockS39-40htop0.top' 34
C 'ft/t_wrapper_context.vl39n3tlinepagev_line/topoblockS39-40htop0.top' 13
C 'ft/t_wrapper_context.vl41n5tbranchpagev_branch/topoifS41htop0.top' 0
C 'ft/t_wrapper_context.vl41n6tbranchpagev_branch/topoelseS47htop0.top' 34
C 'ft/t_wrapper_context.vl41n6tbranchpagev_branch/topoelseS47htop0.top' 13
C 'ft/t_wrapper_context.vl42n24texprpagev_expr/topo((counter >= 32'sh5)==0) => 0htop0.top' 0
C 'ft/t_wrapper_context.vl42n24texprpagev_expr/topo((counter >= 32'sh5)==1 && stop==1) => 1htop0.top' 0
C 'ft/t_wrapper_context.vl42n24texprpagev_expr/topo(stop==0) => 0htop0.top' 0
C 'ft/t_wrapper_context.vl42n8tlinepagev_line/topoelsehtop0.top' 0
C 'ft/t_wrapper_context.vl48n7tbranchpagev_branch/topoifS48-50htop0.top' 1
C 'ft/t_wrapper_context.vl48n8tbranchpagev_branch/topoelsehtop0.top' 33
C 'ft/t_wrapper_context.vl48n8tbranchpagev_branch/topoelsehtop0.top' 12

View File

@ -139,12 +139,12 @@ C 'ft/t_wrapper_context.vl21n3tlinepagev_line/topoblockS21-24,28,3
C 'ft/t_wrapper_context.vl35n3tlinepagev_line/topoblockS35htop1.top' 6
C 'ft/t_wrapper_context.vl36n5tbranchpagev_branch/topoifS36htop1.top' 1
C 'ft/t_wrapper_context.vl36n6tbranchpagev_branch/topoelseS37htop1.top' 5
C 'ft/t_wrapper_context.vl39n3tlinepagev_line/topoblockS39-40htop1.top' 19
C 'ft/t_wrapper_context.vl41n5tbranchpagev_branch/topoifS41htop1.top' 19
C 'ft/t_wrapper_context.vl39n3tlinepagev_line/topoblockS39-40htop1.top' 8
C 'ft/t_wrapper_context.vl41n5tbranchpagev_branch/topoifS41htop1.top' 8
C 'ft/t_wrapper_context.vl41n6tbranchpagev_branch/topoelseS47htop1.top' 0
C 'ft/t_wrapper_context.vl42n24texprpagev_expr/topo((counter >= 32'sh5)==0) => 0htop1.top' 18
C 'ft/t_wrapper_context.vl42n24texprpagev_expr/topo((counter >= 32'sh5)==0) => 0htop1.top' 7
C 'ft/t_wrapper_context.vl42n24texprpagev_expr/topo((counter >= 32'sh5)==1 && stop==1) => 1htop1.top' 1
C 'ft/t_wrapper_context.vl42n24texprpagev_expr/topo(stop==0) => 0htop1.top' 0
C 'ft/t_wrapper_context.vl42n8tlinepagev_line/topoelsehtop1.top' 18
C 'ft/t_wrapper_context.vl42n8tlinepagev_line/topoelsehtop1.top' 7
C 'ft/t_wrapper_context.vl48n7tbranchpagev_branch/topoifS48-50htop1.top' 0
C 'ft/t_wrapper_context.vl48n8tbranchpagev_branch/topoelsehtop1.top' 0