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 maddBurst of 8 inputsRequest-response, 8 inputs
straight from the logic (as shipped)results at e2-e98 results in 8 edges
registeredresults at e3-e108 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.

A burst of eight inputs, output as shipped and output registeredThe bench offers a new input at every edge from edge 2 to edge 9. y is the shipped design; y_r is the same design with a register on the output.
01234567891011clkin_valida012345678out_validy01021324354657687out_valid_ry_r01021324354657687
  1. Cycle 2

    Edge 2 takes a = 1, b = 10, c = 0. The shipped design shows y = 10 after the same edge: latency 1.

  2. Cycle 3

    The registered version shows 10 one edge later. Its latency is 2.

  3. Cycle 9

    Both versions give one result per edge for the whole burst. Throughput is one per cycle either way.

  4. 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 versionLatencyBurst of 8Request-response, 8CellsFlopsLevels of logic on this flow
output from the logic (shipped)1results at e2-e98 edges4072532
output registered2results at e3-e1016 edges4244231
input, product and output registers3results at e4-e1124 edges4516730

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.

madd feeding an accumulatorArrows carry data. The accumulator adds each valid result into total. Levels are measured with Yosys 0.70 on the joined design.
madd feeding an accumulatorArrows carry data. The accumulator adds each valid result into `total`. Levels are measured with Yosys 0.70 on the joined design. flop a_q, b_q, c_q madd input register logic multiply and add 32 levels inside madd flop y register only in the registeredversion logic total + y the user: a 24-bitadder flop total 24-bit register operandsy, combinationalyy, registerednew total
  1. Combinational output

    One path runs from a_q through the multiply, the add and the accumulator's adder into total: 44 levels.

    Follow the calls, values and waits
    1. data one path: a_q, b_q, c_q → multiply and add
    2. data one path: multiply and add → total + y
    3. data one path: total + y → total
  2. Registered output

    The register splits it in two. madd's path ends at y: 31 levels. The accumulator's own path, total back into total, is 39 and now the longest.

    Follow the calls, values and waits
    1. data path 1: 31: a_q, b_q, c_q → multiply and add
    2. data path 1: 31: multiply and add → y register
    3. data path 2: 39: y register → total + y
    4. 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.

Design brief
PortDirectionWidthMeaning
clkinput1clock
rst_ninput1reset, asynchronous, active-low
in_validinput1a, b and c are taken this cycle
a, b, cinput[7:0]the operands
out_validoutput1marks each result
youtput[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
  1. 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.

  2. 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.

  3. 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.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: 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 wrong

Original 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

Predict the outputlat2 has a latency of two edges. The bench sends a request, waits until out_valid is 1, then sends the next, five times. It counts the edges from the first request to the last result. What is the output line?
// lat2: a block with a latency of two edges (an input and an output register).
module lat2 (input logic clk, rst_n, in_valid, output logic out_valid);
  logic v1;
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin v1 <= 1'b0; out_valid <= 1'b0; end
    else        begin v1 <= in_valid; out_valid <= v1; end
endmodule
module tb;
  logic clk = 0, rst_n = 1, in_valid = 0, out_valid;
  int e = 0, first = 0, last = 0, results = 0;
  lat2 dut (.clk(clk), .rst_n(rst_n), .in_valid(in_valid), .out_valid(out_valid));
  always #5 clk = ~clk;
  initial begin
    #1 rst_n = 0; #1 rst_n = 1;                 // reset pulse before the first edge
    // Request and response: one request, then wait for its result before the next.
    for (int i = 0; i < 5; i++) begin
      in_valid = 1;
      @(posedge clk); e++; #1 in_valid = 0;      // the request is taken at edge e
      if (i == 0) first = e;
      while (!out_valid) begin @(posedge clk); e++; #1; end
      results++; last = e;
    end
    $display("%0d results in %0d edges", results, last - first + 1);
    $finish(0);
  end
endmodule
Spot the bugEach option registers the output of the multiply-add after its input register, for a latency of 2. The bench sends four inputs back to back and checks each result and its edge. Which option fails?
ConceptYou add a register on a block's output. Inputs still arrive one per cycle. What changes?
ConceptWhich user of a block gets slower in cycles when the block's latency grows from 1 to 2?
Read the waveformThis is the burst through madd with a registered output. At which edge does the result of the input with a = 3 appear?
madd with a registered output, burst of eight
01234567891011in_valida012345678out_validy01021324354657687

One value per edge from edge 0, printed after each edge settles. b is 10 throughout and c is a - 1.

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).