Latency counts register stages from input to output. Throughput counts results per second after the pipeline fills: one result per clock cycle divided by the achieved period. Retiming keeps every path register count, so latency is unchanged; a shorter period then raises throughput at the same latency. Pipelining is the opposite bargain: it adds register stages, so latency grows by one cycle per cut even as throughput rises.
Both designs below compute the same AND of two bits and broadcast it to two outputs. The only difference is the register boundary: one flop sits at the fanout stem, or one flop sits on each branch. The script counts both, maps both against the teaching library, and proves the pair equivalent over 4 compared steps. Read the two netlists as one move in two directions: forward across the stem duplicates the flop, backward merges the pair.
The total rises from 1 to 2: one flop per branch. No path gains a cycle: each branch still crosses exactly one flop, so latency stays at 1 cycle and only the count moves.
read_verilog <<EOV
module fanA(input clk, input a, b, output y1, y2);
reg r;
wire t;
assign t = a & b;
always @(posedge clk) r <= t;
assign y1 = r;
assign y2 = r;
endmodule
module fanB(input clk, input a, b, output y1, y2);
reg q1, q2;
wire t;
assign t = a & b;
always @(posedge clk) begin q1 <= t; q2 <= t; end
assign y1 = q1;
assign y2 = q2;
endmodule
EOV
hierarchy -check
proc
opt_clean
design -save all
hierarchy -check -top fanA
tee -o stat_A.txt stat
select -assert-count 1 t:$dff
techmap
opt_clean
tee -o abc_A.txt abc -liberty course.lib -constr typical.constr -D 100000
dfflibmap -liberty course.lib
tee -o statA_lib.txt stat -liberty course.lib
write_json net_A.json
design -load all
hierarchy -check -top fanB
tee -o stat_B.txt stat
select -assert-count 2 t:$dff
techmap
opt_clean
tee -o abc_B.txt abc -liberty course.lib -constr typical.constr -D 100000
dfflibmap -liberty course.lib
tee -o statB_lib.txt stat -liberty course.lib
write_json net_B.json
design -load all
miter -equiv fanA fanB miter
hierarchy -check -top miter
flatten
tee -o equiv_fan.txt sat -seq 4 -prove trigger 0 -verify -set-init-zero
design -load all
hierarchy -check -top fanB
opt
tee -o stat_Bopt.txt stat
select -assert-count 1 t:$dff
log PASS: fanout pair counted, mapped and proved equivalent over 4 cyclesThe stem variant holds 1 flop and the branched variant holds 2 flops; both map the AND to area 3 at a recorded combinational arrival of 133.96 ps, so either side offers one result per cycle at that arrival. That arrival is an ABC combinational number, not a clock period: with no flop clk-to-q or setup in it, no reachable frequency follows. With flops mapped to library cells the chip area moves from 11 to 19, a gap of exactly one DFF cell. The sequential check proves the two streams equal on all 4 compared steps from the zeroed state: same latency, same rate, different count.
Variant | Latency (cycles) | Throughput (results per cycle) | Flops | Chip area |
|---|---|---|---|---|
Stem (1 flop) | 1 | 1 at 133.96 ps arrival | 1 | 11 |
Branches (2 flops) | 1 | 1 at the same arrival | 2 | 19 |
Read the table in both directions. Forward across the stem duplicates the flop and the count rises; backward merges the pair and the count falls. Neither direction touches latency or throughput at the I/O boundary: each branch still crosses one flop and the period never moves. Pipelining, by contrast, would add a cycle per new stage; the program below pins that difference with plain assertions on a classroom model.
The backward reading deserves a second look, because it is the direction a min-area tool pass attempts on its own: two flops doing the same job are one flop too many, and the stem is the single point that reaches both branches. The run above did the merge by hand so both directions could be counted, but the economics are the tool's: whenever two branch flops sample the same net under the same clock, merging them backward saves a flop without touching any path. The period stands still for a simpler reason than any algorithm: both variants map the same single AND against the same load, so the same gate sets the same arrival either way.
import sys
period = 6.0
cuts = 2
stem_regs = ["r"]
branch_regs = ["q1", "q2"]
path_regs_stem = [1, 1]
path_regs_branched = [1, 1]
flops_before = len(stem_regs)
flops_after = len(branch_regs)
lat_before = max(path_regs_stem)
lat_after_retime = max(path_regs_branched)
lat_after_pipe = lat_before + cuts
thr_before = 1.0 / period
thr_after = 1.0 / 6.0
problems = []
if lat_before != 1:
problems.append("stem latency is 1, got %s" % lat_before)
if lat_after_retime != 1:
problems.append("branched latency is 1, got %s" % lat_after_retime)
if lat_after_retime != lat_before:
problems.append("retiming must not change latency")
if lat_after_pipe != 3:
problems.append("pipelining latency is 3, got %s" % lat_after_pipe)
if abs(thr_before - thr_after) > 1e-12:
problems.append("throughput must hold at a fixed period")
if flops_before != 1 or flops_after != 2:
problems.append("flop counts are 1 and 2, got %s %s" % (flops_before, flops_after))
if flops_after - flops_before != 1:
problems.append("fanout duplication adds one flop")
if not problems:
print("PASS: latency 1 stays 1 across retiming and 1 plus 2 across pipelining, flops 1 to 2")
sys.exit(0)
print("FAIL: " + ", ".join(problems))
sys.exit(1)The script deliberately uses opt_clean instead of opt so the twin registers survive to be counted. The final section is the changed variant: full opt on the branched design merges the identical flops and the report falls back to 1 flop. The merge is legal here because both flops sample the same net under the same clock with no reset or enable, but it destroys the very thing the lesson counts. Count first with opt_clean, optimise after.
One result per cycle at that same arrival, exactly the stem variant's rate. The extra flop bought nothing for speed here; it is the structural price of pushing the boundary across the fanout.
Both flops sample the same net under the same clock with no reset or enable, so they always agree. The script uses opt_clean because the lesson counts the unmerged pair; merging first would erase the measurement.
Flop counts, ABC delays, library-cell areas, netlist pictures, and the 4-step equivalence result on this page come from one Yosys 0.70 run of the script shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): both fanout variants counted, mapped against course.lib with typical.constr, flops covered by dfflibmap, and proved equal by sat. The latency and pipelining relationships are asserted by the stdlib-only Python model on the page, labelled so; the library is fictional, so absolute areas and delays are teaching units.