Narrow NBA shadow copies for partial writes (#8266)

Signed-off-by: Artur Bieniek <[email protected]>
This commit is contained in:
Artur Bieniek
2026-09-10 11:38:46 -04:00
committed by GitHub
parent ee792e235a
commit 09fd15af7b
3 changed files with 216 additions and 1 deletions
+44 -1
View File
@@ -56,6 +56,10 @@
//
// Constraint 3 should always hold with V3Delayed, will check assert it.
//
// If old-value reads prevent elimination, constant partial writes can still narrow both
// copies to the words containing those writes. Other shadow bits are never read. The
// assignments keep their original positions, so evaluation order and scheduling are unchanged.
//
//*************************************************************************
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
@@ -124,6 +128,7 @@ class LifePostDlyVisitor final : public VNVisitorConst {
const AstExecGraph* m_execGraphp = nullptr; // Current AstExecGraph being processed (or null)
const ExecMTask* m_execMTaskp = nullptr; // Current ExecMTask being processed (or null)
VDouble0 m_statAssnDel; // Statistic tracking
VDouble0 m_statWordsSaved; // Words removed from NBA shadow copies
// Maps from Varscope to all their reads and writes
using LocMap = std::unordered_map<const AstVarScope*, std::vector<Location<AstVarRef>>>;
LocMap m_reads; // VarScope read locations
@@ -134,6 +139,40 @@ class LifePostDlyVisitor final : public VNVisitorConst {
bool m_inEvalNba = false; // Traversing under the 'nba' region entry point
// METHODS
void narrowCopies(AstNodeAssign* postp, AstVarScope* dVscp,
const std::vector<Location<AstVarRef>>& writes) {
if (!dVscp->isWide()) return;
AstNodeAssign* const prep = VN_AS(writes[0].nodep()->backp(), NodeAssign);
UASSERT_OBJ(VN_AS(prep->lhsp(), VarRef)->varScopep()
== VN_AS(postp->rhsp(), VarRef)->varScopep()
&& VN_AS(prep->rhsp(), VarRef)->varScopep()
== VN_AS(postp->lhsp(), VarRef)->varScopep(),
prep, "NBA shadow pre/post assignments are not reverse copies");
int lsb = dVscp->width();
int end = 0;
for (size_t i = 1; i < writes.size(); ++i) {
const AstSel* const selp = VN_CAST(writes[i].nodep()->backp(), Sel);
if (!selp || !VN_IS(selp->lsbp(), Const)) return;
const int start = selp->lsbConst();
if (start > dVscp->width() - selp->width()) return;
lsb = std::min(lsb, start);
end = std::max(end, start + selp->width());
}
// Copy one word-aligned range enclosing all writes, including any gaps.
lsb = VL_BITWORD_E(lsb) * VL_EDATASIZE;
end = std::min(VL_WORDS_I(end) * VL_EDATASIZE, dVscp->width());
const int width = end - lsb;
if (width == dVscp->width()) return;
for (AstNodeAssign* const assignp : {prep, postp}) {
FileLine* const flp = assignp->fileline();
assignp->lhsp(new AstSel{flp, assignp->lhsp()->unlinkFrBack(), lsb, width});
assignp->rhsp(new AstSel{flp, assignp->rhsp()->unlinkFrBack(), lsb, width});
assignp->dtypeFrom(assignp->lhsp());
}
m_statWordsSaved += 2 * (dVscp->widthWords() - VL_WORDS_I(width));
}
void squashAssignposts() {
for (const Location<AstNodeAssign>& assign : m_assigns) {
AstVarScope* const dVscp = VN_AS(assign.nodep()->rhsp(), VarRef)->varScopep();
@@ -172,7 +211,10 @@ class LifePostDlyVisitor final : public VNVisitorConst {
}
return true;
}();
if (!qRdOK) continue;
if (!qRdOK) {
narrowCopies(assign.nodep(), dVscp, dWrites);
continue;
}
}
// Mark variable for replacement
@@ -308,6 +350,7 @@ public:
}
~LifePostDlyVisitor() override {
V3Stats::addStat("Optimizations, Lifetime postassign deletions", m_statAssnDel);
V3Stats::addStat("Optimizations, Lifetime NBA copy words removed", m_statWordsSaved);
}
};
+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('simulator')
# Exercise ascending packed ranges and a slice extending past the upper bound.
test.compile(verilator_flags2=['--stats', '-Wno-ASCRANGE', '-Wno-SELRANGE'])
if test.vlt_all:
test.file_grep(test.stats, r'Optimizations, Lifetime NBA copy words removed\s+(\d+)', 512)
test.execute()
test.passes()
+153
View File
@@ -0,0 +1,153 @@
// DESCRIPTION: Verilator: Constant partial nonblocking assignments
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Antmicro
// 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=%p exp=%p (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0);
// verilog_format: on
module t (
input clk
);
int cyc = 0;
always @(posedge clk) cyc <= cyc + 1;
always @(negedge clk) begin
if (cyc == 18) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
nba_partial #(
.W(95),
.BASE(6)
) narrow_low (
.clk,
.cyc
);
nba_partial #(
.W(95)
) narrow_high (
.clk,
.cyc
);
nba_partial #(
.W(4097)
) wide (
.clk,
.cyc
);
nba_partial #(
.W(4099),
.ASC(1)
) wide_other (
.clk,
.cyc
);
nba_fallback fallback (
.clk,
.cyc
);
endmodule
module nba_fallback (
input clk,
input int cyc
);
bit [4096:0] direct;
bit [4096:0] direct_expected;
bit [4096:0] mixed;
bit [4096:0] mixed_expected;
bit [4096:0] looped;
bit [4096:0] looped_expected;
bit [16:0][32:0] dense;
bit [16:0][32:0] dense_expected;
bit [94:0] ends = '1;
bit [94:0] ends_expected = '1;
bit [94:0] overrun = '1;
bit [94:0] overrun_expected = '1;
always @(posedge clk) begin
`checkh(direct, direct_expected);
direct_expected[cyc%4097] = cyc[0];
direct[cyc%4097] <= cyc[0];
`checkh(mixed, mixed_expected);
mixed_expected = mixed;
if (cyc[0]) begin
mixed <= '0;
mixed_expected = '0;
end
mixed[cyc%4097] <= mixed[(cyc+1)%4097] ^ cyc[1];
mixed_expected[cyc%4097] = mixed[(cyc+1)%4097] ^ cyc[1];
`checkh(looped, looped_expected);
looped_expected = looped;
// Each assignment can execute more than once per clock cycle.
for (int i = 0; i < (cyc & 3) + 1; ++i) begin
looped[cyc+i] <= looped[cyc+i+1] ^ cyc[0];
looped_expected[cyc+i] = looped[cyc+i+1] ^ cyc[0];
end
`checkh(dense, dense_expected);
dense_expected = {dense[15:0], 1'b0, 32'(cyc + 1)};
// Individually partial writes collectively update the entire pipeline.
for (int i = 16; i > 0; --i) dense[i] <= dense[i-1];
dense[0] <= {1'b0, 32'(cyc + 1)};
`checkh(ends, ends_expected);
ends_expected[0] = cyc[0];
ends_expected[94] = ends[0];
ends[0] <= cyc[0];
ends[94] <= ends[0];
// Only the in-range bits of this partly out-of-range slice are written.
`checkh(overrun, overrun_expected);
overrun_expected[94:91] = cyc[3:0];
overrun_expected[0] = overrun[91];
overrun[91+:7] <= cyc[6:0];
overrun[0] <= overrun[91];
end
endmodule
module nba_partial #(
parameter W = 4097,
parameter ASC = 0,
parameter BASE = 65
) (
input clk,
input int cyc
);
typedef bit [(ASC ? 3 : W+2):(ASC ? W+2 : 3)] state_t;
state_t q = '1;
state_t expected = '1;
bit [32:0] value;
always @(posedge clk) begin
`checkh(q, expected);
expected = q;
value = {cyc[0], 32'(cyc * 76543)};
if (cyc[0]) begin
q[BASE+:33] <= value;
expected[BASE+:33] = value;
end
if (cyc[1]) begin
// Overlapping writes use old values, keep their order, and preserve untouched bits.
q[BASE+7+:9] <= q[BASE+:9] ^ value[8:0];
expected[BASE+7+:9] = q[BASE+:9] ^ value[8:0];
end
if (cyc[2]) begin
q[BASE-3] <= 1'b1;
expected[BASE-3] = 1'b1;
end
// Repeated updates to the same bit.
for (int i = 0; i < (cyc & 3); ++i) begin
q[BASE+2] <= q[BASE+3] ^ 1'(cyc >> i);
expected[BASE+2] = q[BASE+3] ^ 1'(cyc >> i);
end
end
endmodule