The peak-sum block adds the largest of eight 16-bit lanes to a running 24-bit total, every cycle. Its first version has a longest path of 119 logic levels. The budget for this example is 38 levels, so the path is more than three times too long.
Four steps bring it inside the budget. Each one was measured, each one costs something, and the last one exists because the third ran into a feedback loop.
Read the top chart against the dashed line; read the bottom chart as the bill for each step.
Step 2 removes 46 levels and costs no flops: it is the same function in a different shape.
Step 3 halves the path again but adds 16 flops and one cycle of latency.
Step 4 gains only 3 levels, yet it is the one that crosses the line. It adds 17 flops and another cycle.
There is no timing library in this experiment, so the measure is logic levels: the number of generic gates (AND, OR, XOR, MUX, NOT) on the longest path between two flops. Each version is synthesised and the longest path is reported by Yosys:
$ yosys -p "read_verilog -sv design.sv; synth -flatten -noabc -top peak_v1; abc -g AND,OR,XOR,MUX; opt_clean; stat; ltp -noff"
...
1122 cells
360 $_AND_
152 $_DFF_PN0_
...
Longest topological path in peak_v1 (length=119):
0: \x_q [26]
1: $abc$2518$new_n220 (via $abc$2518$auto$blifparse.cc:397:parse_blif$2562)
...
119: $auto$alumacc.cc:548:replace_alu$143.Y [23] (via $abc$2518$auto$blifparse.cc:397:parse_blif$3462)
ff: \acc [23] (via $auto$ff.cc:337:slice$505)length=119 is the depth: 119 gates in series.
0: \x_q [26] is where the path starts: bit 26 of the input register, which is lane 1.
ff: \acc [23] is where it ends: the top bit of the total. The path runs through the compare chain and then the whole carry of the adder.
$_DFF_PN0_ counts the flops: 128 for the lanes plus 24 for the total.
Logic levels are not nanoseconds. To connect them to timing, this lesson uses one assumption throughout: 0.05 ns per generic level, including wire (an assumption for this example, not a library figure). The clock is 2.5 ns, with 0.4 ns clock-to-Q and 0.2 ns setup, which leaves 1.9 ns, or 38 levels.
Step | Levels | Logic at 0.05 ns/level | Setup slack at 2.5 ns |
|---|---|---|---|
1 chain | 119 | 5.95 ns | 2.5 − 0.4 − 0.2 − 5.95 = −4.05 ns |
2 tree | 73 | 3.65 ns | −1.75 ns |
3 cut | 39 | 1.95 ns | −0.05 ns |
4 split loop | 36 | 1.80 ns | +0.10 ns |
Loading slack calculator…
The baseline finds the maximum the way a loop in software does: compare lane 1 with lane 0, then lane 2 with that result, and so on. Each compare waits for the one before it.
// snippet: step 1, from peak_v1 below
for (genvar i = 1; i < 8; i++) begin : g_chain
assign c[i] = (x_q[i*16 +: 16] > c[i-1]) ? x_q[i*16 +: 16] : c[i-1];
endThe tree compares pairs instead: four compares at once, then two, then one. Maximum is associative, so the result is the same; only the shape changes.
// snippet: step 2, the max8_tree module below
for (genvar i = 0; i < 4; i++) begin : g_l1 // level 1: four pairs
assign l1[i] = (x[(2*i+1)*16 +: 16] > x[(2*i)*16 +: 16]) ? x[(2*i+1)*16 +: 16] : x[(2*i)*16 +: 16];
end
// level 2: two pairs, level 3: oneRead each drawing from the lanes on the left to the total on the right.
The chain puts seven compare-selects in series; the tree puts three.
Measured, the depth falls from 119 to 73 levels. The tree has 1046 gates against 970 for the chain (cells minus flops): a little more logic for much less depth.
The adder is still in the same cycle, so the path in step 2 is the tree plus the full carry of a 24-bit add.
The natural cut is between the tree and the adder: one 16-bit register, m_q. The tree and the adder now sit in different cycles.
// snippet: step 3, from peak_v3 below
else begin x_q <= x; m_q <= m; acc <= acc + m_q; endLongest topological path in peak_v3 (length=39):
0: \acc [1]
...
ff: \acc [23] (via $auto$ff.cc:337:slice$399)The path now starts and ends at acc. It is the accumulator loop: the total goes through the adder and back into its own register. The tree stage, measured on its own, is 35 levels. The cut cost 16 flops and one cycle, and the block still misses by one level.
Adding another register does not help here. A register can split the tree stage, but the loop is acc <= acc + m_q: one adder and one register, and any register added inside it changes what the block computes (see Common mistakes). Retiming cannot move a register out of the loop either; see retiming mechanism.
The fix is to make the loop itself shorter. Split the 24-bit total into two 12-bit halves. The low half adds the low 12 bits of m_q and puts its carry into a flop, c_q. The high half adds that carry one cycle later, together with the high bits of m_q, which wait one cycle in mh_q.
// snippet: step 4, from peak_v4 below
{c_q, lo} <= lo + m_q[11:0]; // low loop: carry out goes to a flop
mh_q <= m_q[15:12]; // high bits of m wait one cycle for that carry
hi <= hi + 12'(mh_q) + 12'(c_q); // high loop: carry in from the flop
lo_q <= lo; // line the low half up with the high half
assign acc = {hi, lo_q};Each loop still has one register. lo feeds lo, and hi feeds hi, so neither total changes meaning. The carry flop sits between the loops, not inside one.
Every carry is counted once, one cycle late. That is why the high half and lo_q show the total one cycle after step 3.
Measured: the split loop is 23 levels deep. The longest path is now the tree stage at 36, inside the budget.
Cost: 17 flops (1 carry, 4 for mh_q, 12 for lo_q) and one more cycle of latency.
Step | Levels | Where the path is | Flops | Gates | Latency added |
|---|---|---|---|---|---|
1 chain | 119 | x_q to acc | 152 | 970 | 0 |
2 tree | 73 | x_q to acc | 152 | 1046 | 0 |
3 cut | 39 | acc loop | 168 | 1025 | 1 |
4 split loop | 36 | x_q to m_q | 185 | 1020 | 2 |
The bench below runs all four versions on 4000 cycles of directed and random lanes, and checks step 3 one cycle late and step 4 two cycles late against step 1.
Read it top to bottom; each step costs more than the one above it. Restructuring is free in latency, a register is not, and a loop change also changes how the result is presented. The patterns lesson has more restructurings, and the retiming decision guide covers when to let the tool move registers.
None. The longest path after step 3 is the accumulator loop at 39 levels; the tree stage is already 35. Cutting the tree only shortens a path that is not critical, and it costs flops and latency.
The carry from the low half of value k reaches the high half one cycle late. The high bits of value k must arrive in that same cycle, or the high half would add value k's bits with value k-1's carry. Both are counted once, so the total is right, one cycle later.
All versions plus a self-checking bench. To repeat the depth measurements, save the modules before tb_peak as design.sv and run the Yosys command above with each top.
// Peak-sum block: each cycle, add the largest of eight 16-bit lanes to a 24-bit total.
// Every version registers its inputs in x_q, so each measured path starts and ends at a flop.
// Shared by steps 2 to 4: a balanced tree of compare-selects, 3 comparators deep.
module max8_tree (input logic [8*16-1:0] x, output logic [15:0] m);
logic [3:0][15:0] l1;
logic [1:0][15:0] l2;
for (genvar i = 0; i < 4; i++) begin : g_l1 // level 1: four pairs
assign l1[i] = (x[(2*i+1)*16 +: 16] > x[(2*i)*16 +: 16]) ? x[(2*i+1)*16 +: 16] : x[(2*i)*16 +: 16];
end
for (genvar i = 0; i < 2; i++) begin : g_l2 // level 2: two pairs
assign l2[i] = (l1[2*i+1] > l1[2*i]) ? l1[2*i+1] : l1[2*i];
end
assign m = (l2[1] > l2[0]) ? l2[1] : l2[0]; // level 3
endmodule
// Step 1 (baseline): a chain of 7 compare-selects, then the accumulator add, in one cycle.
module peak_v1 (input logic clk, rst_n, input logic [8*16-1:0] x, output logic [23:0] acc);
logic [8*16-1:0] x_q;
logic [7:0][15:0] c; // c[i] = largest of lanes 0 to i
assign c[0] = x_q[15:0];
for (genvar i = 1; i < 8; i++) begin : g_chain
assign c[i] = (x_q[i*16 +: 16] > c[i-1]) ? x_q[i*16 +: 16] : c[i-1];
end
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin x_q <= '0; acc <= '0; end
else begin x_q <= x; acc <= acc + c[7]; end
endmodule
// Step 2: the same function with the tree. Same latency, same flops.
module peak_v2 (input logic clk, rst_n, input logic [8*16-1:0] x, output logic [23:0] acc);
logic [8*16-1:0] x_q;
logic [15:0] m;
max8_tree u_max (.x(x_q), .m);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin x_q <= '0; acc <= '0; end
else begin x_q <= x; acc <= acc + m; end
endmodule
// Step 3: a register between the tree and the accumulator. One more cycle of latency.
module peak_v3 (input logic clk, rst_n, input logic [8*16-1:0] x, output logic [23:0] acc);
logic [8*16-1:0] x_q;
logic [15:0] m, m_q;
max8_tree u_max (.x(x_q), .m);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin x_q <= '0; m_q <= '0; acc <= '0; end
else begin x_q <= x; m_q <= m; acc <= acc + m_q; end
endmodule
// Step 4: split the accumulator loop into two 12-bit loops joined by a registered carry.
// Each loop still has exactly one register; the total appears one cycle later than in step 3.
module peak_v4 (input logic clk, rst_n, input logic [8*16-1:0] x, output logic [23:0] acc);
logic [8*16-1:0] x_q;
logic [15:0] m, m_q;
logic [11:0] lo, lo_q, hi;
logic [3:0] mh_q;
logic c_q;
max8_tree u_max (.x(x_q), .m);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
x_q <= '0; m_q <= '0; lo <= '0; c_q <= 1'b0; mh_q <= '0; hi <= '0; lo_q <= '0;
end else begin
x_q <= x;
m_q <= m;
{c_q, lo} <= lo + m_q[11:0]; // low loop: carry out goes to a flop
mh_q <= m_q[15:12]; // high bits of m wait one cycle for that carry
hi <= hi + 12'(mh_q) + 12'(c_q); // high loop: carry in from the flop
lo_q <= lo; // delay the low half to line up with the high half
end
assign acc = {hi, lo_q};
endmodule
// The mistake: a "pipeline" register inside the accumulator loop. Two registers now sit
// around the loop, so it keeps two interleaved totals instead of one.
module peak_bad (input logic clk, rst_n, input logic [8*16-1:0] x, output logic [23:0] acc);
logic [8*16-1:0] x_q;
logic [15:0] m, m_q;
logic [23:0] s_q;
max8_tree u_max (.x(x_q), .m);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin x_q <= '0; m_q <= '0; s_q <= '0; acc <= '0; end
else begin x_q <= x; m_q <= m; s_q <= acc + m_q; acc <= s_q; end
endmodule
Tools: Yosys 0.69+post (git sha1 143eb14f9), Icarus Verilog 13.0 and Verilator 5.048, run on 2026-10-17. Depth: yosys -p "read_verilog -sv design.sv; synth -flatten -noabc -top <top>; abc -g AND,OR,XOR,MUX; opt_clean; stat; ltp -noff" on the design modules of the lesson block (checked identical to the measured file).
Results (longest path levels / cells / flops): peak_v1 119 / 1122 / 152, path x_q to acc; peak_v2 73 / 1198 / 152; peak_v3 39 / 1193 / 168, path acc to acc; peak_v4 36 / 1205 / 185, path x_q to m_q; peak_bad 39 / 1212 / 192, path acc to s_q. Separate measurement tops: tree stage alone (x_q to m_q) 35 levels; the split loop alone 23 levels. Gate counts in the lesson are cells minus flops.
Bench: iverilog -g2012 design.sv tb.sv; vvp -n, and verilator --binary: PASS in both, 4000 cycles (directed lanes, ties, all-ones to force wrap, then random), steps 1-4 equal after aligning steps 3 and 4 by one and two cycles; peak_bad differs from the aligned total on 3998 cycles.
Scope and limits: generic two-input gates mapped by ABC, no library, no wire load, fanout or drive; the 0.05 ns per level, 2.5 ns clock, 0.4 ns clock-to-Q and 0.2 ns setup are assumptions for the example, not measured delays. ABC put the same tree stage at 35 or 36 levels depending on context.