From 6c2cfb31e9b41dcbb34038255ac7af4afee57562 Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sat, 20 Jun 2026 12:01:24 -0400 Subject: [PATCH] Lower bit-scan loop idioms to $mostsetbitp1 / $countones Recognize the common single-bit scan loop idioms in V3Unroll (before it unrolls) and lower them to bit-reduction primitives, replacing a literal W-iteration loop with one intrinsic-backed expression: target=0; for (i=0;i $mostsetbitp1(vec) target=0; for (i=0;i $countones(vec) The leading-one form lowers to a new AstMostSetBitP1 node, emitted as VL_MOSTSETBITP1_{I,Q,W}; those runtime helpers now use __builtin_clz where available (same pattern as VL_REDXOR's __builtin_parity), with the existing bit scan as fallback. The count-ones form reuses AstCountOnes ($countones, popcount); as the DFG requires a 32-bit countones result it is built at 32 bits and narrowed to the accumulator width with a select. Matching is structural to stay sound: the index must start at 0, increment by exactly 1, and scan all W==width(vec) bits via a single 1-bit select of a distinct vector, with the target pre-zeroed and no else branch. The loop bound is accepted as a strict ascending 'idx < W' written either way and signed or unsigned (Gt/GtS/Lt/LtS). Gated by -fbit-scan-loops (on at -O). Adds t_bit_scan_loops (I/Q/W, count-ones and unsigned-index positives; step-2, start-1, idx*2+1, vec[idx+1], target=idx and W!=width negatives, all self-checked and asserted via --stats not to lower) plus t_bit_scan_loops_off for the disable flag. Motivated by a transformer inference design whose 80-bit leading-one detector ran every cycle (~37% of runtime); the lowering is worth ~39% there. --- docs/CONTRIBUTORS | 1 + docs/guide/exe_verilator.rst | 5 + include/verilated_funcs.h | 29 ++++- src/V3AstNodeExpr.h | 17 +++ src/V3Number.cpp | 14 +++ src/V3Number.h | 1 + src/V3Options.cpp | 2 + src/V3Options.h | 2 + src/V3Unroll.cpp | 151 +++++++++++++++++++++++++ test_regress/t/t_bit_scan_loops.py | 29 +++++ test_regress/t/t_bit_scan_loops.v | 79 +++++++++++++ test_regress/t/t_bit_scan_loops_off.py | 25 ++++ 12 files changed, 349 insertions(+), 6 deletions(-) create mode 100755 test_regress/t/t_bit_scan_loops.py create mode 100644 test_regress/t/t_bit_scan_loops.v create mode 100755 test_regress/t/t_bit_scan_loops_off.py diff --git a/docs/CONTRIBUTORS b/docs/CONTRIBUTORS index 877011165..0fc960dd0 100644 --- a/docs/CONTRIBUTORS +++ b/docs/CONTRIBUTORS @@ -272,6 +272,7 @@ Teng Huang Thomas Aldrian Thomas Brown Thomas Dybdahl Ahle +Thomas Santerre Tim Hutt Tim Snyder Tobias Jensen diff --git a/docs/guide/exe_verilator.rst b/docs/guide/exe_verilator.rst index 4e302f06d..c428f17f3 100644 --- a/docs/guide/exe_verilator.rst +++ b/docs/guide/exe_verilator.rst @@ -658,6 +658,11 @@ Summary: .. option:: -fno-assemble +.. option:: -fno-bit-scan-loops + + Rarely needed. Disable converting leading-one and count-ones loops into + bit-reduction operations. + .. option:: -fno-case Rarely needed. Disable all case statement optimizations. diff --git a/include/verilated_funcs.h b/include/verilated_funcs.h index d69bca256..682899f4c 100644 --- a/include/verilated_funcs.h +++ b/include/verilated_funcs.h @@ -903,15 +903,32 @@ static inline IData VL_CLOG2_W(int words, WDataInP const lwp) VL_PURE { return 0; } +static inline IData VL_MOSTSETBITP1_I(IData lhs) VL_PURE { + if (VL_UNLIKELY(!lhs)) return 0; // __builtin_clz is undefined for 0 +#if defined(__GNUC__) && (__GNUC__ >= 4) && !defined(VL_NO_BUILTINS) + return VL_EDATASIZE - __builtin_clz(lhs); +#else + for (int bit = VL_EDATASIZE - 1; bit >= 0; --bit) { + if (VL_BITISSET_E(lhs, bit)) return bit + 1; + } + return 0; // LCOV_EXCL_LINE // Can't get here - one bit must be set +#endif +} +static inline IData VL_MOSTSETBITP1_Q(QData lhs) VL_PURE { + if (VL_UNLIKELY(!lhs)) return 0; +#if defined(__GNUC__) && (__GNUC__ >= 4) && !defined(VL_NO_BUILTINS) + return 64 - __builtin_clzll(static_cast(lhs)); +#else + const IData hi = static_cast(lhs >> 32ULL); + return hi ? (VL_EDATASIZE + VL_MOSTSETBITP1_I(hi)) + : VL_MOSTSETBITP1_I(static_cast(lhs)); +#endif +} static inline IData VL_MOSTSETBITP1_W(int words, WDataInP const lwp) VL_PURE { // MSB set bit plus one; similar to FLS. 0=value is zero for (int i = words - 1; i >= 0; --i) { - if (VL_UNLIKELY(lwp[i])) { // Shorter worst case if predict not taken - for (int bit = VL_EDATASIZE - 1; bit >= 0; --bit) { - if (VL_UNLIKELY(VL_BITISSET_E(lwp[i], bit))) return i * VL_EDATASIZE + bit + 1; - } - // Can't get here - one bit must be set - } + // Shorter worst case if predict not taken + if (VL_UNLIKELY(lwp[i])) return i * VL_EDATASIZE + VL_MOSTSETBITP1_I(lwp[i]); } return 0; } diff --git a/src/V3AstNodeExpr.h b/src/V3AstNodeExpr.h index 91d689227..392aee936 100644 --- a/src/V3AstNodeExpr.h +++ b/src/V3AstNodeExpr.h @@ -5734,6 +5734,23 @@ public: void dump(std::ostream& str) const override; void dumpJson(std::ostream& str) const override; }; +class AstMostSetBitP1 final : public AstNodeUniop { + // Most-significant set bit plus one (bit-width); 0 if value is zero +public: + AstMostSetBitP1(FileLine* fl, AstNodeExpr* lhsp) + : ASTGEN_SUPER_MostSetBitP1(fl, lhsp) { + dtypeSetInteger2State(); + } + ASTGEN_MEMBERS_AstMostSetBitP1; + void numberOperate(V3Number& out, const V3Number& lhs) override { out.opMostSetBitP1(lhs); } + string emitVerilog() override { return "%f$mostsetbitp1(%l)"; } + string emitC() override { return "VL_MOSTSETBITP1_%lq(%lW, %P, %li)"; } + bool cleanOut() const override { return false; } + bool cleanLhs() const override { return true; } + bool sizeMattersLhs() const override { return false; } + int instrCount() const override { return widthInstrs() * 16; } + bool isSystemFunc() const override { return true; } +}; class AstNToI final : public AstNodeUniop { // String to any-size integral public: diff --git a/src/V3Number.cpp b/src/V3Number.cpp index 58bb2740b..8f64d9c6c 100644 --- a/src/V3Number.cpp +++ b/src/V3Number.cpp @@ -1456,6 +1456,20 @@ V3Number& V3Number::opCLog2(const V3Number& lhs) { setZero(); return *this; } +V3Number& V3Number::opMostSetBitP1(const V3Number& lhs) { + // Most-significant set bit plus one (bit-width / find-last-set); 0 if value is zero + NUM_ASSERT_OP_ARGS1(lhs); + NUM_ASSERT_LOGIC_ARGS1(lhs); + if (lhs.isFourState()) return setAllBitsX(); + for (int bit = lhs.width() - 1; bit >= 0; bit--) { + if (lhs.bitIs1(bit)) { + setLong(bit + 1); + return *this; + } + } + setZero(); + return *this; +} V3Number& V3Number::opLogNot(const V3Number& lhs) { NUM_ASSERT_OP_ARGS1(lhs); diff --git a/src/V3Number.h b/src/V3Number.h index df726e496..8288caf8e 100644 --- a/src/V3Number.h +++ b/src/V3Number.h @@ -760,6 +760,7 @@ public: V3Number& opOneHot(const V3Number& lhs); V3Number& opOneHot0(const V3Number& lhs); V3Number& opCLog2(const V3Number& lhs); + V3Number& opMostSetBitP1(const V3Number& lhs); V3Number& opClean(const V3Number& lhs, uint32_t bits); V3Number& opConcat(const V3Number& lhs, const V3Number& rhs); V3Number& opLenN(const V3Number& lhs); diff --git a/src/V3Options.cpp b/src/V3Options.cpp index 22f96ca7e..179abd1e5 100644 --- a/src/V3Options.cpp +++ b/src/V3Options.cpp @@ -1448,6 +1448,7 @@ void V3Options::parseOptsList(FileLine* fl, const string& optdir, int argc, DECL_OPTION("-facyc-simp", FOnOff, &m_fAcycSimp); DECL_OPTION("-fassemble", FOnOff, &m_fAssemble); + DECL_OPTION("-fbit-scan-loops", FOnOff, &m_fBitScanLoops); DECL_OPTION("-fcase", CbFOnOff, [this](bool flag) { m_fCaseDecoder = flag; m_fCaseTable = flag; @@ -2358,6 +2359,7 @@ void V3Options::optimize(int level) { const bool flag = level > 0; m_fAcycSimp = flag; m_fAssemble = flag; + m_fBitScanLoops = flag; m_fCaseDecoder = flag; m_fCaseTable = flag; m_fCaseTree = flag; diff --git a/src/V3Options.h b/src/V3Options.h index 71a32c31b..88b1359ba 100644 --- a/src/V3Options.h +++ b/src/V3Options.h @@ -392,6 +392,7 @@ private: // MEMBERS (optimizations) bool m_fAcycSimp; // main switch: -fno-acyc-simp: acyclic pre-optimizations bool m_fAssemble; // main switch: -fno-assemble: assign assemble + bool m_fBitScanLoops; // main switch: -fno-bit-scan-loops: lower priority-encoder loops to CLZ bool m_fCaseDecoder; // main switch: -fno-case-decoder: case decoder conversion bool m_fCaseTable; // main switch: -fno-case-table: case table conversion bool m_fCaseTree; // main switch: -fno-case-tree: case tree conversion @@ -727,6 +728,7 @@ public: // ACCESSORS (optimization options) bool fAcycSimp() const { return m_fAcycSimp; } bool fAssemble() const { return m_fAssemble; } + bool fBitScanLoops() const { return m_fBitScanLoops; } bool fCaseDecoder() const { return m_fCaseDecoder; } bool fCaseTable() const { return m_fCaseTable; } bool fCaseTree() const { return m_fCaseTree; } diff --git a/src/V3Unroll.cpp b/src/V3Unroll.cpp index ce677324b..2707af6ce 100644 --- a/src/V3Unroll.cpp +++ b/src/V3Unroll.cpp @@ -62,6 +62,8 @@ struct UnrollStats final { Stat m_nPragmaDisabled{"Pragma unroll_disable"}; Stat m_nUnrolledLoops{"Unrolled loops"}; Stat m_nUnrolledIters{"Unrolled iterations"}; + Stat m_bitScanLowered{"Lowered priority-encoder to mostsetbitp1"}; + Stat m_countOnesLowered{"Lowered count-set-bits to countones"}; }; //###################################################################### @@ -420,6 +422,152 @@ class UnrollAllVisitor final : VNVisitor { UnrollStats m_stats; // Statistic tracking UnrolllBindings m_bindings; // Variable bindings + // METHODS + // Peel value-preserving width casts/truncations (Extend/ExtendS, or a low-bits Sel + // with constant lsb 0) to reach the underlying VarRef; nullptr for anything else + // (e.g. an arithmetic offset like idx+1 or idx*2). + static AstVarRef* unwrapToVarRef(AstNodeExpr* nodep) { + while (nodep) { + if (AstVarRef* const refp = VN_CAST(nodep, VarRef)) return refp; + if (AstExtend* const ep = VN_CAST(nodep, Extend)) { + nodep = ep->lhsp(); + } else if (AstExtendS* const ep = VN_CAST(nodep, ExtendS)) { + nodep = ep->lhsp(); + } else if (AstSel* const sp = VN_CAST(nodep, Sel)) { + const AstConst* const lsbp = VN_CAST(sp->lsbp(), Const); + if (!lsbp || lsbp->toUInt() != 0) return nullptr; + nodep = sp->fromp(); + } else { + return nullptr; + } + } + return nullptr; + } + // True if 'nodep' is exactly 'var + 1' (commutative), where the var operand is the + // given variable reached through value-preserving casts only. + bool isVarPlus1(AstNode* nodep, const AstVarScope* vscp) { + AstAdd* const addp = VN_CAST(nodep, Add); + if (!addp) return false; + const auto isOne = [](AstNodeExpr* p) -> bool { + const AstConst* const cp = VN_CAST(p, Const); + return cp && !cp->num().isFourState() && cp->toUInt() == 1; + }; + const AstVarRef* const l = unwrapToVarRef(addp->lhsp()); + const AstVarRef* const r = unwrapToVarRef(addp->rhsp()); + if (isOne(addp->rhsp()) && l && l->varScopep() == vscp) return true; + if (isOne(addp->lhsp()) && r && r->varScopep() == vscp) return true; + return false; + } + // Match a strict ascending loop bound 'idx < W', written either way and signed or + // unsigned: (W > idx) [Gt/GtS] or (idx < W) [Lt/LtS]. Sets wp and idxRefp. + static bool ascendingBound(AstNodeExpr* condp, AstConst*& wp, AstVarRef*& idxRefp) { + if (VN_IS(condp, Gt) || VN_IS(condp, GtS)) { + AstNodeBiop* const bp = VN_AS(condp, NodeBiop); + wp = VN_CAST(bp->lhsp(), Const); + idxRefp = VN_CAST(bp->rhsp(), VarRef); + } else if (VN_IS(condp, Lt) || VN_IS(condp, LtS)) { + AstNodeBiop* const bp = VN_AS(condp, NodeBiop); + idxRefp = VN_CAST(bp->lhsp(), VarRef); + wp = VN_CAST(bp->rhsp(), Const); + } else { + return false; + } + return wp && idxRefp && !wp->num().isFourState(); + } + // Recognize a single-bit scan loop over all W bits of 'vec' (idx 0..W-1, target + // pre-zeroed) and lower it to a bit-reduction primitive. Two idioms are matched: + // target = 0; idx = 0; + // loop { looptest(W > idx); if (...vec[idx]...) target = ; idx = idx + 1; } + // where, when W == width(vec): + // = idx + 1 => target = $mostsetbitp1(vec) (leading-one / bit-width) + // = target + 1 => target = $countones(vec) (population count) + bool tryLowerBitScanLoop(AstLoop* loopp) { + // Body must be exactly: LoopTest, If, Assign(increment) + AstLoopTest* const testp = VN_CAST(loopp->stmtsp(), LoopTest); + if (!testp) return false; + AstIf* const ifp = VN_CAST(testp->nextp(), If); + if (!ifp) return false; + AstAssign* const incp = VN_CAST(ifp->nextp(), Assign); + if (!incp || incp->nextp()) return false; + // Loop test: a strict ascending bound 'idx < W' (W > idx), signed or unsigned + AstConst* wp = nullptr; + AstVarRef* idxRefp = nullptr; + if (!ascendingBound(testp->condp(), wp, idxRefp)) return false; + AstVarScope* const idxVscp = idxRefp->varScopep(); + const uint32_t width = wp->toUInt(); + // Index must start at 0 (so it takes exactly the values 0..W-1) + const AstConst* const idxInitp = m_bindings.get(idxVscp); + if (!idxInitp || idxInitp->num().isFourState() || idxInitp->toUInt() != 0) return false; + // Increment must be exactly: idx = idx + 1 + AstVarRef* const incLhsp = VN_CAST(incp->lhsp(), VarRef); + if (!incLhsp || incLhsp->varScopep() != idxVscp) return false; + if (!isVarPlus1(incp->rhsp(), idxVscp)) return false; + // If: no else, and 'then' is exactly one assignment 'target = ' + if (ifp->elsesp()) return false; + AstAssign* const thenp = VN_CAST(ifp->thensp(), Assign); + if (!thenp || thenp->nextp()) return false; + AstVarRef* const targetRefp = VN_CAST(thenp->lhsp(), VarRef); + if (!targetRefp) return false; + AstVarScope* const targetVscp = targetRefp->varScopep(); + if (targetVscp == idxVscp) return false; + const bool isLeadingOne = isVarPlus1(thenp->rhsp(), idxVscp); + const bool isCountOnes = !isLeadingOne && isVarPlus1(thenp->rhsp(), targetVscp); + if (!isLeadingOne && !isCountOnes) return false; + // $countones yields a 32-bit result; only fold when the accumulator fits in 32 bits. + if (isCountOnes && targetRefp->width() > 32) return false; + // If-cond must select exactly bit 'idx' of some vector 'vec' (vec != idx/target) + AstNodeExpr* vecExprp = nullptr; + ifp->condp()->foreach([&](AstSel* selp) { + if (vecExprp || selp->width() != 1) return; + const AstVarRef* const fromp = VN_CAST(selp->fromp(), VarRef); + if (!fromp) return; + const AstVarScope* const fromVscp = fromp->varScopep(); + if (fromVscp == idxVscp || fromVscp == targetVscp) return; + const AstVarRef* const idxInSel = unwrapToVarRef(selp->lsbp()); + if (idxInSel && idxInSel->varScopep() == idxVscp) vecExprp = selp->fromp(); + }); + if (!vecExprp) return false; + // The loop must scan exactly all bits of 'vec' + if (static_cast(width) != vecExprp->width()) return false; + // 'target' must be const-0 immediately before the loop (collected in m_bindings), + // so that an all-zero 'vec' yields 0, matching $mostsetbitp1's definition. + const AstConst* const targetInitp = m_bindings.get(targetVscp); + if (!targetInitp || targetInitp->num().isFourState() || targetInitp->toUInt() != 0) { + return false; + } + // Lower the loop to: target = (vec); idx = W; + // The 'idx = W' store preserves the counted loop's exit value, so the rewrite is + // sound even if idx is read after the loop (dead-code elimination drops it if not). + FileLine* const flp = loopp->fileline(); + AstNodeExpr* reducep; + if (isLeadingOne) { + AstMostSetBitP1* const msbp = new AstMostSetBitP1{flp, vecExprp->cloneTree(false)}; + msbp->dtypeFrom(targetRefp); + reducep = msbp; + } else { + // $countones must yield a 32-bit result (DFG invariant); narrow afterwards to + // the accumulator width to match the loop's wrap-around behaviour. + AstCountOnes* const conep = new AstCountOnes{flp, vecExprp->cloneTree(false)}; + conep->dtypeSetInteger2State(); + reducep + = (targetRefp->width() < 32) + ? static_cast(new AstSel{flp, conep, 0, targetRefp->width()}) + : static_cast(conep); + } + AstAssign* const newp = new AstAssign{flp, targetRefp->cloneTree(false), reducep}; + newp->addNext(new AstAssign{flp, incLhsp->cloneTree(false), wp->cloneTree(false)}); + loopp->replaceWith(newp); + VL_DO_DANGLING(pushDeletep(loopp), loopp); + if (isLeadingOne) { + UINFO(4, "Lowered priority-encoder loop to $mostsetbitp1: " << newp); + ++m_stats.m_bitScanLowered; + } else { + UINFO(4, "Lowered count-set-bits loop to $countones: " << newp); + ++m_stats.m_countOnesLowered; + } + return true; + } + // VISIT void visit(AstLoop* nodep) override { // Gather variable bindings from the preceding statements @@ -448,6 +596,9 @@ class UnrollAllVisitor final : VNVisitor { m_bindings.set(lhsp->varScopep(), valp); } + // Recognize a leading-one / priority-encoder loop and lower it to $mostsetbitp1 + if (v3Global.opt.fBitScanLoops() && tryLowerBitScanLoop(nodep)) return; + // Attempt to unroll this loop const std::pair pair = UnrollOneVisitor::apply(m_stats, m_bindings, nodep); diff --git a/test_regress/t/t_bit_scan_loops.py b/test_regress/t/t_bit_scan_loops.py new file mode 100755 index 000000000..f6ef9fd12 --- /dev/null +++ b/test_regress/t/t_bit_scan_loops.py @@ -0,0 +1,29 @@ +#!/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: 2024 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--stats']) + +# Exactly 4 loops must lower to $mostsetbitp1: the 3 canonical I/Q/W leading-one +# loops plus the unsigned-index one. The 6 negative loops (incl. W != width) must +# be left alone -- a wrong lowering would push the count above 4. +test.file_grep(test.stats, + r'Optimizations, Loop unrolling, Lowered priority-encoder to mostsetbitp1\s+(\d+)', + 4) +# And the one count-ones loop must lower to $countones. +test.file_grep(test.stats, + r'Optimizations, Loop unrolling, Lowered count-set-bits to countones\s+(\d+)', + 1) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_bit_scan_loops.v b/test_regress/t/t_bit_scan_loops.v new file mode 100644 index 000000000..2fc56f7fd --- /dev/null +++ b/test_regress/t/t_bit_scan_loops.v @@ -0,0 +1,79 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Exercises the priority-encoder / leading-one ("bit-width", find-last-set) loop +// idiom that V3Unroll lowers to $mostsetbitp1 (CLZ). +// +// Positive cases (MUST lower): canonical for(b=0;b64b) runtime paths. +// Negative cases (MUST NOT lower): non-canonical scans whose result differs +// from $mostsetbitp1(vec). These both (a) self-check their own values and +// (b) are counted by the .py via --stats so a wrong lowering is caught. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2024 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + + // ---- positive: should lower (I / Q / W paths) ---- + logic [31:0] p32; logic [5:0] n32; + logic [47:0] p48; logic [6:0] n48; + logic [79:0] p80; logic [6:0] n80; + logic [31:0] pc; logic [5:0] nc; // population count (count-ones) -> $countones + logic [31:0] pu; logic [5:0] nu; // unsigned loop var (still $mostsetbitp1) + always_comb begin n32 = 0; for (int b = 0; b < 32; b++) if (p32[b]) n32 = 6'(b + 1); end + always_comb begin n48 = 0; for (int b = 0; b < 48; b++) if (p48[b]) n48 = 7'(b + 1); end + always_comb begin n80 = 0; for (int b = 0; b < 80; b++) if (p80[b]) n80 = 7'(b + 1); end + always_comb begin nc = 0; for (int b = 0; b < 32; b++) if (pc[b]) nc = nc + 1; end + always_comb begin nu = 0; for (int unsigned b = 0; b < 32; b++) if (pu[b]) nu = 6'(b + 1); end + + // ---- negative: must NOT lower (each scans a subset / computes a different value) ---- + logic [31:0] vn; + logic [5:0] e_step2; // step 2 (even bits only) + logic [6:0] e_start1; // starts at 1 (skips bit 0) + logic [6:0] e_mul; // target = idx*2 + 1 + logic [5:0] e_off; // selects vec[idx+1] + logic [5:0] e_noP1; // target = idx (no +1) + logic [31:0] vw; // separate vec with a set bit above the scan bound + logic [5:0] e_narrow; // bound W=16 != width(vec)=32 (scans only a prefix) + always_comb begin e_step2 = 0; for (int b = 0; b < 32; b += 2) if (vn[b]) e_step2 = 6'(b + 1); end + always_comb begin e_start1 = 0; for (int b = 1; b < 32; b++) if (vn[b]) e_start1 = 7'(b + 1); end + always_comb begin e_mul = 0; for (int b = 0; b < 32; b++) if (vn[b]) e_mul = 7'(2 * b + 1); end + always_comb begin e_off = 0; for (int b = 0; b < 31; b++) if (vn[b + 1]) e_off = 6'(b + 1); end + always_comb begin e_noP1 = 0; for (int b = 0; b < 32; b++) if (vn[b]) e_noP1 = 6'(b); end + always_comb begin e_narrow = 0; for (int b = 0; b < 16; b++) if (vw[b]) e_narrow = 6'(b + 1); end + + integer cyc = 0; + integer fails = 0; + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 0) begin + p32 <= 32'h8000_0000; // MSB -> 32 + p48 <= 48'h0; p48[47] <= 1'b1; // MSB -> 48 (Q path) + p80 <= 80'h0; p80[79] <= 1'b1; // MSB -> 80 (W path) + pc <= 32'hf0f0_f0f0; // 16 set bits + pu <= 32'h0001_0000; // bit 16 -> 17 + vn <= 32'h0000_00b4; // bits {2,4,5,7} set + vw <= 32'h0010_0008; // bits {3,20}: prefix-scan(16)->4, full->21 + end + else if (cyc == 1) begin + if (n32 !== 6'd32) fails = fails + 1; + if (n48 !== 7'd48) fails = fails + 1; + if (n80 !== 7'd80) fails = fails + 1; + if (nc !== 6'd16) fails = fails + 1; // popcount(0xF0F0F0F0) + if (nu !== 6'd17) fails = fails + 1; // unsigned-loop leading-one, bit 16 -> 17 + // negatives, hand-computed for vn = 0xB4 (set bits 2,4,5,7): + if (e_step2 !== 6'd5) fails = fails + 1; // highest even set bit (4) + 1 + if (e_start1 !== 7'd8) fails = fails + 1; // highest set bit in [1,32) (7) + 1 + if (e_mul !== 7'd15) fails = fails + 1; // 2*7 + 1 + if (e_off !== 6'd7) fails = fails + 1; // idx where vec[idx+1]; highest = 6 -> 7 + if (e_noP1 !== 6'd7) fails = fails + 1; // highest set bit (7), no +1 + if (e_narrow !== 6'd4) fails = fails + 1; // W != width: only low 16 bits scanned (bit 3) + if (fails == 0) $write("*-* All Finished *-*\n"); + else $write("FAILED: %0d mismatches\n", fails); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_bit_scan_loops_off.py b/test_regress/t/t_bit_scan_loops_off.py new file mode 100755 index 000000000..f11dffe28 --- /dev/null +++ b/test_regress/t/t_bit_scan_loops_off.py @@ -0,0 +1,25 @@ +#!/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: 2024 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +# Reuse the same design; only the optimization switch differs. +test.top_filename = "t/t_bit_scan_loops.v" + +test.compile(verilator_flags2=['--stats', '-fno-bit-scan-loops']) + +# With the optimization disabled, no loop may be lowered (count 0 / line absent). +test.file_grep_not(test.stats, + r'Lowered priority-encoder to mostsetbitp1\s+[1-9]') + +test.execute() + +test.passes()