Latency, throughput and registered outputs
Measure a block's latency and throughput separately, predict who gains and who loses when you register an output, and see on the gate count where a registered output pays off.
- Lesson
- R4.2
- Time
- 25 min
- Runs on
- Your browser
- Check
- 5 questions
A team registered the output of a multiply-add block to meet a faster clock, and reported a win. The block still took one input per cycle. But its user sent one input, waited for the answer, then sent the next, and every answer now came one edge later. That loop took twice as many cycles.
| Output of madd | Burst of 8 inputs | Request-response, 8 inputs |
|---|---|---|
| straight from the logic (as shipped) | results at e2-e9 | 8 results in 8 edges |
| registered | results at e3-e10 | 8 results in 16 edges |
Measured on the lab bench. The burst still gets one result per edge. The loop that waits for each result gets half as many.
Two numbers for speed
Latency is the number of clock edges an input passes before its result is at the output. The lab block registers its inputs and computes the result from those registers, so its latency is 1. Throughput is how many results come out per cycle when inputs arrive back to back.
y is the shipped design; y_r is the same design with a register on the output.- Cycle 2
Edge 2 takes a = 1, b = 10, c = 0. The shipped design shows y = 10 after the same edge: latency 1.
- Cycle 3
The registered version shows 10 one edge later. Its latency is 2.
- Cycle 9
Both versions give one result per edge for the whole burst. Throughput is one per cycle either way.
- Cycle 10
The registered version delivers its last result, 87, at edge 10, one edge after the shipped design.
One value per edge, printed by the bench after each edge settles. Registering the output moved every result one edge later and kept the spacing.
So a register on the output adds one to the latency and leaves the throughput alone. The clock may rise, because the paths got shorter, and results per second rise with it, as long as the user can take a result every cycle.
Who feels the latency
Some users cannot keep a block busy. A processor that needs each answer before it can form the next question sends one input and waits. Each round trip then costs the full latency, and throughput becomes one result per latency. The second half of the bench is that user.
| madd version | Latency | Burst of 8 | Request-response, 8 | Cells | Flops | Levels of logic on this flow |
|---|---|---|---|---|---|---|
| output from the logic (shipped) | 1 | results at e2-e9 | 8 edges | 407 | 25 | 32 |
| output registered | 2 | results at e3-e10 | 16 edges | 424 | 42 | 31 |
| input, product and output registers | 3 | results at e4-e11 | 24 edges | 451 | 67 | 30 |
Measured on the lab bench and with Yosys 0.70. Every extra stage costs the waiting user one more edge per result. The burst never slows down.
Look at the levels column. Registering the output barely changed the block's own count. The longest path still runs from a_q through the multiplier and the adder, now into a flop: 31 levels against 32. Cutting between the multiplier and the adder saves only 2 levels, because the multiplier is the long part. R4.5 deals with that.
Where a registered output pays off
The gain shows where one module meets the next. A combinational output path does not end at the port; it continues into the next module's logic. Join madd to a user that adds each result into a 24-bit running total, and synthesize the two together.
total. Levels are measured with Yosys 0.70 on the joined design.- Combinational output
One path runs from
a_qthrough the multiply, the add and the accumulator's adder intototal: 44 levels.Follow the calls, values and waits
- data one path: a_q, b_q, c_q → multiply and add
- data one path: multiply and add → total + y
- data one path: total + y → total
- Registered output
The register splits it in two. madd's path ends at
y: 31 levels. The accumulator's own path,totalback intototal, is 39 and now the longest.Follow the calls, values and waits
- data path 1: 31: a_q, b_q, c_q → multiply and add
- data path 1: 31: multiply and add → y register
- data path 2: 39: y register → total + y
- data path 2: 39: total + y → total
Read the diagram as text
- a_q, b_q, c_q: madd input register
- multiply and add: 32 levels inside madd
- y register: only in the registered version
- total + y: the user: a 24-bit adder
- total: 24-bit register
- a_q, b_q, c_q → multiply and add: operands
- multiply and add → total + y: y, combinational
- multiply and add → y register: y
- y register → total + y: y, registered
- total + y → total: new total
Joined design, measured: 525 cells and 44 levels with the output from the logic; 545 cells and 39 levels with it registered.
That is why the house rule registers every module output. Each module's levels count is then its own, and a team can meet its clock without knowing the logic on the other side. Bus specifications such as AXI go further and forbid any combinational path from an input to an output of an interface.
Output from the logic
assign y = a_q * b_q + 16'(c_q);
assign out_valid = v_q;
Latency 1. The user's logic adds to this path: 44 levels with an accumulator behind it.
Registered output
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin out_valid <= 1'b0; y <= '0; end
else begin
out_valid <= v_q; // valid moves with y
y <= a_q * b_q + 16'(c_q);
end
Latency 2, same burst throughput, 17 more flops. The path ends at the port: 31 levels, whatever the user does.
| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | clock |
| rst_n | input | 1 | reset, asynchronous, active-low |
| in_valid | input | 1 | a, b and c are taken this cycle |
| a, b, c | input | [7:0] | the operands |
| out_valid | output | 1 | marks each result |
| y | output | [15:0] | y = a * b + c for every input taken with in_valid = 1, in order |
- Latency
- 1 edge as shipped; 2 with the output registered
- Throughput
- one input per cycle; out_valid marks each result
- Corner cases
- a user that waits for each result before the next input
The lab ships madd with its output straight from the logic. Run it: results at e2 to e9, and 8 edges for the request-response loop. Register y and out_valid as in the right panel. Run again: the burst moves one edge later and the loop takes 16 edges. Synthesize: 42 flops and 31 levels.
Asked in interviews
What is the difference between latency and throughput?
Latency is how many cycles one input takes to produce its result. Throughput is how many results come out per cycle when inputs arrive back to back. A pipeline raises latency and keeps throughput at one per cycle.
Does registering an output make a block faster?
It makes each result one cycle later and keeps the results per cycle. It shortens the paths that run on into the next module, so the clock may go faster. A user that waits for each result gets slower in cycles.
Why register module outputs?
So that no path crosses a module boundary. Each block's timing is then its own, synthesis and timing can be done per block, and joining blocks cannot create a new long path.
Try it
Each line shows the input taken at that edge and the output right after it. The shipped design shows the result on the same line as its input: one edge of latency. The request-response loop sends one input per edge, because each result is there before the next edge.
With the output registered, the burst results move to e3-e10 and the loop needs 16 edges, e12-e27. Both halves still report 0 wrong. Synthesize shows 424 cells, 42 flops and 31 levels.
Synthesize the design to measure its cells and inspect the mapped gates.
e2 in_valid=1 a=1 b=10 c=0 out_valid=1 y=10 e3 in_valid=1 a=2 b=10 c=1 out_valid=1 y=21 e4 in_valid=1 a=3 b=10 c=2 out_valid=1 y=32 e5 in_valid=1 a=4 b=10 c=3 out_valid=1 y=43 e6 in_valid=1 a=5 b=10 c=4 out_valid=1 y=54 e7 in_valid=1 a=6 b=10 c=5 out_valid=1 y=65 e8 in_valid=1 a=7 b=10 c=6 out_valid=1 y=76 e9 in_valid=1 a=8 b=10 c=7 out_valid=1 y=87 e10 in_valid=0 a=8 b=10 c=7 out_valid=0 y=87 e11 in_valid=0 a=8 b=10 c=7 out_valid=0 y=87 burst: 8 inputs at e2-e9, results at e2-e9, 0 wrong request-response: 8 results in 8 edges (e12-e19), 0 wrongOriginal design · 407 cells · 25 flip-flops · 0 latches · 32 logic levels. Synthesis uses Yosys; counts depend on the toolchain.
Read included file: tb.sv
`timescale 1ns/1ps
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic rst_n = 1; // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
int edge_n = -1;
always @(posedge clk) edge_n = edge_n + 1;
logic in_valid = 0;
logic [7:0] a = 0, b = 0, c = 0;
logic out_valid;
logic [15:0] y;
int want[$]; // the model's results, oldest first
int results = 0, wrong = 0, first = -1, last = -1, start;
bit show = 0;
// Offer one input before the next edge; the model computes its result now.
task automatic offer(input int x, input int z, input int w);
in_valid = 1; a = x; b = z; c = w;
want.push_back(x * z + w);
endtask
madd dut (.clk(clk), .rst_n(rst_n), .in_valid(in_valid), .a(a), .b(b), .c(c),
.out_valid(out_valid), .y(y));
// Advance to the next rising edge, wait for the design to settle, then print one line.
task automatic tick();
@(posedge clk); #1;
if (out_valid) begin // a result: check it against the oldest model value
results++;
if (y != want.pop_front()) wrong++;
if (first < 0) first = edge_n;
last = edge_n;
end
if (show) $display("e%0d in_valid=%b a=%0d b=%0d c=%0d out_valid=%b y=%0d", edge_n, in_valid, a, b, c, out_valid, y);
endtask
initial begin
#1 rst_n = 0; tick(); tick(); rst_n = 1; // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
// Burst: eight inputs on eight edges in a row, every line printed.
show = 1;
for (int i = 0; i < 8; i++) begin offer(i + 1, 10, i); tick(); end
in_valid = 0;
repeat (2) tick();
$display("burst: 8 inputs at e2-e9, results at e%0d-e%0d, %0d wrong", first, last, wrong);
// Request and response: the next input waits until the previous result has come out.
show = 0; results = 0; first = -1; start = edge_n + 1;
for (int i = 0; i < 8; i++) begin
offer(20 + i, 3, 1); tick();
in_valid = 0;
while (results < i + 1) tick();
end
$display("request-response: 8 results in %0d edges (e%0d-e%0d), %0d wrong", last - start + 1, start, last, wrong);
$finish(0); // quiet: no "$finish called" line
end
endmodule
Check yourself
Remember
- Latency is the edges from an input to its result; throughput is results per cycle when inputs arrive back to back.
- A register on an output adds one edge of latency and keeps burst throughput at one result per cycle.
- A user that waits for each result before the next input sees only the latency: 8 results took 8 edges with latency 1 and 16 with latency 2.
- A registered output barely changes a block's own levels (madd: 32, then 31); it stops the next module's logic adding to the path (44 levels joined, 39 with the register).