Add DFG binToOneHot pass to generate one-hot decoders (#6096)

Somewhat commonly, there is code out there that compares an expression (or
variable) against many different constants, e.g. a one-hot decoder:

```systemverilog
  assign oneHot = {x == 3, x == 2, x == 1, x == 0};
```

If the width of the expression is sufficiently large, this can blow up
a GCC pass and take an egregious amount of memory and time to compile.

Adding a new DFG pass that will generate a cheap one-hot decoder:
to compute:

```systemverilog
  wire [$bits(x)-1:0] idx = <the expression being compared many times>
  reg tab [1<<$bits(x)] = '{default: 0};
  reg [$bits(x)-1:0] pre = '0;

  always_comb begin
    tab[pre] = 0;
    tab[idx] = 1;
    pre = idx ; // This assignment marked to avoid a false UNOPFTLAT
  end
```

We then replace the comparisons `x == CONST` with `tab[CONST]`.

This is generally performance neutral, but avoids the compile time and memory
blowup with GCC (128GB+ -> 1GB in one example).

We do not apply this if the comparisons seem to be part of a `COMPARE ?
val : COND` conditional tree, which the C++ compilers can turn into jump
tables.

This enables all XiangShan configurations from RTLMeter to now build with GCC,
so in this patch we enabled those in the nightly runs.
This commit is contained in:
Geza Lore
2025-06-16 23:14:24 +01:00
committed by GitHub
parent de2818c733
commit 277611bcdd
10 changed files with 716 additions and 4 deletions
+244
View File
@@ -25,6 +25,11 @@
VL_DEFINE_DEBUG_FUNCTIONS;
V3DfgBinToOneHotContext::~V3DfgBinToOneHotContext() {
V3Stats::addStat("Optimizations, DFG " + m_label + " BinToOneHot, decoders created",
m_decodersCreated);
}
V3DfgCseContext::~V3DfgCseContext() {
V3Stats::addStat("Optimizations, DFG " + m_label + " CSE, expressions eliminated",
m_eliminated);
@@ -226,6 +231,244 @@ void V3DfgPasses::removeUnused(DfgGraph& dfg) {
}
}
void V3DfgPasses::binToOneHot(DfgGraph& dfg, V3DfgBinToOneHotContext& ctx) {
const auto userDataInUse = dfg.userDataInUse();
// Structure to keep track of comparison details
struct Term final {
DfgVertex* m_vtxp; // Vertex to replace
bool m_inv; // '!=', instead of '=='
Term() = default;
Term(DfgVertex* vtxp, bool inv)
: m_vtxp{vtxp}
, m_inv{inv} {}
};
// Map from 'value beign compared' -> 'terms', stored in DfgVertex::user()
using Val2Terms = std::map<uint32_t, std::vector<Term>>;
// Allocator for Val2Terms, so it's cleaned up on return
std::deque<Val2Terms> val2TermsAllocator;
// List of vertices that are used as sources
std::vector<DfgVertex*> srcps;
// Only consider input variables from a reasonable range:
// - not too big to avoid huge tables, you are doomed anyway at that point..
// - not too small, as it's probably not worth it
constexpr uint32_t WIDTH_MIN = 7;
constexpr uint32_t WIDTH_MAX = 20;
const auto widthOk = [](const DfgVertex* vtxp) {
const uint32_t width = vtxp->width();
return WIDTH_MIN <= width && width <= WIDTH_MAX;
};
// Do not convert terms that look like they are in a Cond tree
// the C++ compiler can generate jump tables for these
const std::function<bool(const DfgVertex*, bool)> useOk
= [&](const DfgVertex* vtxp, bool inv) -> bool {
// Go past a single 'Not' sink, which is common
if (DfgVertex* const sinkp = vtxp->singleSink()) {
if (sinkp->is<DfgNot>()) return useOk(sinkp, !inv);
}
return !vtxp->findSink<DfgCond>([vtxp, inv](const DfgCond& sink) {
if (sink.condp() != vtxp) return false;
return inv ? sink.thenp()->is<DfgCond>() : sink.elsep()->is<DfgCond>();
});
};
// Look at all comparison nodes and build the 'Val2Terms' map for each source vertex
uint32_t nTerms = 0;
for (DfgVertex& vtx : dfg.opVertices()) {
DfgVertex* srcp = nullptr;
uint32_t val = 0;
bool inv = false;
if (DfgEq* const eqp = vtx.cast<DfgEq>()) {
DfgConst* const constp = eqp->lhsp()->cast<DfgConst>();
if (!constp || !widthOk(constp) || !useOk(eqp, false)) continue;
srcp = eqp->rhsp();
val = constp->toU32();
inv = false;
} else if (DfgNeq* const neqp = vtx.cast<DfgNeq>()) {
DfgConst* const constp = neqp->lhsp()->cast<DfgConst>();
if (!constp || !widthOk(constp) || !useOk(neqp, true)) continue;
srcp = neqp->rhsp();
val = constp->toU32();
inv = true;
} else if (DfgRedAnd* const redAndp = vtx.cast<DfgRedAnd>()) {
srcp = redAndp->srcp();
if (!widthOk(srcp) || !useOk(redAndp, false)) continue;
val = (1U << srcp->width()) - 1;
inv = false;
} else if (DfgRedOr* const redOrp = vtx.cast<DfgRedOr>()) {
srcp = redOrp->srcp();
if (!widthOk(srcp) || !useOk(redOrp, true)) continue;
val = 0;
inv = true;
} else {
// Not a comparison-like vertex
continue;
}
// Grab the Val2Terms entry
Val2Terms*& val2Termspr = srcp->user<Val2Terms*>();
if (!val2Termspr) {
// Remeber and allocate on first encounter
srcps.emplace_back(srcp);
val2TermsAllocator.emplace_back();
val2Termspr = &val2TermsAllocator.back();
}
// Record term
(*val2Termspr)[val].emplace_back(&vtx, inv);
++nTerms;
}
// Somewhat arbitrarily, only apply if more than 64 unique comparisons are required
constexpr uint32_t TERM_LIMIT = 65;
// This should hold, otherwise we do redundant work gathering terms that will never be used
static_assert((1U << WIDTH_MIN) >= TERM_LIMIT, "TERM_LIMIT too big relative to 2**WIDTH_MIN");
// 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;
// Create decoders for each srcp
for (DfgVertex* const srcp : srcps) {
const Val2Terms& val2Terms = *srcp->getUser<Val2Terms*>();
// If not enough terms in this vertex, ignore
if (val2Terms.size() < TERM_LIMIT) continue;
// Width of the decoded binary value
const uint32_t width = srcp->width();
// Number of bits in the input operand
const uint32_t nBits = 1U << width;
// Construct the decoder by converting many "const == vtx" by:
// - Adding a single decoder block, where 'tab' is zero initialized:
// always_comb begin
// tab[pre] = 0;
// tab[vtx] = 1;
// pre = vtx;
// end
// We mark 'pre' so the write is ignored during scheduling, so this
// won't cause a combinational cycle.
// Note that albeit this looks like partial udpates to 'tab', the
// actual result is that only one value in 'tab' is ever one, while
// all the others are always zero.
// - and replace the comparisons with 'tab[const]'
FileLine* const flp = srcp->fileline();
// Required data types
AstNodeDType* const idxDTypep = srcp->dtypep();
AstNodeDType* const bitDTypep = DfgVertex::dtypeForWidth(1);
AstUnpackArrayDType* const tabDTypep = new AstUnpackArrayDType{
flp, bitDTypep, new AstRange{flp, static_cast<int>(nBits - 1), 0}};
v3Global.rootp()->typeTablep()->addTypesp(tabDTypep);
// The index variable
DfgVarPacked* const idxVtxp = [&]() {
// If there is an existing result variable, use that, otherwise create a new variable
DfgVarPacked* varp = srcp->getResultVar();
if (!varp) {
const std::string name = dfg.makeUniqueName("BinToOneHot_Idx", nTables);
varp = dfg.makeNewVar(flp, name, idxDTypep)->as<DfgVarPacked>();
varp->varp()->isInternal(true);
}
varp->setHasModRefs();
return varp;
}();
// The previous index variable - we don't need a vertex for this
AstVar* const preVarp = [&]() {
const std::string name = dfg.makeUniqueName("BinToOneHot_Pre", nTables);
AstVar* const varp = new AstVar{flp, VVarType::MODULETEMP, name, idxDTypep};
dfg.modulep()->addStmtsp(varp);
varp->isInternal(true);
varp->noReset(true);
varp->setIgnoreSchedWrite();
return varp;
}();
// The table variable
DfgVarArray* const tabVtxp = [&]() {
const std::string name = dfg.makeUniqueName("BinToOneHot_Tab", nTables);
DfgVarArray* const varp = dfg.makeNewVar(flp, name, tabDTypep)->as<DfgVarArray>();
varp->varp()->isInternal(true);
varp->varp()->noReset(true);
varp->setHasModRefs();
return varp;
}();
++nTables;
++ctx.m_decodersCreated;
// Initialize 'tab' and 'pre' variables statically
AstInitialStatic* const initp = new AstInitialStatic{flp, nullptr};
dfg.modulep()->addStmtsp(initp);
{ // pre = 0
initp->addStmtsp(new AstAssign{
flp, //
new AstVarRef{flp, preVarp, VAccess::WRITE}, //
new AstConst{flp, AstConst::WidthedValue{}, static_cast<int>(width), 0}});
}
{ // tab.fill(0)
AstCMethodHard* const callp = new AstCMethodHard{
flp, new AstVarRef{flp, tabVtxp->varp(), VAccess::WRITE}, "fill"};
callp->addPinsp(new AstConst{flp, AstConst::BitFalse{}});
callp->dtypeSetVoid();
initp->addStmtsp(callp->makeStmt());
}
// Build the decoder logic
AstAlways* const logicp = new AstAlways{flp, VAlwaysKwd::ALWAYS_COMB, nullptr, nullptr};
dfg.modulep()->addStmtsp(logicp);
{ // tab[pre] = 0;
logicp->addStmtsp(new AstAssign{
flp, //
new AstArraySel{flp, new AstVarRef{flp, tabVtxp->varp(), VAccess::WRITE},
new AstVarRef{flp, preVarp, VAccess::READ}}, //
new AstConst{flp, AstConst::BitFalse{}}});
}
{ // tab[idx] = 1
logicp->addStmtsp(new AstAssign{
flp, //
new AstArraySel{flp, new AstVarRef{flp, tabVtxp->varp(), VAccess::WRITE},
new AstVarRef{flp, idxVtxp->varp(), VAccess::READ}}, //
new AstConst{flp, AstConst::BitTrue{}}});
}
{ // pre = idx
logicp->addStmtsp(new AstAssign{flp, //
new AstVarRef{flp, preVarp, VAccess::WRITE}, //
new AstVarRef{flp, idxVtxp->varp(), VAccess::READ}});
}
// Replace terms with ArraySels
for (const auto& pair : val2Terms) {
const uint32_t val = pair.first;
const std::vector<Term>& terms = pair.second;
// Create the ArraySel
FileLine* const flp = terms.front().m_vtxp->fileline();
DfgArraySel* const aselp = new DfgArraySel{dfg, flp, bitDTypep};
aselp->fromp(tabVtxp);
aselp->bitp(new DfgConst{dfg, flp, width, val});
// The inverted value, if needed
DfgNot* notp = nullptr;
// Repalce the terms
for (const Term& term : terms) {
if (term.m_inv) {
if (!notp) {
notp = new DfgNot{dfg, flp, bitDTypep};
notp->srcp(aselp);
}
term.m_vtxp->replaceWith(notp);
} else {
term.m_vtxp->replaceWith(aselp);
}
VL_DO_DANGLING(term.m_vtxp->unlinkDelete(dfg), term.m_vtxp);
}
}
}
}
void V3DfgPasses::eliminateVars(DfgGraph& dfg, V3DfgEliminateVarsContext& ctx) {
const auto userDataInUse = dfg.userDataInUse();
@@ -355,6 +598,7 @@ void V3DfgPasses::optimize(DfgGraph& dfg, V3DfgOptimizationContext& ctx) {
apply(3, "input ", [&]() {});
apply(4, "inlineVars ", [&]() { inlineVars(dfg); });
apply(4, "cse0 ", [&]() { cse(dfg, ctx.m_cseContext0); });
apply(4, "binToOneHot ", [&]() { binToOneHot(dfg, ctx.m_binToOneHotContext); });
if (v3Global.opt.fDfgPeephole()) {
apply(4, "peephole ", [&]() { peephole(dfg, ctx.m_peepholeContext); });
// We just did CSE above, so without peephole there is no need to run it again these