Optimize DFG temporary sharing across module instances (#8456)

This commit is contained in:
Michael Bedford Taylor
2026-09-28 00:46:27 +01:00
committed by GitHub
parent c30604222f
commit 2c441d853c
10 changed files with 196 additions and 67 deletions
+27 -19
View File
@@ -21,6 +21,7 @@
#include "V3Ast.h"
#include "V3EmitV.h"
#include "V3File.h"
#include "V3Stats.h"
VL_DEFINE_DEBUG_FUNCTIONS;
@@ -31,6 +32,8 @@ DfgGraph::DfgGraph(const string& name)
: m_name{name} {}
DfgGraph::~DfgGraph() {
V3Stats::addStatSum("Optimizations, DFG, temporary declarations reused",
static_cast<double>(m_tempDeclarationsReused));
forEachVertex([&](DfgVertex& vtx) { vtx.unlinkDelete(*this); });
}
@@ -98,26 +101,31 @@ void DfgGraph::mergeGraphs(std::vector<std::unique_ptr<DfgGraph>>&& otherps) {
}
}
std::string DfgGraph::makeUniqueName(const std::string& prefix, size_t n) {
// Construct the tmpNameStub if we have not done so yet
if (m_tmpNameStub.empty()) {
// Use the hash of the graph name (avoid long names and non-identifiers)
const std::string hash = V3Hash{m_name}.toString();
// We need to keep every variable globally unique, and graph hashed
// names might not be, so keep a static table to track multiplicity
static std::unordered_map<std::string, uint32_t> s_multiplicity;
m_tmpNameStub += '_' + hash + '_' + std::to_string(s_multiplicity[hash]++) + '_';
}
// Assemble the globally unique name
return "__Vdfg" + prefix + m_tmpNameStub + std::to_string(n);
}
DfgVertexVar* DfgGraph::makeNewVar(FileLine* flp, const std::string& name,
DfgVertexVar* DfgGraph::makeNewVar(FileLine* flp, const std::string& prefix,
const DfgDataType& dtype, AstScope* scopep) {
// Create AstVar
AstVar* const varp = new AstVar{flp, VVarType::MODULETEMP, name, dtype.astDtypep()};
// Add AstVar to the scope's module
scopep->modp()->addStmtsp(varp);
// AstVar declarations outlive all DFG graphs. Splitting or merging graphs
// does not transfer slots: each graph creates globally unique declarations.
TempDeclarations& temps = m_temporaries[scopep->modp()][{prefix, dtype.astDtypep()}];
const size_t slot = temps.m_scopeCounts[scopep]++;
AstVar* varp;
if (slot == temps.m_declps.size()) {
// Construct the name stub on the first new declaration in this graph
if (m_tmpNameStub.empty()) {
// Use the hash of the graph name (avoid long names and non-identifiers)
const std::string hash = V3Hash{m_name}.toString();
// Graph hashes may collide, so track multiplicity to keep names globally unique
static std::unordered_map<std::string, uint32_t> s_multiplicity;
m_tmpNameStub += '_' + hash + '_' + std::to_string(s_multiplicity[hash]++) + '_';
}
const std::string varName
= "__Vdfg" + prefix + m_tmpNameStub + std::to_string(m_tmpNameCount++);
varp = new AstVar{flp, VVarType::MODULETEMP, varName, dtype.astDtypep()};
scopep->modp()->addStmtsp(varp);
temps.m_declps.emplace_back(varp);
} else {
varp = temps.m_declps[slot];
++m_tempDeclarationsReused;
}
// Create AstVarScope
AstVarScope* const vscp = new AstVarScope{flp, scopep, varp};
// Add to scope
+20 -8
View File
@@ -46,6 +46,7 @@
#include <algorithm>
#include <array>
#include <functional>
#include <map>
#include <new>
#include <type_traits>
#include <unordered_map>
@@ -429,6 +430,19 @@ class DfgGraph final {
size_t m_size = 0; // Number of vertices in the graph
const std::string m_name; // Name of graph - need not be unique
std::string m_tmpNameStub{""}; // Name stub for temporary variables - computed lazy
size_t m_tmpNameCount = 0; // Sequence number for newly created temporary declarations
// Slots are local to this graph and keyed by module, prefix and type.
// Different prefixes may carry different AstVar attributes, but temporaries
// with the same prefix may share an AstVar, so they must have identical ones.
// Each scope consumes each slot at most once.
struct TempDeclarations final {
std::map<AstScope*, size_t> m_scopeCounts; // Next slot for each instance
std::vector<AstVar*> m_declps; // Declarations indexed by slot
};
std::map<AstNodeModule*, std::map<std::pair<std::string, AstNodeDType*>, TempDeclarations>>
m_temporaries; // Shared slots indexed by module, purpose, and type
uint64_t m_tempDeclarationsReused = 0; // Declarations shared across instance scopes
// The only way to access thes is via DfgUserMap, so mutable is appropriate,
// the map can change while the graph is const.
@@ -527,14 +541,12 @@ public:
// DfgVertexVar instances representing the same Ast variable are unified.
void mergeGraphs(std::vector<std::unique_ptr<DfgGraph>>&& otherps) VL_MT_DISABLED;
// Genarete a unique name. The provided 'prefix' and 'n' values will be part of the name, and
// must be unique (as a pair) in each invocation for this graph.
std::string makeUniqueName(const std::string& prefix, size_t n) VL_MT_DISABLED;
// Create a new variable with the given name and data type. For a Scoped
// Dfg, the AstScope where the corresponding AstVarScope will be inserted
// must be provided
DfgVertexVar* makeNewVar(FileLine*, const std::string& name, const DfgDataType&,
// Create a new scoped variable. Instances of a module share temporary
// declarations of the same prefix and type, but have independent storage.
// Each scope uses a declaration at most once; new declarations get unique names.
// As the AstVar may be shared, callers must set identical AstVar attributes
// on all temporaries created with the same prefix.
DfgVertexVar* makeNewVar(FileLine*, const std::string& prefix, const DfgDataType&,
AstScope*) VL_MT_DISABLED;
// Split this graph into individual components (unique sub-graphs with no edges between them).
+1 -6
View File
@@ -239,12 +239,10 @@ class TraceDriver final : public DfgVisitor {
// Create temporary capable of holding the result of 'vtxp'
DfgVertexVar* createTmp(const char* prefix, DfgVertex* vtxp) {
AstNode* nodep = v3Global.rootp();
const std::string name = m_dfg.makeUniqueName(prefix, nodep->user2Inc());
FileLine* const flp = vtxp->fileline();
DfgVertex::ScopeCache scopeCache;
AstScope* const scopep = vtxp->scopep(scopeCache);
DfgVertexVar* const varp = m_dfg.makeNewVar(flp, name, vtxp->dtype(), scopep);
DfgVertexVar* const varp = m_dfg.makeNewVar(flp, prefix, vtxp->dtype(), scopep);
varp->vscp()->varp()->isInternal(true);
varp->tmpForp(varp->vscp());
m_sccInfo.add(*varp, 0);
@@ -1654,9 +1652,6 @@ void breakCycles(DfgGraph& dfg, V3DfgBreakCyclesContext& ctx) {
if (dumpDfgLevel() >= level) dfg.dumpDotFilePrefixed("breakCycles-" + name);
};
// AstNetlist/AstNodeModule user2 used as sequence numbers for temporaries
const VNUser2InUse user2InUse;
// Show input for debugging
dump(7, dfg, "input");
+3 -10
View File
@@ -344,9 +344,6 @@ void V3DfgPasses::binToOneHot(DfgGraph& dfg, V3DfgBinToOneHotContext& ctx) {
// Fast path exit if we surely don't need to convet anything
if (nTerms < TERM_LIMIT) return;
// Sequence numbers for name generation
size_t nTables = 0;
DfgVertex::ScopeCache scopeCache;
// Create decoders for each srcp
@@ -419,8 +416,7 @@ void V3DfgPasses::binToOneHot(DfgGraph& dfg, V3DfgBinToOneHotContext& ctx) {
// If there is an existing result variable, use that
if (DfgVertexVar* const vp = srcp->getResultVar()) return vp->as<DfgVarPacked>();
// Otherwise create a new variable
const std::string name = dfg.makeUniqueName("BinToOneHot_Idx", nTables);
DfgVertexVar* const vtxp = dfg.makeNewVar(flp, name, idxDType, scopep);
DfgVertexVar* const vtxp = dfg.makeNewVar(flp, "BinToOneHot_Idx", idxDType, scopep);
vtxp->vscp()->varp()->isInternal(true);
vtxp->srcp(srcp);
return vtxp->as<DfgVarPacked>();
@@ -428,8 +424,7 @@ void V3DfgPasses::binToOneHot(DfgGraph& dfg, V3DfgBinToOneHotContext& ctx) {
AstVarScope* const idxVscp = idxVtxp->vscp();
// The previous index variable - we don't need a vertex for this
AstVarScope* const preVscp = [&]() {
const std::string name = dfg.makeUniqueName("BinToOneHot_Pre", nTables);
DfgVertexVar* const vtxp = dfg.makeNewVar(flp, name, idxDType, scopep);
DfgVertexVar* const vtxp = dfg.makeNewVar(flp, "BinToOneHot_Pre", idxDType, scopep);
AstVarScope* const vscp = vtxp->vscp();
VL_DO_DANGLING(vtxp->unlinkDelete(dfg), vtxp);
vscp->varp()->isInternal(true);
@@ -439,15 +434,13 @@ void V3DfgPasses::binToOneHot(DfgGraph& dfg, V3DfgBinToOneHotContext& ctx) {
}();
// The table variable
DfgVarArray* const tabVtxp = [&]() {
const std::string name = dfg.makeUniqueName("BinToOneHot_Tab", nTables);
DfgVertexVar* const varp = dfg.makeNewVar(flp, name, tabDType, scopep);
DfgVertexVar* const varp = dfg.makeNewVar(flp, "BinToOneHot_Tab", tabDType, scopep);
varp->vscp()->varp()->isInternal(true);
varp->vscp()->varp()->noReset(true);
varp->setHasModWrRefs();
return varp->as<DfgVarArray>();
}();
++nTables;
++ctx.m_decodersCreated;
// Initialize 'tab' and 'pre' variables statically
+1 -3
View File
@@ -209,7 +209,6 @@ class V3DfgPeephole final : public DfgVisitor {
DfgVertex* m_vtxp = nullptr; // Currently considered vertex
size_t m_currentGeneration = 0; // Current generation number
size_t m_lastId = 0; // Last unique vertex ID assigned
size_t m_nTemps = 0; // Number of temporary variables created
// Scope for transient temporariy variables cerated in this pass. They should all be
// eliminated wihtin this pass, so anything should be ok, pick the top scope as easy to find.
AstScope* const m_tmpScopep = v3Global.rootp()->topScopep()->scopep();
@@ -2181,9 +2180,8 @@ class V3DfgPeephole final : public DfgVisitor {
DfgSplicePacked* const sp = new DfgSplicePacked{m_dfg, flp, vtxp->dtype()};
m_vInfo[sp].m_id = ++m_lastId;
sp->addDriver(catp, lsb, flp);
const std::string name = m_dfg.makeUniqueName("PeepholeNarrow", m_nTemps++);
DfgVertexVar* const varp
= m_dfg.makeNewVar(flp, name, vtxp->dtype(), m_tmpScopep);
= m_dfg.makeNewVar(flp, "PeepholeNarrow", vtxp->dtype(), m_tmpScopep);
varp->tmpForp(varp->vscp());
m_vInfo[varp].m_id = ++m_lastId;
varp->vscp()->varp()->isInternal(true);
+2 -4
View File
@@ -30,7 +30,6 @@ class DfgRegularize final {
// STATE
DfgGraph& m_dfg; // The graph being processed
V3DfgRegularizeContext& m_ctx; // The optimization context for stats
size_t m_nTmps = 0; // Number of temporaries added to this graph - for variable names only
VNDeleter m_deleter; // Deletes replacement nodes at the end
// METHODS
@@ -208,11 +207,10 @@ class DfgRegularize final {
if (!needsTemporary(*vtxp, *vtxp)) continue;
// Need to create an intermediate variable
++m_ctx.m_temporariesIntroduced;
const std::string name = m_dfg.makeUniqueName("Regularize", m_nTmps);
FileLine* const flp = vtxp->fileline();
AstScope* const scopep = vtxp->scopep(scopeCache);
DfgVertexVar* const newp = m_dfg.makeNewVar(flp, name, vtxp->dtype(), scopep);
++m_nTmps;
DfgVertexVar* const newp
= m_dfg.makeNewVar(flp, "Regularize", vtxp->dtype(), scopep);
// Replace vertex with the variable, make it drive the variable
vtxp->replaceWith(newp);
newp->srcp(vtxp);
+6 -17
View File
@@ -58,8 +58,6 @@ DfgArraySel* makeVertex<DfgArraySel, AstArraySel>(const AstArraySel* nodep, DfgG
class AstToDfgConverter final : public VNVisitor {
// NODE STATE
// AstNodeExpr/AstVar/AstVarScope::user2p -> DfgVertex* for this Node
// AstVar::user3() -> int temporary counter for variable
const VNUser3InUse m_user3InUse;
// STATE
DfgGraph& m_dfg; // The graph being built
@@ -74,8 +72,6 @@ class AstToDfgConverter final : public VNVisitor {
bool m_foundUnhandled = false; // Found node not implemented as DFG or not implemented 'visit'
bool m_converting = false; // We are trying to convert some logic at the moment
size_t m_nUnpack = 0; // Sequence numbers for temporaries
// METHODS
// Allocate a new non-variable vertex, add it to the currently synthesized logic
@@ -273,8 +269,7 @@ class AstToDfgConverter final : public VNVisitor {
// Assigning compound expressions to a concatenated LHS requires a temporary
// to avoid multiple use of the expression
if (VN_IS(lhsp, Concat) && !vtxp->is<DfgVertexVar>() && !vtxp->is<DfgConst>()) {
const size_t n = ++m_nUnpack;
DfgVertexVar* const tmpp = createTmp(*m_logicp, flp, vtxp->dtype(), "Unpack", n);
DfgVertexVar* const tmpp = createTmp(*m_logicp, flp, vtxp->dtype(), "Unpack");
tmpp->srcp(vtxp);
vtxp = tmpp;
}
@@ -496,9 +491,8 @@ public:
// Create temporay variable capable of holding the given type
DfgVertexVar* createTmp(DfgLogic& logic, FileLine* flp, const DfgDataType& dtype,
const std::string& prefix, size_t tmpCount) {
const std::string name = m_dfg.makeUniqueName(prefix, tmpCount);
DfgVertexVar* const vtxp = m_dfg.makeNewVar(flp, name, dtype, logic.scopep());
const std::string& prefix) {
DfgVertexVar* const vtxp = m_dfg.makeNewVar(flp, prefix, dtype, logic.scopep());
logic.synth().emplace_back(vtxp);
vtxp->vscp()->varp()->isInternal(true);
vtxp->tmpForp(vtxp->vscp());
@@ -511,8 +505,7 @@ public:
FileLine* const flp = astVarp->fileline();
const DfgDataType& dtype = *DfgDataType::fromAst(astVarp->dtypep());
const std::string prfx = prefix + "_" + astVarp->name();
const size_t tmpCount = astVarp->user3Inc();
DfgVertexVar* const vtxp = createTmp(logic, flp, dtype, prfx, tmpCount);
DfgVertexVar* const vtxp = createTmp(logic, flp, dtype, prfx);
vtxp->tmpForp(vscp);
return vtxp;
}
@@ -624,8 +617,6 @@ class AstToDfgSynthesize final {
DfgGraph& m_dfg; // The graph being built
V3DfgSynthesisContext& m_ctx; // The context for stats
AstToDfgConverter m_converter; // The convert instance to use for each construct
size_t m_nBranchCond = 0; // Sequence numbers for temporaries
size_t m_nPathPred = 0; // Sequence numbers for temporaries
DfgWorklist m_toRevert{m_dfg}; // We need a worklist for reverting synthesis
// STATE - for current DfgLogic being synthesized
@@ -1539,12 +1530,11 @@ class AstToDfgSynthesize final {
return resp;
}();
size_t n = m_nPathPred++; // Sequence number for temporaries
const DfgDataType& dtype = predp->dtype();
const auto mkTmp = [&](FileLine* flp, const char* name, DfgVertex* srcp) {
const std::string prefix = "_BB" + std::to_string(bb.id()) + "_" + name;
DfgVertexVar* const tmpp = m_converter.createTmp(*m_logicp, flp, dtype, prefix, n);
DfgVertexVar* const tmpp = m_converter.createTmp(*m_logicp, flp, dtype, prefix);
tmpp->srcp(srcp);
return tmpp;
};
@@ -1717,8 +1707,7 @@ class AstToDfgSynthesize final {
FileLine* const flp = condp->fileline();
const DfgDataType& dtype = condp->dtype();
const std::string prefix = "_BB" + std::to_string(bb.id()) + "_Cond";
const size_t n = m_nBranchCond++;
DfgVertexVar* const vp = m_converter.createTmp(*m_logicp, flp, dtype, prefix, n);
DfgVertexVar* const vp = m_converter.createTmp(*m_logicp, flp, dtype, prefix);
vp->srcp(condp);
m_bbToCondp[bb] = vp;
}
+9
View File
@@ -262,6 +262,15 @@ module t (
`signal(ARRAY_DEFAULT, 21);
assign ARRAY_DEFAULT = {array_default[2], array_default[1], array_default[0]};
// Element 0 is partially driven, with bits 5:4 undriven, so its packed splice
// is not coalesced. It sits on the cycle boundary and is traced via a temporary.
wire [7:0] array_splice[2]; // UNOPTFLAT
assign array_splice[0][3:0] = rand_a[3:0];
assign array_splice[0][7:6] = rand_a[7:6];
assign array_splice[1] = {array_splice[0][7:6], 2'd0, array_splice[0][3:0]} + 8'd1;
`signal(ARRAY_SPLICE, 8);
assign ARRAY_SPLICE = array_splice[1];
`signal(ADD_A, 8); // UNOPTFLAT
`signal(ADD_B, 8);
`signal(ADD_C, 8);
+19
View File
@@ -0,0 +1,19 @@
#!/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', 'vltmt')
# Disable V3Gate, which would otherwise inline the temporaries, so the
# self-checks also exercise the shared declarations in the executed model.
test.compile(verilator_flags2=['--binary', '--stats', '-fno-gate'])
test.execute()
test.file_grep(test.stats, r'Optimizations, DFG, temporary declarations reused\s+(\d+)', 36)
test.passes()
+108
View File
@@ -0,0 +1,108 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Michael Taylor
// 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);
// verilog_format: on
module temp_leaf #(
parameter W = 7
) (
input clk_i,
input [W-1:0] a_i,
b_i,
c_i,
d_i,
output [W-1:0] comb0_o,
comb1_o,
output [W-3:0] comb2_o,
output logic [W-1:0] state_o = 0
);
/* verilator no_inline_module */
typedef logic [W-1:0] word_t;
word_t a_r = 0, b_r = 0, c_r = 0, d_r = 0;
always_ff @(posedge clk_i) begin
a_r <= a_i;
b_r <= b_i;
c_r <= c_i;
d_r <= d_i;
end
// Two live intermediates of the same type must occupy distinct slots.
assign comb0_o = ((a_r + b_r) ^ c_r) + ((a_r + b_r) & d_r);
assign comb1_o = ((a_r ^ b_r) + c_r) ^ ((a_r ^ b_r) | d_r);
// A different type in the same module needs a distinct declaration name,
// even when both types allocate the same slot number.
assign comb2_o = ((a_r[W-3:0] + c_r[W-3:0]) ^ b_r[W-3:0])
+ ((a_r[W-3:0] + c_r[W-3:0]) & d_r[W-3:0]);
always_ff @(negedge clk_i) state_o <= (a_r + b_r) ^ (state_o + c_r);
endmodule
module t;
for (genvar n = 0; n < 16; ++n) begin : g
localparam W = (n % 4 == 0) ? 7 : (n % 4 == 1) ? 33 : (n % 4 == 2) ? 65 : 95;
typedef logic [W-1:0] word_t;
typedef logic [W-3:0] narrow_t;
bit clk = 0;
word_t a = 0, b = 0, c = 0, d = 0;
wire [W-1:0] comb0, comb1, state_value;
wire [W-3:0] comb2;
temp_leaf #(
.W(W)
) leaf (
.clk_i(clk),
.a_i(a),
.b_i(b),
.c_i(c),
.d_i(d),
.comb0_o(comb0),
.comb1_o(comb1),
.comb2_o(comb2),
.state_o(state_value)
);
initial begin
automatic word_t expected0 = 0, expected1 = 0, expected_state = 0;
automatic narrow_t expected2 = 0;
word_t sum, xored;
narrow_t narrow_sum;
for (int cycle = 0; cycle < 200; ++cycle) begin
a = W'({$random, $random, $random});
b = W'({$random, $random, $random});
c = W'({$random, $random, $random});
d = W'({$random, $random, $random});
#1;
`checkh(comb0, expected0);
`checkh(comb1, expected1);
`checkh(comb2, expected2);
`checkh(state_value, expected_state);
clk = 1;
sum = a + b;
xored = a ^ b;
expected0 = (sum ^ c) + (sum & d);
expected1 = (xored + c) ^ (xored | d);
narrow_sum = narrow_t'(a) + narrow_t'(c);
expected2 = (narrow_sum ^ narrow_t'(b)) + (narrow_sum & narrow_t'(d));
#1;
`checkh(comb0, expected0);
`checkh(comb1, expected1);
`checkh(comb2, expected2);
`checkh(state_value, expected_state);
clk = 0;
expected_state = sum ^ (expected_state + c);
#1;
`checkh(comb0, expected0);
`checkh(comb1, expected1);
`checkh(comb2, expected2);
`checkh(state_value, expected_state);
end
end
end
initial begin
#601;
$write("*-* All Finished *-*\n");
$finish;
end
endmodule