DDR · Module 9
Performance Impact
Two request streams with identical addresses and counts can demand more than twice the row-state work, decided only by their order — and the instrumentation that measures it lies in specific, recognisable ways.
Chapters 9.3 to 9.5 established three classes and the work each requires: a hit needs zero row-state transitions, a miss one, and a conflict two, serialised.
Those three numbers are enough to answer this chapter's question without a single timing parameter:
How does the mix of classes in a request stream change the work the memory system must do?
The answer is larger than most people expect — two streams with identical addresses and identical request counts can differ by more than a factor of two in required work, decided entirely by their order. And the second half of the chapter is about the instrumentation that measures this, because a class counter lies in specific, recognisable ways, and a report nobody distrusts is a report nobody checks.
1. Work, Not Time
The temptation here is to assign costs in nanoseconds. This chapter will not, and the reason is not caution.
A timing figure is true of one device, at one speed bin, under one set of conditions. Quote a conflict's cost in nanoseconds and the statement expires — it was never true of every part, and it stops being true of any part as soon as the configuration changes. This chapter therefore quotes no such figure, including as an illustration, because a number in a tutorial is quoted back as a fact.
A transition count does not expire. A conflict requires a precharge and an activate, serialised, on every DDR device that has ever existed and on every one that will. The count is structural, derived in Chapter 9.5 §2 from the fact that a bank holds one row.
So the model is:
EDUCATIONAL TRANSITION-COUNT MODEL
hit → 0 row-state transitions
miss → 1 transition (activate)
conflict → 2 transitions (precharge, then activate — serialised)
work(stream) = 1 × misses + 2 × conflictsThis is not a latency equation and it is not a JEDEC relationship. It counts one thing — required row-state transitions — and deliberately ignores everything else the memory system does. §4 is about what it leaves out.
2. Two Streams, Same Requests, Different Work
The derivation is best done concretely. Take eight requests across two banks and two rows each — the same eight addresses in both streams, differing only in order. Both banks start closed.
Stream A — grouped by row:
| # | Request | Bank state on arrival | Class | Transitions |
|---|---|---|---|---|
| 1 | b0 r0 | closed | MISS | 1 |
| 2 | b0 r0 | OPEN(r0) | HIT | 0 |
| 3 | b0 r1 | OPEN(r0) | CONFLICT | 2 |
| 4 | b0 r1 | OPEN(r1) | HIT | 0 |
| 5 | b1 r0 | closed | MISS | 1 |
| 6 | b1 r0 | OPEN(r0) | HIT | 0 |
| 7 | b1 r1 | OPEN(r0) | CONFLICT | 2 |
| 8 | b1 r1 | OPEN(r1) | HIT | 0 |
Totals: 4 hits, 2 misses, 2 conflicts. Work = 2 × 1 + 2 × 2 = 6 transitions. Hit rate 50%.
Stream B — alternating rows:
| # | Request | Bank state on arrival | Class | Transitions |
|---|---|---|---|---|
| 1 | b0 r0 | closed | MISS | 1 |
| 2 | b0 r1 | OPEN(r0) | CONFLICT | 2 |
| 3 | b0 r0 | OPEN(r1) | CONFLICT | 2 |
| 4 | b0 r1 | OPEN(r0) | CONFLICT | 2 |
| 5 | b1 r0 | closed | MISS | 1 |
| 6 | b1 r1 | OPEN(r0) | CONFLICT | 2 |
| 7 | b1 r0 | OPEN(r1) | CONFLICT | 2 |
| 8 | b1 r1 | OPEN(r0) | CONFLICT | 2 |
Totals: 0 hits, 2 misses, 6 conflicts. Work = 2 × 1 + 6 × 2 = 14 transitions. Hit rate 0%.
This is the chapter's central derivation, and note what it did not require: no timing parameter, no device, no speed bin, no bandwidth figure. The class distribution alone determined the answer, and that is why the distribution is worth measuring.
Where the distribution comes from is a chain this curriculum has already built: the workload's access pattern, transformed by Module 8's address map into a bank and row sequence, met by whatever Module 17's page policy left the banks in. Each stage is owned elsewhere. This chapter's contribution is the reason the chain matters.
3. Work Is Not the Only Thing That Matters
The model counts one thing well and is silent about several others. Being explicit about what it omits is what keeps it from being misused.
It ignores parallelism. Six transitions across six banks can proceed concurrently; six in one bank cannot. The model counts the same six either way. Chapter 5.2 established bank independence and Module 16 owns its exploitation.
It ignores the shared column path. Chapter 5.3 established that banks in a group share one, so two hits in a group contend where two hits in different groups do not. A hit's zero transitions are not a claim that the access is free.
It ignores everything except row state. Refresh occupies banks — Chapter 7.5 — and so does calibration. Neither appears in the count.
It ignores the column access itself. Every request, whatever its class, still issues a column command and still moves data. The model counts the difference between classes, not the total cost of an access.
And it ignores duration. A transition is one unit regardless of how long it actually takes, which is the deliberate abstraction of §1 and the reason Modules 13 and 14 are needed to turn this into time.
4. Why a Hit Rate Alone Is Not Enough
A single "hit rate" is the near-universal way this is reported, and it discards the distinction the module spent two chapters establishing.
Consider two streams that both report a 50% hit rate over 100 requests:
Stream P: 50 hits, 50 misses, 0 conflicts
work = 50 × 1 + 0 × 2 = 50 transitions
Stream Q: 50 hits, 0 misses, 50 conflicts
work = 0 × 1 + 50 × 2 = 100 transitionsThe same hit rate. Twice the work. And the two have different fixes: stream P's misses are often a page-policy question — banks are being closed that could have been left open — while stream Q's conflicts are usually an address-mapping question, since two hot rows are landing in one bank.
So the reporting requirement is simple and rarely met: report hits, misses and conflicts as three separate counts, all the way to the dashboard. A blended rate cannot distinguish a policy problem from a mapping problem, and those go to different engineers.
This is the same argument this repository's CXL memory-controller chapter makes — that a blended rate hides which of the two failures dominates — and it is worth noting that the conclusion was reached independently there, on different hardware, for the same structural reason.
5. RTL — Saturating Class Telemetry
The engineering problem
Count classified requests by class and accumulate the transition work they demand — and be unable to report a flattering number when the counters can no longer hold the truth.
Why hardware needs it
Class distribution is the primary diagnostic for everything in this module, and it cannot be reconstructed after the fact from a trace of any useful length. It has to be counted as it happens, which means in hardware, cheaply.
Classification
SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL. Instrumentation, level C.
What it models
Saturating counts of each request class, a saturating accumulator of demanded row-state transitions, and an accounting-validity flag that goes false the moment saturation makes the counts stop adding up.
What it does NOT model
Classification (Chapter 9.3) or planning (Chapter 9.5) — both consumed. Timing, duration, throughput, latency or bandwidth (Modules 13, 14 and 23). Parallelism (Module 16). Refresh (Module 15). And it cannot verify that each classification corresponds to one request — §5's limitation, and the source of the most common telemetry bug.
Interface and parameter contract
// ─────────────────────────────────────────────────────────────────────────
// row_class_counters
//
// Classification: SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL (instrumentation).
//
// MODELS: saturating counts per request class, a saturating accumulator of
// DEMANDED row-state transitions, and an accounting-validity flag.
//
// COUNTERS SATURATE, NEVER WRAP. A wrapped counter reports a FLATTERING
// ratio, and an instrument that errs toward what its reader hopes for is
// worse than no instrument. Same argument as Chapter 5.4's channel
// telemetry and Chapter 5.3's classifier.
//
// NO SCORE, NO RATIO, NO DIVIDER. Counts are reported; ratios are computed
// by whoever knows what they mean.
//
// NOT A PERFORMANCE MODEL. It counts row-state work only -- not
// parallelism (Module 16), not the shared column path (Chapter 5.3), not
// refresh (Module 15), not duration (Modules 13, 14), not throughput
// (Module 23). Chapter 9.6 Section 3 lists the omissions.
//
// MODELS NO PHYSICAL OR ANALOG BEHAVIOUR.
// ─────────────────────────────────────────────────────────────────────────
module row_class_counters #(
// Width of every counter. Sized for the expected run length: Section 9
// is about what happens when it is not.
parameter int ACC_W = 24
) (
input logic clk,
input logic rst_n,
// ── Class strobes from Chapter 9.3, already gated on req_valid there.
// One at a time -- that classifier's P3 proves it.
input logic hit,
input logic miss,
input logic conflict,
input logic busy,
input logic bad_bank,
// ── Committed, meaning this classification is being ACTED ON rather
// than merely observed. THE CALLER MUST ASSERT THIS ONCE PER REQUEST.
// A request re-presented every cycle while its bank transitions would
// otherwise be counted once per cycle, and Section 9 is about
// recognising that from the numbers.
input logic committed,
output logic [ACC_W-1:0] cnt_total,
output logic [ACC_W-1:0] cnt_hit,
output logic [ACC_W-1:0] cnt_miss,
output logic [ACC_W-1:0] cnt_conflict,
output logic [ACC_W-1:0] cnt_busy,
output logic [ACC_W-1:0] cnt_bad,
// Section 1's model: 0 for a hit, 1 for a miss, 2 for a conflict.
output logic [ACC_W-1:0] cnt_transitions,
// Sticky. Once any counter has clamped, every ratio derived from these
// numbers is wrong in a FLATTERING direction.
output logic any_saturated,
// The accounting invariant of Section 7 holds only while this is high.
output logic accounting_valid
);
if (ACC_W < 2) begin : g_aw
initial $fatal(1, "row_class_counters: ACC_W must be >= 2");
end
localparam logic [ACC_W-1:0] MAXV = {ACC_W{1'b1}};
// ── Saturating add. Returns the clamped value and reports whether it
// clamped, because the clamp is the interesting event rather than a
// silent detail.
function automatic logic [ACC_W:0] sat_add
(input logic [ACC_W-1:0] v, input logic [2:0] inc);
logic [ACC_W:0] sum;
begin
// A size cast, NOT a zero-padding concatenation: {{(ACC_W-2){1'b0}}, inc}
// is a zero-width replication when ACC_W == 2, which is illegal -- and
// ACC_W == 2 is an allowed parameter (see Corner cases).
sum = {1'b0, v} + (ACC_W+1)'(inc);
// Bit ACC_W of the result is the CLAMPED flag; the rest is the value.
sat_add = sum[ACC_W] ? {1'b1, MAXV} : {1'b0, sum[ACC_W-1:0]};
end
endfunction
logic [2:0] work_inc;
always_comb begin
// Section 1's model, and the only place these three numbers appear.
// BUSY contributes nothing: a request waiting demands no NEW work, and
// counting it would charge the same conflict twice.
work_inc = 3'd0;
if (committed) begin
if (miss) work_inc = 3'd1;
else if (conflict) work_inc = 3'd2;
end
end
logic [ACC_W:0] n_total, n_hit, n_miss, n_conf, n_busy, n_bad, n_work;
always_comb begin
n_total = sat_add(cnt_total, (committed && (hit || miss || conflict
|| busy || bad_bank)) ? 3'd1 : 3'd0);
n_hit = sat_add(cnt_hit, (committed && hit) ? 3'd1 : 3'd0);
n_miss = sat_add(cnt_miss, (committed && miss) ? 3'd1 : 3'd0);
n_conf = sat_add(cnt_conflict, (committed && conflict) ? 3'd1 : 3'd0);
n_busy = sat_add(cnt_busy, (committed && busy) ? 3'd1 : 3'd0);
n_bad = sat_add(cnt_bad, (committed && bad_bank) ? 3'd1 : 3'd0);
n_work = sat_add(cnt_transitions, work_inc);
end
always_ff @(posedge clk) begin
if (!rst_n) begin
cnt_total <= '0;
cnt_hit <= '0;
cnt_miss <= '0;
cnt_conflict <= '0;
cnt_busy <= '0;
cnt_bad <= '0;
cnt_transitions <= '0;
any_saturated <= 1'b0;
end else begin
cnt_total <= n_total[ACC_W-1:0];
cnt_hit <= n_hit [ACC_W-1:0];
cnt_miss <= n_miss [ACC_W-1:0];
cnt_conflict <= n_conf [ACC_W-1:0];
cnt_busy <= n_busy [ACC_W-1:0];
cnt_bad <= n_bad [ACC_W-1:0];
cnt_transitions <= n_work [ACC_W-1:0];
// STICKY. A run that saturated for one cycle produced wrong ratios
// for the whole run, and clearing the flag would hide that.
if (n_total[ACC_W] || n_hit[ACC_W] || n_miss[ACC_W] || n_conf[ACC_W]
|| n_busy[ACC_W] || n_bad[ACC_W] || n_work[ACC_W])
any_saturated <= 1'b1;
end
end
assign accounting_valid = !any_saturated;
endmoduleState representation and transitions
Seven counters and one sticky flag. Every counter moves only on committed, and the transition accumulator moves only for a miss or a conflict.
busy is counted as a class but contributes zero work, and that asymmetry is the single most important line in the block. A request waiting on a transition demands no new work — the work was already counted when the conflict or miss was first committed. Charging it again would count one conflict as two, three, or twenty, depending on how long the transition took, and §9 is about spotting exactly that from the numbers.
Combinational behaviour
Seven saturating adds and a small work decode. The sat_add function returns an extra top bit carrying the clamp flag, so the clamp is a value the design can act on rather than a condition inferred by comparison.
Sequential behaviour and reset
Nonblocking throughout. Every counter resets to zero and any_saturated resets low. The flag is sticky within a run: once any counter has clamped, every ratio derived from the set is wrong, and clearing the flag would hide that a run's numbers are unusable.
Cycle-by-cycle example
ACC_W = 24, a five-request stream:
| Request | Class | cnt_total | cnt_hit | cnt_conflict | cnt_transitions |
|---|---|---|---|---|---|
| 1 | HIT | 1 | 1 | 0 | 0 |
| 2 | HIT | 2 | 2 | 0 | 0 |
| 3 | CONFLICT | 3 | 2 | 1 | 2 |
| 4 | MISS | 4 | 2 | 1 | 3 |
| 5 | HIT | 5 | 3 | 1 | 3 |
Five requests, three transitions of demanded work. Note that requests 1, 2 and 5 moved cnt_total and left cnt_transitions alone — which is the model working: a hit is a request that costs no row-state work.
How to simulate, and expected output
Drive class strobes with committed and check all seven counters plus both flags. Then the cases that matter:
Check the accounting invariant — cnt_total == cnt_hit + cnt_miss + cnt_conflict + cnt_busy + cnt_bad — after every request, and confirm it holds exactly while accounting_valid is high.
Drive past saturation deliberately. With a small ACC_W — 4, say — run enough requests to clamp a counter and confirm any_saturated asserts, stays asserted, and that accounting_valid goes low. This test is usually skipped and it is the one that matters, because a design that has never been saturated in simulation will saturate in the lab.
Assert busy repeatedly with committed high and confirm cnt_transitions does not move. This is the double-counting guard.
Assert committed low with class strobes high and confirm nothing moves at all.
ACC_W = 2 is the minimum and clamps after three increments — a fast way to exercise every saturation path.
Expected waveform
§6, which shows the counters and the work accumulator advancing over a mixed stream.
Synthesis implications
Seven ACC_W-bit registers with saturating adders — at ACC_W = 24, 168 flops and seven 24-bit adders. This is the most expensive block in the module and it is still small, which is the argument for keeping it: instrumentation that costs a few hundred flops and answers the first question of every performance investigation is cheap at any price.
Corner cases
ACC_W = 2 is the minimum, clamps quickly, and is the right configuration for testing saturation. ACC_W = 1 does not elaborate — a one-bit counter cannot express the invariant meaningfully. Two class strobes high at once would break the invariant; it cannot happen, because Chapter 9.3's P3 proves the strobes are one-hot, and this block depends on that proof rather than re-checking it. committed high with no class strobe increments nothing, including cnt_total, which is correct: a request with no classification is not a request.
Failure modes and debugging clues
cnt_total exceeding the known request count means committed is asserting more than once per request — §9's first mechanism. cnt_transitions far exceeding cnt_miss + 2 × cnt_conflict means busy is contributing work. any_saturated high invalidates every ratio in the report and should be checked before anything else.
Limitations
It cannot verify that committed pulses once per request — that is the caller's contract, it is the most common telemetry bug in this area, and no property inside this block can detect it. It counts classes, not requests. It has no per-bank breakdown, which §9 and Chapter 9.4 §8 both want — a real design would instantiate it per bank or add a bank dimension. And it measures demand, not performance.
6. A Request Stream, in Cycles
row_class_counters — counting classes and the work they imply
10 cyclesCycles 1 and 2 are two hits, and cnt_transitions stays at zero. A hit is a request that demanded no row-state work — the whole value of the class in one row of a waveform.
Cycle 3's conflict adds two to the accumulator in a single step, because the model charges the demanded work at the moment the class is committed rather than as the commands are issued.
Cycle 4 is the one that teaches. A BUSY classification is counted in cnt_busy and cnt_total — and cnt_transitions does not move. The request waiting there is very likely the conflict from cycle 3, still waiting for its precharge. Its work was already charged. Counting it again would inflate the conflict's cost by however many cycles it happened to wait, which would make a slow system look like a busy one.
By cycle 9: six classified requests, three transitions of demanded work. The two numbers answer different questions, and reporting only the first is how a conflict-bound workload gets described as "six requests."
accounting_valid is high throughout, so the invariant in §7 holds and the numbers can be trusted. That row belongs on the waveform precisely because its absence is what §9 is about.
REPRESENTATIVE EDUCATIONAL CYCLES. No interval here corresponds to any DDR timing parameter, and the one-cycle spacing between requests implies no request rate.
7. Five Assertions Worth Writing
// P1 -- THE ACCOUNTING INVARIANT, and note the guard. Every classified
// request lands in exactly one class, so the classes must sum to the
// total -- but ONLY while nothing has saturated. Stating it
// unconditionally would produce a property that fires legitimately on a
// long run and trains people to waive it.
property p_classes_sum_to_total;
@(posedge clk) disable iff (!rst_n)
accounting_valid
|-> (cnt_total == (cnt_hit + cnt_miss + cnt_conflict
+ cnt_busy + cnt_bad));
endproperty
assert property (p_classes_sum_to_total);
// P2 -- the work accumulator matches Section 1's model exactly. This is
// the property that catches BUSY contributing work, which is the
// double-counting bug of Section 9 and the one that inflates a conflict's
// cost by its waiting time.
property p_work_matches_the_model;
@(posedge clk) disable iff (!rst_n)
accounting_valid
|-> ((ACC_W+2)'(cnt_transitions)
== ((ACC_W+2)'(cnt_miss) + 2 * (ACC_W+2)'(cnt_conflict)));
endproperty
assert property (p_work_matches_the_model);
// P3 -- counters never decrease. A counter that falls means it wrapped,
// and this catches a saturating adder that does not, independently of
// whether the saturation flag was wired correctly.
property p_counters_are_monotonic;
@(posedge clk) disable iff (!rst_n)
(cnt_total >= $past(cnt_total))
and (cnt_transitions >= $past(cnt_transitions))
and (cnt_conflict >= $past(cnt_conflict));
endproperty
assert property (p_counters_are_monotonic);
// P4 -- saturation is sticky and honest. Once the numbers have stopped
// adding up they must never silently start being trusted again.
property p_saturation_is_sticky;
@(posedge clk) disable iff (!rst_n)
$past(any_saturated) |-> any_saturated && !accounting_valid;
endproperty
assert property (p_saturation_is_sticky);
// P5 -- nothing is counted without a commit. The gate that stops idle
// cycles becoming misses, which Chapter 9.3 Section 9 lists as a cause of
// a hit rate that is too good.
property p_no_count_without_commit;
@(posedge clk) disable iff (!rst_n)
!committed |=> (cnt_total == $past(cnt_total))
&& (cnt_transitions == $past(cnt_transitions));
endproperty
assert property (p_no_count_without_commit);What these prove. P1 is the accounting invariant, and its accounting_valid guard is the interesting part — an invariant that fires legitimately is an invariant that gets waived, so it is scoped to the conditions under which it is actually a claim. P2 pins the work model to the counts, catching the double-count that inflates conflicts by their waiting time. P3 catches a non-saturating adder independently of the flag. P4 keeps the honesty sticky. P5 gates everything on a commit.
What they do not prove. Nothing here proves committed pulses once per request — that is the caller's contract and it is the most common bug in this area, invisible from inside the block. Nothing proves the classification was right: the strobes are inputs, so Chapter 9.3's properties are what establish them. Nothing here is a performance claim — the counters measure demanded row-state work, and §3 lists everything that turns demand into time. And nothing proves the numbers are useful: a perfectly accurate count over a workload that does not represent production tells you about the workload.
8. When a High Hit Rate Means Poor Performance
The model has been careful to say it measures demand rather than performance. Here is the case that makes the distinction concrete, and it is not a corner case — it is a common outcome of an address-mapping choice.
Two configurations, same workload, same device.
Configuration one maps the workload so that nearly all traffic lands in one bank. Accesses to that bank have excellent locality, so the hit rate is 90% and the transition count is low.
Configuration two spreads the same traffic across eight banks. Each bank sees a less local slice, so the hit rate falls to 50% and the transition count rises.
Configuration one has the better numbers on every metric this chapter defines. It is very likely slower.
Because one bank can do one thing at a time. Chapter 5.2 established that a bank holds one row and serves one access at a time, so configuration one serialises everything through a single bank while seven sit idle. Configuration two's higher transition count is spread across eight independent resources that can overlap.
9. Debugging — Telemetry That Lies
Symptom. A class report does not make sense: a hit rate near 100% on random traffic, a transition count far exceeding what the class counts imply, or numbers that contradict an independently known request count.
Candidate mechanisms.
committedasserts more than once per request — most commonly every cycle while a request waits — so one request is counted many times.busyis contributing to the transition accumulator, inflating each conflict by its waiting time.- A counter has saturated and every ratio is now wrong in a flattering direction.
- A counter wraps instead of saturating, so the ratio is not merely wrong but wrong periodically.
- The strobes are not gated on a valid request, so idle cycles are being classified — Chapter 9.3 §9's mechanism 4.
- The numbers are right and the expectation is wrong.
Evidence to collect. All seven counters as absolute values, never ratios. any_saturated and accounting_valid. The request count from an independent source — the requester side, not the classifier. And the configured ACC_W against the run length.
Discriminator — in this order, because each check is cheaper than the next.
- Check
accounting_validfirst. Low means mechanism 3 and every other number in the report is unusable. This is one bit and it invalidates everything, so nothing else is worth examining until it is high. - Check the accounting invariant by hand: does
cnt_totalequal the sum of the five class counts? A failure withaccounting_validhigh means mechanism 4 — the saturation logic is not doing what the flag claims. - Compare
cnt_totalagainst the independent request count. Exceeding it is mechanism 1 or 5. The ratio between them is diagnostic: a small excess suggests occasional double-commits, while an excess of many times suggests every waiting cycle is being counted. - Check
cnt_transitionsagainstcnt_miss + 2 × cnt_conflict. Exceeding it is mechanism 2, and the excess divided bycnt_busytells you it is the waiting cycles. - Check
cnt_busyagainstcnt_total. A very high proportion ofBUSYwith a plausible total usually means the caller is re-presenting requests every cycle and committing each time — mechanism 1 again, seen from a different angle. - If all of that reconciles, mechanism 6: the numbers are right. Compare the class distribution against the workload's own locality, computed independently as Chapter 9.3 §9 describes, and against the per-bank distribution from §8.
Responsible layer. Every mechanism except 6 is the instrumentation or its caller — not the design. That is worth stating plainly, because a class report is usually the first evidence in a performance investigation, and starting from bad numbers sends the whole investigation somewhere expensive. Mechanism 6 is a real finding and is owned by Modules 16, 18 and 23.
Fix. For 1, make committed a single pulse per request at the point the request is accepted, not at every classification. For 2, P2. For 3, widen ACC_W for the run length — and report accounting_valid on the dashboard, so the next run's numbers cannot be quietly trusted. For 4, P3. For 5, P5.
10. Common Misconceptions
"A high hit rate always means good performance."
Why it is tempting: hits are the best class, so more of them should be better.
Concrete failure: §8's configuration one — 90% hits, everything serialised through one bank, seven banks idle, and worse throughput than a 50%-hit configuration spread across eight.
Correct model: a hit rate measures row-state work avoided. Throughput depends on that and on how much can proceed in parallel. Report both distributions.
Prevention: always look at the per-bank breakdown alongside the class breakdown.
"Row hits are faster."
Why it is tempting: it is true in most systems most of the time, so it never gets challenged.
Concrete failure: a model that treats a hit as free predicts that a 95% hit rate delivers 95% of peak, and cannot explain the measurement.
Correct model: a hit is the absence of a required row-state transition. The column access, its pipeline depth, the shared column path and the timing rules all remain. Chapter 9.3 §4.
Prevention: state the claim as an absence of work, and let Modules 13 and 14 supply the time.
"A miss and a conflict cost about the same."
Why it is tempting: both are non-hits ending in an activate.
Concrete failure: a work model under-counts by a factor of two on conflict-heavy traffic — and the fixes differ, since misses usually point at page policy and conflicts at address mapping.
Correct model: one transition against two, serialised. §1 and §4.
Prevention: three separate counts, all the way to the report.
"Counters can wrap; the ratio is still about right."
Why it is tempting: ratios feel robust to absolute magnitude.
Concrete failure: a conflict counter wraps and the conflict rate collapses toward zero. The report improves the moment the system gets worse — because a worse system saturates sooner.
Correct model: saturate, and expose a sticky validity flag. §5.
Prevention: P3 and P4, and running the saturation test deliberately with a small ACC_W.
"The transition-count model predicts latency."
Why it is tempting: it counts the expensive operations, which feels like counting time.
Concrete failure: an engineer converts transitions to nanoseconds with a guessed constant and produces a prediction that matches nothing.
Correct model: it counts demanded row-state work. Converting to time needs Modules 13 and 14' durations, and even then ignores parallelism and everything in §3.
Prevention: keep the unit. A count that composes is worth more than a number that expires.
"A performance counter is part of the design, so it must be right."
Why it is tempting: it is RTL, it was reviewed, and it has assertions.
Concrete failure: the block is perfect and committed is asserted every cycle a request waits, so a conflict that waited twenty cycles is counted as twenty requests. Every number in the report is wrong and every assertion passes, because the bug is in the contract, not the block.
Correct model: the counter's correctness depends on a caller contract it cannot verify. §5's limitation.
Prevention: reconcile cnt_total against an independent request count before believing any report. It is one comparison and it catches the whole class.
11. Interview Reasoning
"Why can two streams with the same request count do very different amounts of DRAM work?"
Because the work is in row-state transitions, not in requests, and the class of a request depends on the state it meets. A hit needs zero transitions, a miss one, and a conflict two — and the two conflict transitions are serialised, since the activate needs the closed state the precharge produces. So the same addresses in a different order can differ by more than a factor of two: grouping accesses by row produces hits, while alternating rows in a bank produces a conflict for nearly every request. §2 works an eight-request example that comes out at six transitions against fourteen.
"How would you express the cost of a row conflict without quoting a timing number?"
As two serialised row-state transitions against a miss's one and a hit's zero. That formulation is structural — it follows from a bank holding one row and from the activate requiring a state only the precharge can produce — so it is true of every DDR generation and speed bin. It is also composable: multiply by the durations from the timing modules and you have the answer for a specific device; do not multiply and you still have a correct comparison between two workloads, which is usually the question being asked. A figure in nanoseconds gives you neither.
"Your hit rate went up and throughput went down. Explain."
Almost certainly a change that concentrated traffic into fewer banks. Locality within each bank improves, so the hit rate rises and the transition count falls — but one bank serves one access at a time, so the work that remains cannot overlap. The previous configuration spread traffic across more banks: a worse hit rate, more total transitions, and far more of them proceeding concurrently. The check is the per-bank distribution alongside the class distribution, and the underlying lever is the address map's bank-field position.
"Why must a performance counter saturate rather than wrap?"
Because a wrapped counter fails in the flattering direction. If the conflict counter rolls over, the conflict rate collapses toward zero — so the report gets better exactly as the system gets worse, since a worse system reaches the rollover sooner. An instrument that errs toward what its reader hopes for is worse than no instrument, because nobody investigates good news. Saturating is not sufficient on its own either: once a counter has clamped the ratios are wrong, so the design needs a sticky validity flag that says the run's numbers are unusable.
"A class report shows more classified requests than the requester issued. What happened?"
The commit signal is asserting more than once per request — most commonly every cycle while a request waits for its bank to transition, so a conflict that waited twenty cycles is counted twenty times. The counter block is correct and every assertion inside it passes, because the bug is in a caller contract the block cannot verify. The check is to reconcile the total against an independent request count from the requester side, which is one comparison and catches the entire class. The ratio between the two numbers also tells you which variant it is: a small excess means occasional double-commits, a large multiple means waiting cycles.
12. Engineering Exercise
Use §1's model throughout. Both banks start closed.
1. Compute the transition count for: b0 r5 · b0 r5 · b0 r9 · b0 r5 · b0 r9.
2. Reorder those five requests to minimise transitions. What is the minimum?
3. A run reports 1,000 hits, 200 misses, 300 conflicts, and cnt_transitions of 1,400. Is the report self-consistent?
4. The same run reports cnt_total of 1,500 and cnt_busy of 0, but the requester issued 1,500 requests. Is that consistent, and what would cnt_busy of 4,000 have meant?
5. With ACC_W = 4, how many committed hits before cnt_hit saturates, and what does accounting_valid show afterwards?
6. Configuration X: 90% hits, all traffic in one bank. Configuration Y: 55% hits, traffic spread over eight banks. Which has less demanded work, and which would you expect to be faster?
13. Summary
Count work, not time. A conflict costs two serialised row-state transitions on every DDR device that has ever existed; a figure in nanoseconds is true of one part, at one speed bin, on one day. The count composes — multiply by Modules 13 and 14' durations for a specific device — and a figure in nanoseconds cannot be decomposed back.
The model is three numbers: a hit demands zero transitions, a miss one, a conflict two. Work equals misses plus twice conflicts, and nothing else appears in it.
Order alone can more than double the work. Eight identical addresses grouped by row demand six transitions; the same eight alternating demand fourteen. Nothing about the requests changed — only what state each one met.
A single hit rate is not enough. Two streams at 50% hits can differ by a factor of two in work, and their causes differ too: misses usually point at page policy, conflicts at address mapping. Three counts, all the way to the report.
A better hit rate can mean worse performance. Concentrating traffic into one bank improves locality and destroys parallelism, and the class distribution alone will recommend it. Report the per-bank distribution beside it.
And instrumentation lies in recognisable ways. Counters must saturate rather than wrap, because a wrapped counter improves as the system degrades. Saturation must be sticky and visible, because a clamped counter invalidates every ratio derived from it. And a counter cannot verify that its caller commits once per request — so reconcile the total against an independent request count before believing any of it.
14. What Comes Next
Module 9 is complete, and its claim is worth stating in full.
Module 5 asked what structures exist. Module 6 asked what crosses the interface. Module 7 asked what operation is requested. Module 8 asked what location it targets. This module asked what happens when that location meets the state the bank is already in — and the answer is that DRAM access is state-dependent in a way that nothing upstream reveals.
The same request, unchanged, is cheap or expensive or impossible depending only on when it arrives. A bank holds one row; a request for a different one cannot simply take it; and the controller's model of which row is held is the only source of truth in the system, because nothing can be asked.
Module 10 — Read Operations picks up where every chapter here stopped. This module has said repeatedly that a column access "may proceed" once the state permits, and has never once said what then happens. Module 10 is what happens: the read command, the interval before data appears, how data returns, and the burst that carries it.
And the question this module deliberately deferred at every turn — when may any of this be issued — is Modules 13 and 14, which supply the durations that turn this chapter's transition counts into time.
Return to Row Conflicts for the two-transition derivation, Row Hits for the taxonomy and the classifier that feeds these counters, Row Misses for the per-bank evidence this chapter also wants, Banks for the parallelism §8 turns on, Bank Address for the map that sets the class distribution, and Channels for the saturating-telemetry argument reused here.
Continue learning
Related tutorials
- Related topic
Data-Transfer Efficiency
Bursting buys command efficiency and can spend payload efficiency to get it. Those are different quantities that trade against each other, and collapsing them into a single percentage is how architectural arguments go wrong.
- Related topic
The Memory-Subsystem View
Six levels, six shared resources, six conflicts. Assembling them into one picture and tracing a single request through it produces the question that organises all memory debugging: which level is blocking this, and how often?
- Related topic
Refresh Issues
Three investigations wearing one name: a missed obligation, late service, or something else that merely correlates with refresh. Separating them is most of the work.
- Related topic
The Refresh Requirement
Leakage produces a rule about the passage of time rather than about any operation. What the maintenance operation actually does, why it costs device availability, and how a digital design tracks a deadline, arbitrates it against traffic, and proves it never silently drops the obligation.
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.
