Skip to content
VLSI Mentor

DDR · Module 23

DDR Latency Anatomy

Chapter 10.5 asks whether a read met its timing. This asks where the time went — a decomposition into six components that sum exactly, of which only one has no upper bound.

Chapter 10.5 built a read transaction tracker and asked a verification question: did the data arrive when the timing said it should? Its outputs are first_beat_early, first_beat_late, txn_overdue — a verdict on legality.

This chapter asks a different question about the same transaction: where did the time go?

Those are not the same question, and the second one has an answer the first cannot produce. A read can meet every device timing rule perfectly and still take four times longer than another read to the same bank, because most of a request's latency is often spent somewhere the device timing rules do not reach.

The module's central law, stated once and used throughout:

Peak bandwidth is a property of the interface. Achieved bandwidth is a property of the workload meeting the timing rules. The gap between them is not waste — it is the cost of constraints that cannot be removed, plus decisions that can be improved.

For latency the same split applies, and it is this chapter's organising idea: some components of a request's latency are fixed by the device and cannot be reduced by any controller. Exactly one component has no upper bound at all. Telling them apart is the whole skill.

1. Two Different Questions About One Transaction

It is worth being precise about the difference, because conflating the two produces a controller that is verifiably correct and inexplicably slow.

10.5's questionThis chapter's question
AsksDid the data arrive legally?Where did the time go?
Answer shapeA verdict — early, late, overdueA decomposition that sums
ObservesThe device pathThe whole path, including the queue
Fails whenA timing rule is violatedNothing — it always has an answer
Useful forVerificationDeciding what to change

The fourth row is the one that matters. A timing checker is silent when everything is legal, and that silence is its success condition. A decomposition is never silent — every request has a breakdown, including the ones that behaved perfectly, and comparing breakdowns across requests is where the information is.

The fifth row is why this module exists. A verification failure tells you what to fix. A decomposition tells you what is worth fixing, which is a different and usually harder question.

2. The Verified Configuration

Every derivation in this module uses one configuration, fixed here so that no chapter can quietly change an assumption between comparisons.

DDR4-1600K, x8 device, 8Gb density, 1KB page. Chosen for one reason beyond being a real JEDEC bin: at this grade every timing parameter is an exact integer number of clock cycles, so the arithmetic below is hand-checkable rather than rounded.

ParameterValuenCKCategory
tCK(avg)1.25 nsVERIFIED
CL13.75 ns11VERIFIED
tRCD13.75 ns11VERIFIED
tRP13.75 ns11VERIFIED
tRAS35 ns28VERIFIED
tRC48.75 ns39VERIFIED
CWL9 or 11VERIFIED
tCCD_S / tCCD_L4 / 5VERIFIED
tRRD_S / tRRD_L (x8)4 / 5VERIFIED
tFAW (1KB page)25 ns20VERIFIED
tWTR_S / tWTR_L2 / 6VERIFIED
tWR15 ns12VERIFIED
tRTPmax(4 nCK, 7.5 ns)6VERIFIED
tRFC1 (8Gb)350 ns280VERIFIED

Derived from the above, and used throughout: peak bandwidth is 1600 MT/s × 8 bytes = 12.8 GB/s; the interface carries 2 transfer slots per clock cycle, so 16 bytes per cycle; and a BL8 burst occupies 4 clock cycles. The 64-bit data width is the DDR4 DIMM's verified organisation — 64 data bits plus 8 for ECC.

3. The Components, Named and Exhaustive

A decomposition is only useful if it is exhaustive — every cycle of the total belongs to exactly one component. Here is the set this chapter uses, and the reason each is separate.

ComponentFromToOwner
IngressRequest presentedAccepted into the queueThe interconnect and the controller's admission
Queue waitAcceptedSelected by the arbiterPolicy17.4 and 23.4
ArbitrationSelectedCommand committedThe commit point of 17.1
Row workFirst commandRow ready for a column commandThe devicetRP and/or tRCD
CASColumn commandFirst data beatThe deviceCL
BurstFirst beatLast beatThe device and the burst length
ReturnLast beatDelivered to the requesterThe controller's return path

Two properties of that set are what make it a decomposition rather than a list.

The components are disjoint. A cycle spent waiting in the queue is not also a tRCD cycle. This sounds obvious and is the single most common modelling error: a naive measurement that starts a tRCD timer at request arrival rather than at the ACT command double-counts the queue wait into the device's column.

They are exhaustive by construction. The seven intervals are contiguous and abut exactly — each one's end is the next one's start. So their sum is the total, not approximately, and §12's first property asserts precisely that.

4. A Row Miss, Decomposed

Take one read to an idle bank. DERIVED from §2's verified parameters, with the queue wait marked ILLUSTRATIVE because it is a property of the workload and the policy rather than of the device.

ComponentCyclesCategoryWhy this value
Ingress0ILLUSTRATIVEAssumed immediate acceptance
Queue wait6ILLUSTRATIVEA chosen value; §8 is about why no other kind exists
Arbitration1ILLUSTRATIVEOne cycle from selection to commit
Row work (tRCD)11DERIVED from VERIFIEDBank idle, so one ACT then tRCD
CAS (CL)11DERIVED from VERIFIEDCL = 11 at this bin
Burst (BL8)4DERIVED from VERIFIED8 beats at 2 per cycle
Return0ILLUSTRATIVEAssumed immediate delivery
TOTAL33= 41.25 ns

The sum is 6 + 1 + 11 + 11 + 4 = 33, and 33 × 1.25 ns = 41.25 ns. Both recomputed.

Note what fraction is device time. Of 33 cycles, 26 are tRCD + CL + burst — device timing, unreducible by any controller decision. Seven cycles are queueing and arbitration. So on this request, 79% of the latency is irreducible, and a scheduler that eliminated all queueing entirely would improve it by 21%.

That ratio is the reason performance work on memory controllers is hard, and it is also why the ratio itself is the first thing worth measuring.

5. The Same Request as a Conflict

Change one thing — the bank has a different row open — and re-derive. Nothing else changes.

ComponentMissConflictChange
Queue wait66
Arbitration11
Row work1122+11 — a PRE before the ACT
CAS1111
Burst44
TOTAL3344+11

44 × 1.25 ns = 55.00 ns. And the difference is exactly tRP = 11 cycles, verified by subtraction: 44 − 33 = 11.

That exactness is worth pausing on. The cost of a conflict over a miss is precisely one tRP, not approximately, and not something requiring measurement — it is a subtraction over published parameters. Which means a controller that knows its hit, miss and conflict counts can compute the row-work contribution to average latency without measuring latency at all.

Chapter 23.3 builds on exactly that, and it is why 9.6's class counters are a performance instrument rather than merely diagnostic telemetry.

6. And as a Hit

The third class completes the set.

ComponentHitMissConflict
Queue wait666
Arbitration111
Row work01122
CAS111111
Burst444
TOTAL223344
Nanoseconds27.5041.2555.00

Three numbers worth extracting, all recomputed.

A conflict is exactly twice a hit's latency under this configuration — 44 = 2 × 22. That is an artefact of tRP = tRCD = CL = 11 at this bin rather than a general truth, and saying so is the difference between an observation and a claim.

The spread is 22 cycles, and all of it is row work. Queueing, arbitration, CAS and burst are identical across the three classes. Locality is the only thing that moved.

The minimum possible latency here is 15 cycles — a hit with zero queueing and zero arbitration, so CL + burst = 11 + 4. That is the floor for this configuration, and no policy reaches below it. DERIVED, and worth knowing because it is the denominator any latency improvement should be measured against.

7. Irreducible Versus Avoidable

§3's ownership column divides the components into two groups, and the division is the practical content of the chapter.

Irreducible — device timingAvoidable — policy
Row work (tRP, tRCD)Queue wait
CAS (CL)Arbitration
BurstIngress, Return

But that table is too simple in one important way, and the correction is the section's point.

Row work is irreducible per access and avoidable in aggregate. A controller cannot make tRCD shorter. It can change how often a request pays it at all, by changing which row is open when the request arrives — which is page policy (23.5) and address mapping (18.2).

So the honest three-way split is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   TRULY IRREDUCIBLE      CL, burst
                          every access pays these, always

   IRREDUCIBLE PER ACCESS,  tRP, tRCD
   AVOIDABLE IN AGGREGATE   paid only on a miss or conflict;
                            how often is a POLICY outcome

   AVOIDABLE                queue wait, arbitration
                            entirely a policy outcome

CL and burst are the floor. Nothing in five chapters reduces them. Everything else is a question about how often a cost is incurred rather than how large it is.

8. The One Component With No Upper Bound

Six of §3's seven components have hard upper bounds. CL, burst, tRCD and tRP are published constants. Arbitration and return are small fixed pipeline depths.

Queue wait has no upper bound at all.

It is bounded by nothing in the device specification, nothing in the controller's structure, and nothing in the timing rules. A request can wait in a queue indefinitely if the arbitration policy never selects it — and 17.4's clue about an arbiter with no defined behaviour when two candidates are equally legal is exactly this hazard.

Three consequences follow, and they shape the rest of the module.

Latency distributions have a tail, and the tail is queueing. The device components are constant, so all of the variance in a latency distribution comes from queue wait. A histogram of request latencies is, up to a constant offset, a histogram of queue waits.

The worst case is a policy property, not a device property. “What is the worst latency this memory system can produce?” has no answer from the datasheet. It is set by the arbitration policy's fairness guarantee — and if the policy has no such guarantee, the honest answer is unbounded.

So a latency bound must be designed, not measured. Chapter 23.4 builds the starvation bound that provides one. This chapter's job is to make the unbounded component visible and separately attributed, which is what §10's block does.

9. Why the Average Is the Wrong Statistic

The problem with a mean is not that it is inaccurate. It is that latency distributions in a memory controller are not symmetric, and a mean is only a good summary of a symmetric distribution.

§8's structure explains the asymmetry precisely: latency is a constant (device time) plus a queue wait that is bounded below by zero and unbounded above. That is a right-skewed distribution by construction, and for right-skewed data the mean sits above the mode and below the tail — describing neither.

What to report instead, in increasing order of usefulness:

StatisticTells youCost to compute
MeanTotal time / total requestsOne accumulator, one counter
MaximumThe worst thing that happenedOne register and a compare
DistributionThe shape, so the tail is visibleOne counter per bucket
High percentileWhat most requests stay underA distribution plus a scan

The maximum is nearly free and is the single highest-value addition. One register, one comparison per request. It cannot be averaged away, it cannot be diluted by a large number of fast requests, and it is the statistic that answers §8's worst-case question.

A bucketed distribution is the honest summary, and §12's second block builds one. The reason to bucket rather than store every sample is bounded storage: a histogram over N buckets costs N counters regardless of how many requests pass through.

10. The Latency Decomposer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// latency_decomposer -- attributes one request's end-to-end latency to
// named, disjoint, exhaustive components.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHAT IT DOES NOT MODEL:
//   - the device, its timing rules, or their legality (10.5 owns that)
//   - the arbitration policy (17.4 / 23.4 own it)
//   - any absolute time. Everything here is in CLOCK CYCLES; the
//     conversion to nanoseconds needs tCK, which is a configuration
//     fact and not this block's business.
//
// THE CENTRAL INVARIANT: the components SUM to the total, exactly, on
// every cycle of a tracked request's life. §14's P1 asserts it, and
// it is what makes this a decomposition rather than a set of timers.
// ---------------------------------------------------------------------
module latency_decomposer #(
  // Widest latency this block can attribute, in cycles. Saturating,
  // never wrapping: §12's note that a wrapped latency reads as a small
  // one and inverts the conclusion.
  parameter int MAX_LAT = 1023,
  parameter int LAT_W   = $clog2(MAX_LAT + 1),
  // Number of stages. Fixed by §3's decomposition, not configurable --
  // changing it would change what the block claims to measure.
  parameter int STAGES  = 6
) (
  input  logic              clk,
  input  logic              rst_n,

  // ── Stage-transition events. Each is a ONE-CYCLE pulse marking the
  //    boundary between two components of §3. The controller already
  //    produces every one of these internally; the contribution here is
  //    attributing the intervals between them.
  input  logic              ev_accept,      // ingress -> queue wait
  input  logic              ev_selected,    // queue wait -> arbitration
  input  logic              ev_committed,   // arbitration -> row work
  input  logic              ev_col_cmd,     // row work -> CAS
  input  logic              ev_first_beat,  // CAS -> burst
  input  logic              ev_last_beat,   // burst -> return
  input  logic              ev_delivered,   // return -> done

  // ── Per-component results, valid when `done` is asserted.
  output logic [LAT_W-1:0]  c_queue,
  output logic [LAT_W-1:0]  c_arb,
  output logic [LAT_W-1:0]  c_rowwork,
  output logic [LAT_W-1:0]  c_cas,
  output logic [LAT_W-1:0]  c_burst,
  output logic [LAT_W-1:0]  c_return,
  output logic [LAT_W-1:0]  total,

  // ── Observability.
  output logic              busy,
  output logic [2:0]        stage,
  output logic              done,
  // The invariant, published as a signal so a checker can require it
  // rather than recompute it. Held high while the sum is correct.
  output logic              sum_ok,
  // Which component is currently the largest. Costs a small comparison
  // tree and answers the only question anyone asks of a breakdown.
  output logic [2:0]        dominant,

  output logic              err_saturated,
  output logic              err_out_of_order,
  output logic              err_restart_while_busy
);

  initial begin
    if (MAX_LAT < 8)
      $fatal(1, "latency_decomposer: MAX_LAT (%0d) too small to be useful", MAX_LAT);
    if (STAGES != 6)
      $fatal(1, "latency_decomposer: STAGES is fixed at 6 by the decomposition");
  end

  localparam logic [2:0] S_IDLE  = 3'd0;
  localparam logic [2:0] S_QUEUE = 3'd1;
  localparam logic [2:0] S_ARB   = 3'd2;
  localparam logic [2:0] S_ROW   = 3'd3;
  localparam logic [2:0] S_CAS   = 3'd4;
  localparam logic [2:0] S_BURST = 3'd5;
  localparam logic [2:0] S_RET   = 3'd6;

  logic [2:0]       st;
  logic [LAT_W-1:0] acc [6];      // one accumulator per component
  logic [LAT_W-1:0] tot;
  logic             sat;
  logic             dn;
  logic             ooo;

  // Which accumulator the current stage charges. Declared above the
  // assigns that read it.
  logic [2:0] chg;
  always_comb begin
    unique case (st)
      S_QUEUE : chg = 3'd0;
      S_ARB   : chg = 3'd1;
      S_ROW   : chg = 3'd2;
      S_CAS   : chg = 3'd3;
      S_BURST : chg = 3'd4;
      S_RET   : chg = 3'd5;
      default : chg = 3'd0;
    endcase
  end

  // ── Legal next-event check. The stages are ORDERED (§3: each one's
  //    end is the next one's start), so an event arriving out of order
  //    is an integration error and must not silently mis-attribute.
  logic ev_expected;
  always_comb begin
    unique case (st)
      S_IDLE  : ev_expected = ev_accept;
      S_QUEUE : ev_expected = ev_selected;
      S_ARB   : ev_expected = ev_committed;
      S_ROW   : ev_expected = ev_col_cmd;
      S_CAS   : ev_expected = ev_first_beat;
      S_BURST : ev_expected = ev_last_beat;
      S_RET   : ev_expected = ev_delivered;
      default : ev_expected = 1'b0;
    endcase
  end

  logic any_ev;
  assign any_ev = ev_accept | ev_selected | ev_committed | ev_col_cmd
                | ev_first_beat | ev_last_beat | ev_delivered;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      st  <= S_IDLE;
      tot <= '0;
      sat <= 1'b0;
      dn  <= 1'b0;
      ooo <= 1'b0;
      for (int i = 0; i < 6; i++) acc[i] <= '0;
    end else begin
      dn  <= 1'b0;
      ooo <= 1'b0;

      if (st == S_IDLE) begin
        if (ev_accept) begin
          // A fresh request clears every accumulator. Not clearing them
          // is the defect that makes a second request's breakdown the
          // SUM of two requests -- plausible-looking and wrong.
          for (int i = 0; i < 6; i++) acc[i] <= '0;
          tot <= '0;
          sat <= 1'b0;
          st  <= S_QUEUE;
        end
      end else begin
        // ── Charge this cycle to the current stage, and to the total.
        //    Both saturate, and they saturate TOGETHER so the invariant
        //    of §14's P1 survives saturation rather than breaking at it.
        if (tot != LAT_W'(MAX_LAT)) begin
          acc[chg] <= acc[chg] + LAT_W'(1);
          tot      <= tot + LAT_W'(1);
        end else begin
          sat <= 1'b1;
        end

        // ── Advance on the expected event.
        if (ev_expected) begin
          unique case (st)
            S_QUEUE : st <= S_ARB;
            S_ARB   : st <= S_ROW;
            S_ROW   : st <= S_CAS;
            S_CAS   : st <= S_BURST;
            S_BURST : st <= S_RET;
            S_RET   : begin st <= S_IDLE; dn <= 1'b1; end
            default : st <= S_IDLE;
          endcase
        end else if (any_ev) begin
          // An event arrived that is not the one this stage expects.
          ooo <= 1'b1;
        end
      end
    end
  end

  assign c_queue   = acc[0];
  assign c_arb     = acc[1];
  assign c_rowwork = acc[2];
  assign c_cas     = acc[3];
  assign c_burst   = acc[4];
  assign c_return  = acc[5];
  assign total     = tot;
  assign busy      = (st != S_IDLE);
  assign stage     = st;
  assign done      = dn;

  // ── THE INVARIANT. Computed in a wider accumulator so the comparison
  //    itself cannot overflow and report a false pass.
  logic [LAT_W+2:0] sum_wide;
  always_comb begin
    sum_wide = '0;
    for (int i = 0; i < 6; i++) sum_wide = sum_wide + {3'b0, acc[i]};
  end
  assign sum_ok = (sum_wide == {3'b0, tot});

  // ── Largest component. Answers "where did the time go" in one value.
  always_comb begin
    dominant = 3'd0;
    for (int i = 1; i < 6; i++)
      if (acc[i] > acc[dominant]) dominant = 3'(i);
  end

  assign err_saturated          = sat;
  assign err_out_of_order       = ooo;
  assign err_restart_while_busy = busy && ev_accept;

endmodule

Three details in that block are the chapter rather than the implementation.

The accumulators clear on ev_accept, not on done. Clearing at completion looks equivalent and is not: a request that never completes leaves stale values that the next request adds to. The breakdown then reports the sum of two requests, sums correctly against a total that is also the sum of two, and passes every property. Clearing at admission is what makes the block idempotent per request.

tot and acc[chg] saturate together. Saturating one without the other breaks the invariant exactly at the boundary where a long-latency request most needs measuring — so a design that saturated only the total would have its sum property fail precisely on the interesting requests.

sum_wide is three bits wider than LAT_W. Six accumulators each up to MAX_LAT can sum to 6 × MAX_LAT, which does not fit in LAT_W. Comparing in the narrow width would wrap and could report sum_ok on a genuinely broken decomposition.

11. A Request, Decomposed Cycle by Cycle

Stage progression with components accumulating — and the sum holding

10 cycles
Ten cycles following one request through the six latency components, with deliberately compressed stage durations so the whole life fits in ten cycles. The clock runs throughout. The stage row moves from queue for two cycles, to arbitration for one, to row work for two, to column access for two, to burst for two, and finally to done. Below it, five accumulator rows charge one cycle each to whichever component is currently active: the queue accumulator rises to two and holds, arbitration rises to one and holds, row work rises to two, the column access accumulator rises to two, and the burst accumulator rises to two. The total row counts every cycle of the request's life and reaches nine. The sum-ok row is high on every cycle, which is the block's central invariant: the five component accumulators always add up to exactly the total. Nothing here represents a nanosecond; every row is a count of clock cycles.queue wait closed at 2 cyclesqueue wait closed at 2cyclesCAS charging — row work frozen at 2CAS charging — row workfrozen at 2total 9 = 2+1+2+2+2, sum_ok heldtotal 9 = 2+1+2+2+2, sum_okheldCKstageQUEQUEARBROWROWCASCASBSTBSTDONEc_queue1222222222c_arb0011111111c_rowwork0001222222c_cas0000012222c_burst0000000122totalsum_okt0t1t2t3t4t5t6t7t8t9

The row to watch is sum_ok, high on every cycle. At cycle 8 the components are 2 + 1 + 2 + 2 + 2 = 9 and the total is 9. That is not a coincidence of the trace — it is the property §14's P1 asserts, and it is what distinguishes a decomposition from five independent timers that happen to be running.

Also worth reading: exactly one accumulator moves per cycle. When c_cas starts charging at cycle 5, c_rowwork freezes at 2 and stays there. That disjointness is §3's first property, made visible.

Stage durations are compressed for legibility. §2's real values put tRCD and CL at 11 cycles each, so a true trace of the §4 request would be 33 cycles long. The shape is what this figure shows.

12. The Distribution Tracker

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// latency_distribution -- bounded-storage shape of a latency
// distribution, plus the statistics a histogram cannot provide.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHAT IT DOES NOT MODEL:
//   - causes. It reports shape, never why.
//   - sub-bucket detail (§9's callout): within a bucket the
//     distribution is unknown, and the top bucket is open-ended.
//
// WHY A SEPARATE MAXIMUM: §9 -- the top bucket is open-ended, so the
// worst case is NOT recoverable from the histogram. A design reporting
// only buckets has lost the statistic §8 says matters most.
// ---------------------------------------------------------------------
module latency_distribution #(
  parameter int LAT_W   = 10,
  // Histogram buckets. Boundaries are POWERS OF TWO, so bucket
  // selection is a leading-one search rather than a divide -- which is
  // why the buckets are logarithmic and the resolution is coarse at
  // the tail. That is a deliberate trade, stated in §9.
  parameter int BUCKETS = 8,
  parameter int CNT_W   = 20,
  parameter int BKT_W   = (BUCKETS <= 1) ? 1 : $clog2(BUCKETS)
) (
  input  logic                clk,
  input  logic                rst_n,

  // ── One completed request.
  input  logic                sample_valid,
  input  logic [LAT_W-1:0]    sample_lat,

  // ── Shape.
  output logic [CNT_W-1:0]    bucket_count [BUCKETS],
  output logic [BKT_W-1:0]    last_bucket,

  // ── Percentile query. §9's table names a high percentile the most
  //    useful statistic; §9's callout states its resolution limit. Both
  //    are honoured here: the answer is a BUCKET, never a value.
  //    Target is a fraction over 256, so 253 is approximately the 99th
  //    percentile and no divider is needed.
  input  logic [7:0]          pctl_target_256,
  output logic [BKT_W-1:0]    pctl_bucket,
  output logic                pctl_valid,

  // ── Statistics the histogram cannot give (§9's callout).
  output logic [LAT_W-1:0]    lat_min,
  output logic [LAT_W-1:0]    lat_max,
  output logic [CNT_W-1:0]    n_samples,
  // Running sum, for a mean the CONSUMER computes -- this block does
  // not divide, because a mean needs a stated denominator and §9 says
  // the mean is the wrong summary anyway. Published so a consumer that
  // wants it must state what it divided by.
  output logic [CNT_W+LAT_W-1:0] lat_sum,

  // ── Saturation. Sticky and per-source, because a saturated bucket
  //    and a saturated sum invalidate different conclusions.
  output logic                any_bucket_saturated,
  output logic                sum_saturated,
  output logic                counts_valid,

  output logic                err_sample_while_reset
);

  initial begin
    if (BUCKETS < 2)
      $fatal(1, "latency_distribution: BUCKETS must be at least 2");
    if (LAT_W < 2)
      $fatal(1, "latency_distribution: LAT_W must be at least 2");
    if (CNT_W < 4)
      $fatal(1, "latency_distribution: CNT_W too small");
    // With power-of-two boundaries, BUCKETS-1 doublings must cover the
    // latency range or the top bucket swallows most of the data and the
    // histogram stops being informative.
    if ((BUCKETS - 1) > LAT_W)
      $fatal(1, "latency_distribution: %0d buckets exceed a %0d-bit range",
             BUCKETS, LAT_W);
  end

  // ── Bucket selection: a leading-one search. Bucket b holds latencies
  //    in [2^(b-1), 2^b), with bucket 0 holding 0 and 1, and the top
  //    bucket open-ended.
  logic [BKT_W-1:0] sel;
  always_comb begin
    sel = '0;
    for (int b = 1; b < BUCKETS; b++)
      if (sample_lat >= (LAT_W'(1) << (b - 1))) sel = BKT_W'(b);
  end

  logic [CNT_W-1:0]        bkt [BUCKETS];
  logic [LAT_W-1:0]        mn, mx;
  logic [CNT_W-1:0]        n;
  logic [CNT_W+LAT_W-1:0]  sm;
  logic                    bsat, ssat, first;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int b = 0; b < BUCKETS; b++) bkt[b] <= '0;
      // min starts at ALL ONES so the first sample always replaces it.
      // Starting at zero would pin the minimum at zero forever.
      mn    <= {LAT_W{1'b1}};
      mx    <= '0;
      n     <= '0;
      sm    <= '0;
      bsat  <= 1'b0;
      ssat  <= 1'b0;
      first <= 1'b1;
    end else if (sample_valid) begin
      // ── Bucket. Saturate rather than wrap.
      if (bkt[sel] == {CNT_W{1'b1}}) bsat <= 1'b1;
      else                           bkt[sel] <= bkt[sel] + CNT_W'(1);

      // ── Extremes. The maximum is the statistic §8 cares about and it
      //    is one compare.
      if (first || (sample_lat < mn)) mn <= sample_lat;
      if (sample_lat > mx)            mx <= sample_lat;
      first <= 1'b0;

      if (n != {CNT_W{1'b1}}) n <= n + CNT_W'(1);

      // ── Running sum, saturating. A wrapped sum produces a mean that
      //    is not merely wrong but LOWER than the truth -- flattering,
      //    which is the worst direction for a performance metric.
      if (sm > ({(CNT_W+LAT_W){1'b1}} - {{CNT_W{1'b0}}, sample_lat}))
        ssat <= 1'b1;
      else
        sm <= sm + {{CNT_W{1'b0}}, sample_lat};
    end
  end

  always_comb
    for (int b = 0; b < BUCKETS; b++) bucket_count[b] = bkt[b];

  // ── Percentile scan. A running prefix sum over the buckets, and the
  //    lowest bucket whose cumulative count reaches the target share of
  //    the samples. Combinational: BUCKETS is small and a prefix sum
  //    over it is a short adder chain, so no multi-cycle scan is needed.
  //
  //    THE COMPARISON IS DONE BY CROSS-MULTIPLICATION, not by dividing.
  //    cum/n >= target/256  becomes  cum*256 >= n*target, which is
  //    exact in integers. Dividing would truncate and bias the answer
  //    one bucket low on almost every query.
  logic [CNT_W+8:0] cum;
  logic [CNT_W+8:0] threshold;
  logic [BKT_W-1:0] pb;
  logic             pfound;

  always_comb begin
    threshold = {{9{1'b0}}, n} * {{(CNT_W+1){1'b0}}, pctl_target_256};
    cum       = '0;
    pb        = BKT_W'(BUCKETS - 1);
    pfound    = 1'b0;
    for (int b = 0; b < BUCKETS; b++) begin
      cum = cum + {9'b0, bkt[b]};
      if (!pfound && ((cum << 8) >= threshold)) begin
        pb     = BKT_W'(b);
        pfound = 1'b1;
      end
    end
  end

  assign pctl_bucket          = pb;
  // A percentile is meaningless without samples, and untrustworthy if
  // any bucket clamped -- the cumulative sum would then understate.
  assign pctl_valid           = pfound && (n != '0) && !bsat;

  assign last_bucket          = sel;
  assign lat_min              = first ? '0 : mn;
  assign lat_max              = mx;
  assign n_samples            = n;
  assign lat_sum              = sm;
  assign any_bucket_saturated = bsat;
  assign sum_saturated        = ssat;
  // Every reported figure is trustworthy only while nothing clamped.
  assign counts_valid         = !bsat && !ssat && (n != '0);

  assign err_sample_while_reset = sample_valid && !rst_n;

endmodule

The mn register initialising to all ones is the detail worth copying. A minimum initialised to zero is pinned at zero forever, reports a minimum latency of zero on every workload, and looks entirely plausible in a register dump. The first flag exists so that lat_min reads zero before any sample rather than reading all-ones, which would be equally misleading in the other direction.

The percentile query completes §9's table, and it answers with a bucket. pctl_target_256 is a fraction over 256 — so 253 is roughly the 99th percentile — and the scan returns the lowest bucket whose cumulative count reaches that share. The comparison is a cross-multiplication, cum × 256 ≥ n × target, rather than a division: dividing would truncate and bias the answer one bucket low on almost every query, which for logarithmic buckets means reporting half the true value.

And lat_sum is published without a divide on purpose. §9 argues the mean is the wrong summary; a block that computed one would be endorsing it. Publishing the sum forces the consumer to state its denominator, which is 12.4's discipline about naming denominators applied to latency.

13. The Path, in Sequence

One read request travelling through the memory system, with the six latency components marked at the boundaries that define them. The requester presents a request to the controller's ingress, which accepts it into the request queue; the interval from presentation to acceptance is the ingress component. The request then waits in the queue until the arbiter selects it, and that interval is the queue wait, the only component with no upper bound. The arbiter selects it and the scheduler commits a command, and that interval is arbitration. The scheduler issues an activate to the device if a row must be opened, and the interval until a column command may issue is row work, which is zero for a hit, one row-cycle delay for a miss, and a precharge plus a row-cycle delay for a conflict. The scheduler then issues the read, and the interval until the first data beat arrives is the column access latency. Data beats then return for the burst duration. Finally the controller delivers the data to the requester, and that interval is the return component. Every interval abuts the next exactly, which is why the six components sum to the total.Where each component begins and endsRequesterQueueArbiterSchedulerDRAMrequest presented —INGRESS endswaiting — QUEUEWAIT, unboundedselected — QUEUEWAIT endsgranted —ARBITRATION endsPRE and/or ACT ifneededROW WORK: 0, tRCD,or tRP+tRCDRD issued — ROW WORKendsCAS: CL cyclesfirst beat — CASendsBURST: BL/2 cyclesdelivered — RETURNends

The two self-messages on the DRAM lifeline are the irreducible components of §7, and the self-message on the queue is the unbounded one of §8. That is the whole chapter in one picture: the device's contributions are bounded and known, and the only interval whose length is nobody's published constant is the one the controller controls.

14. What the Assertions Prove

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bind unit note: P1-P6 reference latency_decomposer's internal state
// and are written as though bound into it, so clk and rst_n are
// visible directly. P7-P10 reference latency_distribution likewise.

// P1 -- THE property of this chapter. The components sum to the total,
// on every cycle. If this holds, the breakdown is a decomposition; if
// it does not, it is six unrelated timers.
property p_components_sum_to_total;
  @(posedge clk) disable iff (!rst_n)
    sum_ok;
endproperty
assert property (p_components_sum_to_total);

// P2 -- exactly one component charges per cycle while busy. §3's
// disjointness, and the guard against a cycle being counted twice.
property p_one_component_per_cycle;
  @(posedge clk) disable iff (!rst_n)
    (busy && !err_saturated)
      |=> ($countones({c_queue   != $past(c_queue, 1),
                       c_arb     != $past(c_arb, 1),
                       c_rowwork != $past(c_rowwork, 1),
                       c_cas     != $past(c_cas, 1),
                       c_burst   != $past(c_burst, 1),
                       c_return  != $past(c_return, 1)}) <= 1);
endproperty
assert property (p_one_component_per_cycle);

// P3 -- the total only ever grows while a request is tracked, and only
// by one. A total that could jump would mean a stage was skipped.
property p_total_monotonic_by_one;
  @(posedge clk) disable iff (!rst_n)
    (busy && !err_saturated && $past(busy, 1))
      |-> (total == $past(total, 1) + 1);
endproperty
assert property (p_total_monotonic_by_one);

// P4 -- a fresh request starts from zero. The stale-accumulator defect
// of §10, forbidden: without this, a second request reports the sum of
// two and still satisfies P1.
property p_fresh_request_starts_clean;
  @(posedge clk) disable iff (!rst_n)
    (ev_accept && !busy) |=> (total == LAT_W'(1));
endproperty
assert property (p_fresh_request_starts_clean);

// P5 -- stages advance in the fixed order of §3. An out-of-order event
// is reported and never silently mis-attributed.
property p_stage_order;
  @(posedge clk) disable iff (!rst_n)
    (stage != $past(stage, 1)) && $past(busy, 1) && busy
      |-> (stage == $past(stage, 1) + 3'd1);
endproperty
assert property (p_stage_order);

// P6 -- the saturation flag and the total's ceiling agree, so a
// clamped measurement is never reported as a real one.
property p_saturation_is_flagged;
  @(posedge clk) disable iff (!rst_n)
    (total == LAT_W'(MAX_LAT)) && busy |=> err_saturated;
endproperty
assert property (p_saturation_is_flagged);

// P7 -- the maximum never falls. A high-water mark that could decrease
// loses the event it exists to record (§9).
property p_max_monotonic;
  @(posedge clk) disable iff (!rst_n)
    (lat_max >= $past(lat_max, 1));
endproperty
assert property (p_max_monotonic);

// P8 -- the minimum never rises once established.
property p_min_monotonic;
  @(posedge clk) disable iff (!rst_n)
    ((n_samples != '0) && $past(n_samples, 1) != '0)
      |-> (lat_min <= $past(lat_min, 1));
endproperty
assert property (p_min_monotonic);

// P9 -- min bounds max. Trivial-looking, and it catches the
// all-ones-initialisation bug of §12 in both directions.
property p_min_le_max;
  @(posedge clk) disable iff (!rst_n)
    (n_samples != '0) |-> (lat_min <= lat_max);
endproperty
assert property (p_min_le_max);

// P10 -- counts_valid is only asserted when nothing has clamped, so no
// consumer can compute a ratio from a saturated count.
property p_valid_requires_no_saturation;
  @(posedge clk) disable iff (!rst_n)
    counts_valid |-> (!any_bucket_saturated && !sum_saturated);
endproperty
assert property (p_valid_requires_no_saturation);

// P11 -- the percentile bucket is monotonic in the target. A higher
// target can never select a LOWER bucket, which is the one structural
// property a percentile must have and the one a truncating divide
// breaks.
property p_percentile_monotonic;
  @(posedge clk) disable iff (!rst_n)
    (pctl_valid && $past(pctl_valid, 1)
       && (pctl_target_256 >= $past(pctl_target_256, 1))
       && (n_samples == $past(n_samples, 1)))
      |-> (pctl_bucket >= $past(pctl_bucket, 1));
endproperty
assert property (p_percentile_monotonic);

// P12 -- a percentile is never reported from a clamped histogram, where
// the cumulative sum would understate and bias the answer low.
property p_percentile_needs_clean_counts;
  @(posedge clk) disable iff (!rst_n)
    pctl_valid |-> !any_bucket_saturated;
endproperty
assert property (p_percentile_needs_clean_counts);

// ── Cover.
cover property (@(posedge clk) disable iff (!rst_n) done);
// A high percentile landing in the open-ended top bucket: the answer
// is "at least this much" and §9's callout says so.
cover property (@(posedge clk) disable iff (!rst_n)
                  pctl_valid && (pctl_bucket == BKT_W'(BUCKETS - 1)));
// A row HIT: row work charged zero cycles. The decomposition must
// handle a component of length zero, which is the case a naive
// implementation with a minimum-one-cycle stage gets wrong.
cover property (@(posedge clk) disable iff (!rst_n)
                  done && (c_rowwork == '0));
// A CONFLICT-shaped request: row work at least twice the CAS
// component, which at §2's parameters means a PRE preceded the ACT.
cover property (@(posedge clk) disable iff (!rst_n)
                  done && (c_rowwork >= (c_cas << 1)) && (c_cas != '0));
// Queue wait dominant -- §8's case, and the one that says the device
// is not the problem.
cover property (@(posedge clk) disable iff (!rst_n)
                  done && (dominant == 3'd0));
// Row work dominant -- the locality-bound case of 23.3.
cover property (@(posedge clk) disable iff (!rst_n)
                  done && (dominant == 3'd2));
cover property (@(posedge clk) disable iff (!rst_n) err_out_of_order);
cover property (@(posedge clk) disable iff (!rst_n) err_saturated);
// A sample landing in the open-ended top bucket: §9's resolution limit
// actually exercised.
cover property (@(posedge clk) disable iff (!rst_n)
                  sample_valid && (last_bucket == BKT_W'(BUCKETS - 1)));

P1 is the property worth keeping if only one survives. It is the definition of a decomposition, and it fails immediately for the most natural wrong implementation — six independently started timers, which produce six plausible numbers that do not add up to anything.

P4 is the one most likely to be thought redundant. It is not: a stale-accumulator implementation satisfies P1 perfectly, because the components and the total are both the sum of two requests. Only P4 distinguishes them.

15. Corner Cases

CaseBehaviourWhy
Row hit — zero row workc_rowwork == 0, sum still holdsA zero-length component is legal; §14's second cover requires it
Request completing in the minimum 15 cyclesValid; c_queue and c_arb both zero§6's floor — CL + burst only
Latency exceeding MAX_LATTotal clamps, err_saturated stickyA wrapped latency reads as small — the flattering direction
Saturation mid-requesttot and acc[chg] clamp togetherThe invariant survives saturation rather than breaking at it
ev_accept while busyerr_restart_while_busyTwo overlapping requests need two instances
Out-of-order stage eventerr_out_of_order; no mis-attributionThe stages are ordered by §3's construction
Request that never completesAccumulators hold; next ev_accept clears themClearing at admission rather than completion — §10
First sample into the distributionlat_min replaced from all-onesA zero-initialised minimum is pinned at zero forever
No samples yetlat_min reads 0, counts_valid lowAll-ones would be as misleading as zero
A bucket counter clampingany_bucket_saturated, counts_valid lowEvery ratio from the set is then wrong
BUCKETS − 1 > LAT_W$fatal at elaborationThe top bucket would swallow the data and the histogram would say nothing
All samples in one bucketLegal; the distribution is degenerateNot an error, and a strong signal the bucketing is wrong for this workload

Row four is the one worth dwelling on. Saturating the total without saturating the component being charged breaks P1 exactly at the boundary where a long request most needs measuring — so the property would fail only on the interesting requests, which is the worst possible place for a measurement instrument to become unreliable.

16. DV — An Independent Timeline

The checker must not re-run the state machine, or it agrees with the DUT's bugs. What it should do is build a timeline from the events alone and compare the resulting intervals.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Independent timeline reconstruction. Records the cycle at which each
// event occurred, then computes the components by SUBTRACTION -- a
// different method from the DUT's per-cycle accumulation, so agreement
// is evidence rather than tautology.
// SIMULATION-ONLY.
class latency_timeline;
  int unsigned t_accept, t_selected, t_committed;
  int unsigned t_col, t_first, t_last, t_delivered;

  function int unsigned comp_queue();   return t_selected  - t_accept;    endfunction
  function int unsigned comp_arb();     return t_committed - t_selected;  endfunction
  function int unsigned comp_rowwork(); return t_col       - t_committed; endfunction
  function int unsigned comp_cas();     return t_first     - t_col;       endfunction
  function int unsigned comp_burst();   return t_last      - t_first;     endfunction
  function int unsigned comp_return();  return t_delivered - t_last;      endfunction
  function int unsigned total();        return t_delivered - t_accept;    endfunction

  // The reference's own sanity check: subtraction must be exhaustive.
  function bit intervals_partition();
    return (comp_queue() + comp_arb() + comp_rowwork() + comp_cas()
          + comp_burst() + comp_return()) == total();
  endfunction
endclass
CheckWhat it establishes
Every component matches the DUT's accumulatorAccumulation agrees with subtraction — two methods, one answer
intervals_partition() holds for every requestThe reference is itself exhaustive before being used as one
Drive §4's configuration: expect 11 / 11 / 4 for row work, CAS, burstThe verified parameters reproduced from the RTL
Drive §5's conflict: expect row work 22, total +11 over the missThe tRP difference, independently computed
Drive §6's hit: expect row work 0, total 22The zero-length component
Zero-length queue wait (selected on the accept cycle)The other zero-length case
Latency beyond MAX_LAT: expect clamp and sticky flag, and P1 still holdingSaturation does not break the invariant
Two back-to-back requests: expect the second's total to match its own timelineThe stale-accumulator defect would double it
Feed a known latency multiset; check lat_max against the true maximumThe statistic §8 cares about
Feed the same multiset in a different order; expect identical statisticsOrder independence of min, max, sum and buckets

The eighth check is the one that catches the defect P4 exists for:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  STALE-ACCUMULATOR DEFECT — TWO REQUESTS, ONE BREAKDOWN
    MAX_LAT = 1023. Two identical row-miss reads, back to back.
    §4's configuration: queue 6, arb 1, rowwork 11, cas 11, burst 4.

    request A : timeline gives 6 / 1 / 11 / 11 / 4  total 33
    request B : timeline gives 6 / 1 / 11 / 11 / 4  total 33

    DUT with accumulators cleared at DONE (the natural choice):
      request A reports 6 / 1 / 11 / 11 / 4  total 33      CORRECT
      request B reports 6 / 1 / 11 / 11 / 4  total 33      CORRECT

    DUT with accumulators cleared NOWHERE (the defect):
      request A reports 6 / 1 / 11 / 11 / 4  total 33      CORRECT
      request B reports 12 / 2 / 22 / 22 / 8  total 66     WRONG

    and note what still passes on request B:
      sum_ok    : 12+2+22+22+8 = 66 == total.  P1 HOLDS.
      P2        : still one component per cycle. HOLDS.
      P3        : total still grew by one per cycle. HOLDS.
      P5        : stages still advanced in order. HOLDS.
      dominant  : still reports rowwork. PLAUSIBLE.

    diagnosis : every arithmetic property is satisfied because the
                breakdown is internally consistent -- it is the
                breakdown of a REQUEST THAT DOES NOT EXIST, namely
                A and B concatenated.
    caught by : P4 (total == 1 on the cycle after a fresh accept) and
                by this check comparing against a per-request timeline.
    why it matters : the reported latency is exactly DOUBLE, which on
                a real system looks like a genuine performance problem
                and sends the investigation to the arbiter.

That report is the most useful thing this testbench produces. A sound-looking decomposition that satisfies four of five properties and describes a request that never happened is a harder bug to find than one that produces obvious nonsense — and it would be reported as a latency regression rather than as a measurement defect.

17. Debugging

SymptomLikely causeHow to confirm
Reported latency is roughly double expectationStale accumulators — §16P4; compare two consecutive requests against a timeline
Minimum latency reads 0 on every workloadmn initialised to zero — §12P9; a minimum below CL + burst is impossible (§6)
Mean looks fine, users report stallsThe tail — §8's calloutRead lat_max, not the mean
lat_max far above the meanQueue wait dominant — §8dominant == 0; the device is not the problem
Row work dominant across many requestsLocality-bound — 23.3Compare against 9.6's class counts
Components do not sumIndependent timers, not a decompositionP1; this is the defining defect
Latency improved but throughput did notLatency and bandwidth are different questions23.2 — a reordering can cut latency and lose slots
err_out_of_order firingEvent wiring, or overlapping requestsThe stages are ordered; one instance tracks one request
A histogram with everything in one bucketBucketing wrong for this workloadNot an error; rescale BUCKETS or LAT_W
counts_valid lowSomething clamped — §12Ratios from a saturated set are wrong in a flattering direction
Latency rises with no code changeAnother requester's queueing — §8Queue wait is a shared resource; the tail moves first
Device components vary between requestsShould be impossible at fixed configurationCL and burst are constants; a varying c_cas is a wiring fault

Row twelve is the one that indicates a real bug rather than a performance question. CL and the burst length are configuration constants, so c_cas and c_burst must be identical on every request in a fixed configuration. A varying c_cas means the event marking the first beat is not what the block thinks it is.

Row seven is the most common conceptual error in memory performance work, and 23.2 opens on it: reducing latency and increasing throughput are different objectives and sometimes conflict.

18. Misconceptions

“Memory latency is CL.” §4. CL is 11 of 33 cycles on a row miss under §2's configuration — a third. On a conflict it is 11 of 44, barely a quarter.

“A timing checker tells you where the time goes.” §1. 10.5 reports a verdict and is silent when everything is legal. A decomposition always has an answer.

“The components are approximate.” §3. They abut exactly by construction, so they sum exactly — P1 asserts it, and a breakdown that only approximately sums is six timers.

“A conflict costs about double a miss.” §5. It costs exactly one tRP more — 11 cycles at this bin. The doubling in §6 is an artefact of tRP = tRCD = CL at this grade, not a general truth.

“Most of a request's latency is the controller's fault.” §4. Under §2's configuration, 26 of 33 cycles on a row miss are device timing. Eliminating all queueing improves that request by 21%.

“Row work is irreducible.” §7. Irreducible per access; how often it is paid is a policy outcome, which is why 23.5 exists.

“Latency has a worst case you can look up.” §8. Queue wait has no upper bound in any specification. A bound must be designed into the arbitration policy.

“The mean summarises the distribution.” §9. The distribution is right-skewed by construction — a constant plus something bounded below and unbounded above — so the mean describes neither the mode nor the tail.

“A histogram gives you the maximum.” §9's callout. The top bucket is open-ended, which is why §12 keeps a separate register for it.

“A percentile from a histogram is a number.” §9's callout. It is the bucket containing the percentile, and for logarithmic buckets that can be a wide interval.

“If the components sum, the measurement is right.” §16. A stale-accumulator implementation sums perfectly and describes a request that never existed.

“Lower latency means higher throughput.” §17's row seven, and 23.2's opening. They are different objectives and can conflict.

19. Interview Reasoning

Decompose a row-miss read at DDR4-1600K. Queue wait plus arbitration, then tRCD of 11 cycles, CL of 11, and a BL8 burst of 4 — 26 cycles of device time plus whatever the controller added. With 6 cycles of queueing and 1 of arbitration, 33 cycles total, or 41.25 ns.

What does a conflict add, exactly? One tRP. The row must be closed before the new one opens, and the two are serialised — so 11 more cycles at this bin, making 44. Not approximately: exactly tRP.

Which component has no upper bound? Queue wait. Every other component is a published constant or a fixed pipeline depth. That is why the worst-case latency of a memory system is a property of its arbitration policy rather than of the DRAM.

So where does all the variance in a latency distribution come from? Queue wait, entirely. The device components are constant at a fixed configuration, so a latency histogram is a queue-wait histogram shifted by a constant.

Why is the mean a poor summary here? Because the distribution is a constant plus a quantity bounded below by zero and unbounded above — right-skewed by construction. The mean sits between the mode and the tail and describes neither.

What is the cheapest high-value statistic to add? The maximum. One register and one comparison per request. It cannot be diluted by fast requests, and it answers the worst-case question the mean hides.

Why keep a maximum separately if you already have a histogram? Because the top bucket is open-ended, so the histogram cannot recover the worst case. Reporting only buckets loses the statistic that matters most.

Your breakdown's components do not add up to the total. What have you built? Six independent timers, not a decomposition. The likely cause is starting a device-timing timer at request arrival rather than at the command, which double-counts the queue wait into the device's column.

The breakdown sums perfectly and every latency is exactly double expectation. What is wrong? The accumulators are not cleared per request, so each breakdown is the sum of that request and the previous one. It is internally consistent, satisfies the sum property, and describes a request that never existed.

Given the decomposition, where would you look first to improve latency? At which component dominates. If queue wait dominates, the arbitration policy is the lever. If row work dominates, locality and page policy are. If neither does, the latency is near the configuration's floor of CL + burst and the answer is that this workload is device-bound.

20. Exercises

  1. Recompute §4's, §5's and §6's totals for DDR4-1866M — CL-nRCD-nRP = 13-13-13, tCK = 1.071 ns, tRAS = 34 ns. Report each total in cycles and nanoseconds, and state which of the three classes improved most in nanoseconds. Was the improvement uniform?

  2. §6 notes that a conflict is exactly twice a hit at DDR4-1600K, and calls it an artefact. Find a real DDR4 speed bin where it is not twice, and state the general expression for the ratio in terms of tRP, tRCD, CL and burst length.

  3. Remove the first flag from §12's block and describe what lat_min reports before the first sample and after it. Which of P8 and P9 catches the result, and which does not?

  4. §10's accumulators clear on ev_accept. Change them to clear on done and construct the request sequence that produces a wrong breakdown anyway. What does its existence say about where idempotence should be enforced?

  5. sum_wide is three bits wider than LAT_W. Prove three is sufficient for six accumulators, and state the minimum extra width for a decomposition with N components.

  6. Write the property that would bound queue wait, given an additional input carrying a starvation limit. Then explain from §8 why this chapter cannot assert it and 23.4 can.

  7. §12 publishes lat_sum without dividing. Argue both sides, then state what a consumer must report alongside a mean for the figure to be meaningful, referring to 12.4 §2's denominators.

  8. Using §2's verified parameters only, compute the fraction of a row-miss read's latency that is irreducible under §7's strictest definition — CL plus burst alone. Then compute it for a hit. Explain why the two fractions differ and what that implies about which workloads benefit from a better scheduler.

21. Where This Goes

A request's latency is now a decomposition rather than a number: six disjoint, exhaustive components that sum exactly, with the device's contributions known constants and exactly one component unbounded.

Three results carry forward. Device time dominates a single access — 26 of 33 cycles on a row miss under §2's configuration, so a perfect scheduler improves that request by a fifth. Row work is the only component that varies across access classes, and its variation is exactly tRCD and tRP, which means locality's cost is computable from class counts without measuring latency at all. And queue wait is unbounded, so the worst case is a policy property that has to be designed.

What this chapter deliberately did not ask is whether any of it affects throughput. A request's latency and the interface's utilisation are different quantities, and §17's row seven flagged that they can move in opposite directions: a scheduler that reorders requests to fill more data slots makes some individual requests wait longer.

Chapter 23.2 takes that up. It starts from the peak figure §2 derived — 12.8 GB/s, 16 bytes per cycle — and asks where the difference goes, by building the accounting 12.4 named and did not construct: every cycle on the data bus charged to exactly one named cause, with the categories provably exhaustive. That is measure D, and it is the only one of 12.4's four measures that can see refresh, turnaround and row work at once.

Continue learning

Standards & specifications

Governing standard
JEDEC JESD79 (DDR SDRAM)(opens JEDEC Solid State Technology Association in a new tab)

Defines the DDR SDRAM device itself — signals, command encoding, mode registers, timing parameters and the initialisation sequence — one document per generation. Memory-controller microarchitecture, address-mapping policy, PHY training algorithms and board-level design are not specified by it.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the DDR curriculum.