DDR · Module 23
Controller Policies
Every chapter assumed a row stays open after a burst. That is a decision, its crossover is exactly tRCD over tRP plus tRCD, and an adaptive policy that mispredicts is worse than both fixed choices.
Every chapter in this module has assumed something without naming it: that a row, once opened, stays open until another request needs the bank.
That is a policy, not a fact. Chapter 23.3 §2's cost model charges a different-row access tRP + tRCD = 22 cycles precisely because the old row was still open when the new request arrived. Had the controller closed it when the previous burst finished, the same access would have been a miss at tRCD = 11 cycles — half the cost.
So there is a decision nobody has made yet, and it is made once per burst: when a burst completes, close the row or leave it open?
The module's central law, for the last time:
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.
This chapter is the purest instance of the second clause in the module. The decision costs nothing to make, changes no timing parameter, and moves the cost of a workload by a factor of five — and §3 shows its crossover point is not a measurement but an algebraic consequence of two published numbers.
1. The Decision, and When It Is Made
The decision has a precise moment: the cycle a burst completes. Before that the row must stay open — it is carrying the transfer. After that, two options.
| Policy | On burst completion | Next access to that bank, same row | Next access, different row |
|---|---|---|---|
| Open-page | Leave the row open | HIT — 0 cycles | CONFLICT — tRP + tRCD = 22 |
| Close-page | Precharge immediately | MISS — tRCD = 11 | MISS — tRCD = 11 |
Read the two right-hand columns together, because the whole chapter is in them.
Open-page is a bet that the next access wants the same row. It wins completely when right — zero cycles — and loses badly when wrong, because the precharge it avoided must now happen in the critical path of the new request.
Close-page is a refusal to bet. Its cost is tRCD regardless of what comes next. It never achieves zero and it never pays 22.
So the two policies are not better and worse; they are a gamble and an insurance premium. And §3 shows the break-even odds are computable rather than empirical.
2. Closing After a Write Is Not the Same
One asymmetry before the algebra, because it is easy to miss and it changes the arithmetic for write-heavy workloads.
Chapter 11.4 owns tWR — write recovery time — and it is the interval a device needs after write data before the row may be precharged. VERIFIED at this configuration: tWR = 15 ns = 12 cycles.
So close-page after a read can precharge as soon as tRTP allows — VERIFIED at 6 cycles. Close-page after a write must wait tWR = 12 cycles first.
| Preceding access | Delay before precharge may begin | Category |
|---|---|---|
| Read | tRTP = 6 cycles | VERIFIED |
| Write | tWR = 12 cycles | VERIFIED |
Consequence: close-page's precharge is twice as slow to start after a write, so the window in which it can be hidden (§1's callout) is correspondingly tighter. On a write-heavy workload, close-page's advantage is harder to realise — and a policy engine that ignores the distinction will predict a benefit it does not get.
This is also why the auto-precharge variant of the column command exists (7.4 owns it): the device applies the correct delay itself, so the controller does not have to track which of the two applies.
3. The Crossover Is Algebra, Not Measurement
Here is the chapter's central result, and it is derived rather than simulated.
Let h be the probability that the next access to a bank wants the same row — the hit probability under open-page. Then expected row work per access is:
open-page = h x 0 + (1-h) x (tRP + tRCD)
= (1-h) x (tRP + tRCD)
close-page = tRCD -- independent of hOpen-page wins when (1-h) × (tRP + tRCD) < tRCD, which rearranges to:
tRCD
h > ─────────────────────────
tRP + tRCDThat is the crossover, in closed form. At DDR4-1600K, where tRP = tRCD = 11:
h_crossover = 11 / 22 = 1/2 — exactly 50%.
DERIVED, and it is exact rather than approximate. Two consequences follow immediately.
A workload with better than 50% row-hit probability should use open-page; worse than 50%, close-page. At exactly 50% the two are identical in expectation, which §4's table confirms numerically.
And the threshold is a property of the device, not of the workload. It depends only on tRP and tRCD. So the threshold is fixed at design time from published numbers, and only the measurement of h is workload-dependent — which is precisely the division an adaptive policy needs.
4. Neither Fixed Policy Wins
§3's algebra predicts a crossover. Here it is numerically, over five workload shapes at 1000 accesses each. All DERIVED; h is the stated model parameter.
| Workload shape | h | Open-page | Close-page | Winner | Margin |
|---|---|---|---|---|---|
| High locality | 90% | 2200 | 11000 | Open-page | 8800 cycles |
| Moderate | 70% | 6600 | 11000 | Open-page | 4400 cycles |
| At the crossover | 50% | 11000 | 11000 | Exact tie | 0 |
| Poor | 30% | 15400 | 11000 | Close-page | 4400 cycles |
| Low locality | 10% | 19800 | 11000 | Close-page | 8800 cycles |
Four observations, and the last two are the design content.
The tie at 50% is exact, which is §3's algebra confirmed rather than approximated.
The table is symmetric about the crossover. Margins of 8800 and 4400 appear on both sides at h of 90/10 and 70/30 — a consequence of open-page's cost being linear in h while close-page's is constant.
Open-page's range is far wider. It spans 2200 to 19800 cycles — a factor of 9 across the workload range — while close-page is flat at 11000. Open-page is the high-variance policy, and that matters beyond its mean: a system that must bound its worst case has a reason to prefer close-page that this table's winner column does not capture.
And no fixed choice is safe. Choosing open-page costs 8800 cycles on the low-locality workload; choosing close-page costs 8800 on the high-locality one. A fixed policy is a bet on the workload, which is the entire argument for adapting.
5. What Adaptation Can Buy, and Its Ceiling
Before building an adaptive policy it is worth deriving what a perfect one would achieve, because that is the ceiling and it is smaller than it first appears.
A perfect predictor closes the row exactly when the next access to that bank will want a different row, and leaves it open exactly when the next access will want the same row. So:
- Same-row accesses stay hits — 0 cycles, as under open-page.
- Different-row accesses become misses at
tRCD— not conflicts attRP + tRCD, because the row was closed in advance.
Expected cost is therefore (1-h) × tRCD. Compare all four options on the high-locality workload, h = 90%, 1000 accesses:
| Policy | Row work | Relative to perfect |
|---|---|---|
| Perfect adaptive | 1100 cycles | 1.0× |
| Open-page | 2200 | 2.0× |
| Close-page | 11000 | 10.0× |
| Thrashing adaptive (§6) | 12100 | 11.0× |
All DERIVED. Two results.
Perfect adaptation beats open-page by exactly 2× on the non-hit accesses, and the reason is structural: it converts every conflict into a miss, and a conflict is (tRP + tRCD) / tRCD = 2× a miss at this bin. That factor is the ceiling on what prediction can buy over open-page, and it is the same ratio 23.3 §2's callout identified.
So adaptation's upside is bounded and known in advance. At h = 90% it is 1100 cycles on 1000 accesses. A predictor that is right 80% of the time captures a proportion of that, and a predictor that is right less than half the time is worse than not predicting — which §6 quantifies.
6. A Policy That Changes Its Mind Is Worse Than Either
§5's table has a fourth row that needs explaining, because it is the failure mode an adaptive policy must be designed against.
Consider an anti-correlated predictor on the same h = 90% workload: it closes the row before every same-row access and leaves it open before every different-row access. Perfectly wrong, every time.
- The 900 would-be hits become misses at
tRCD= 11 → 9900 cycles. - The 100 different-row accesses stay conflicts at 22 → 2200 cycles.
- Total: 12100 cycles.
DERIVED. That is 5.5× worse than open-page, 1.1× worse than close-page, and 11× worse than a perfect predictor on the identical workload.
A wrong adaptive policy is worse than either fixed policy, and the reason is that it combines both failure modes: it pays close-page's premium on the accesses where open-page would have been free, and open-page's penalty on the accesses where close-page would have been cheap.
same-row access different-row access
open-page 0 (best) 22 (worst)
close-page 11 11
perfect adaptive 0 (best) 11 (best)
anti-correlated 11 22 (worst)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
worst of both columnsThat table is the argument for hysteresis. A predictor with no memory, reacting to the most recent outcome, is not merely imprecise — on an alternating access pattern it can be systematically anti-correlated, because the thing it just observed is exactly the wrong guide to what comes next.
7. What the Predictor Must Predict
It is worth being precise, because the obvious formulation is wrong in a way that matters.
The wrong formulation: predict whether this bank's hit rate is above the crossover. That is a per-bank aggregate, it changes slowly, and by the time it has moved the workload phase that caused it may be over.
The right formulation: predict, for this bank at this moment, whether the next access will want the row currently open. That is a per-bank, per-burst binary prediction, and it is what §1's decision actually needs.
The distinction shows up in what state the two require:
| Formulation | State per bank | Responds to |
|---|---|---|
| Hit-rate estimate | A counter over many accesses | Slow phase changes |
| Next-access prediction | A small saturating counter | Local behaviour |
And the counter is not counting hits — it is counting whether keeping the row open paid off. Those differ: a hit means the row was still open and matched; a correct decision to keep it open means a hit occurred and the alternative would have been worse. On a bank where accesses arrive in pairs to the same row and then move on, the hit rate is 50% and keeping the row open is right exactly half the time — which the crossover says is break-even, and a counter measuring the decision's outcome reports directly.
So the predictor's update rule is:
kept the row open, next access HIT -> the bet paid. Increment.
kept the row open, next access CONFLICT -> the bet lost. Decrement.
closed the row, next access MISS -> no information about
whether keeping it would
have hit. See §8.That third line is the problem §8 is about.
8. The Predictor Cannot Observe Its Own Counterfactual
Here is the structural difficulty, and it is the reason an adaptive page policy is harder than it looks.
When the policy closes a row, it destroys the evidence it needs to evaluate that decision. The next access is a miss. Whether it would have been a hit — in which case closing was wrong — is unobservable, because the row is gone.
So a policy in close mode receives no signal about whether it should switch back. It can stay closed indefinitely on a workload that would have been served better by staying open, and nothing in its inputs contradicts it.
Two mechanisms address this, and both cost something:
Remember the closed row's address. Keep the row address after precharging it, and on the next access compare: if it matches, the decision was wrong. This costs one row-address register per bank and it is exact — the counterfactual becomes observable at the price of storage.
Periodically explore. Occasionally keep a row open despite the counter saying otherwise, and observe the outcome. This costs the occasional bad decision in exchange for information — the same trade Module 21 makes when it retrains a setting that currently works.
§10's block takes the first, because it is exact and the cost is one register per bank against a predictor that is otherwise blind in one of its two states.
9. The Policy State Machine
The two-way exit from PREDICT is the policy, and UPDATE COUNTER is what makes it adaptive. A fixed policy is this machine with one of the two exits removed — which is worth noticing, because it means open-page and close-page are not different designs but degenerate parameterisations of the same one, and §10's block exposes exactly that through its mode parameter.
The edge from CLOSE NOW to UPDATE COUNTER is the one §8 says is hard: the observation available there is a miss, and a miss carries no information about whether holding would have hit — unless the retained row address supplies it.
10. The Page Policy Engine
// ---------------------------------------------------------------------
// page_policy_engine -- per-bank open / close / adaptive page policy
// with a saturating confidence counter and an observable counterfactual.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHAT IT DOES NOT MODEL:
// - the device, or tRP / tRCD / tWR enforcement. §2: closing after a
// write must wait tWR, and 13.4 owns whether a precharge may issue.
// This block requests; the legality gate decides when.
// - the scheduler's request choice (23.4 owns it)
// - the hidden-precharge assumption of §1's callout: whether the
// precharge actually lands in an idle interval is a scheduling
// outcome this block cannot see.
//
// THE THREE MODES ARE ONE DESIGN. Open-page and close-page are
// degenerate parameterisations (§9), which is why MODE selects rather
// than switching between implementations.
// ---------------------------------------------------------------------
module page_policy_engine #(
parameter int NUM_BANKS = 16,
parameter int ROW_W = 16,
// 0 = always open, 1 = always close, 2 = adaptive.
parameter int MODE = 2,
// Saturating counter width per bank. §6's callout: hysteresis is
// mandatory, and 2 bits is the smallest that provides any.
parameter int CTR_W = 2,
// Decide to hold open when the counter is at or above this. With
// CTR_W = 2 the counter spans 0..3 and a threshold of 2 means two
// consecutive wins are needed to switch to holding.
parameter int HOLD_THRESHOLD = 2,
// Retain the precharged row's address so the counterfactual of §8 is
// observable. Costs one register per bank and removes the blindness.
parameter bit TRACK_COUNTERFACTUAL = 1'b1,
parameter int BK_W = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS),
parameter int CNT_W = 20
) (
input logic clk,
input logic rst_n,
// ── A burst has completed on this bank: the decision point of §1.
input logic burst_done,
input logic [BK_W-1:0] burst_bank,
input logic [ROW_W-1:0] burst_row,
// Direction matters for WHEN the precharge may start (§2), not for
// whether to close. Published onward rather than acted on here.
input logic burst_was_write,
// ── The next access to a bank, which is the outcome observation.
input logic acc_valid,
input logic [BK_W-1:0] acc_bank,
input logic [ROW_W-1:0] acc_row,
// ── THE DECISION.
output logic close_request,
output logic [BK_W-1:0] close_bank,
// Forwarded so the scheduler applies tWR rather than tRTP (§2).
output logic close_after_write,
// ── Observability: the predictor's own accuracy, which §8 says is
// only observable at all because of the retained row address.
output logic [CNT_W-1:0] n_held_and_hit, // the bet paid
output logic [CNT_W-1:0] n_held_and_conflict, // the bet lost
output logic [CNT_W-1:0] n_closed_and_reused, // closing was WRONG
output logic [CNT_W-1:0] n_closed_and_other, // closing was right
output logic [CTR_W-1:0] ctr_of_bank,
output logic predict_hold,
output logic any_saturated,
output logic err_close_unopened,
output logic err_mode_invalid
);
initial begin
if (NUM_BANKS < 1)
$fatal(1, "page_policy_engine: NUM_BANKS must be at least 1");
if (MODE < 0 || MODE > 2)
$fatal(1, "page_policy_engine: MODE must be 0, 1 or 2");
if (CTR_W < 2 && MODE == 2)
// A 1-bit counter has no hysteresis: one observation flips it,
// which is exactly §6's oscillator.
$fatal(1, "page_policy_engine: adaptive MODE needs CTR_W >= 2");
if (HOLD_THRESHOLD < 1 || HOLD_THRESHOLD > (1 << CTR_W) - 1)
$fatal(1, "page_policy_engine: HOLD_THRESHOLD out of counter range");
end
logic [CTR_W-1:0] ctr [NUM_BANKS];
logic is_open [NUM_BANKS];
logic [ROW_W-1:0] open_row [NUM_BANKS];
// §8's counterfactual: the row we precharged, kept so a later access
// can reveal that closing was wrong.
logic had_closed [NUM_BANKS];
logic [ROW_W-1:0] closed_row [NUM_BANKS];
// Declared above the assigns that read them.
logic [CNT_W-1:0] n_hh, n_hc, n_cr, n_co;
logic sat, bad_close;
// ── THE DECISION. Open-page and close-page are the degenerate cases
// of the adaptive rule, which is why there is one expression.
logic hold_now;
always_comb begin
unique case (MODE)
0 : hold_now = 1'b1; // always open
1 : hold_now = 1'b0; // always close
default : hold_now = burst_done
&& (ctr[burst_bank] >= CTR_W'(HOLD_THRESHOLD));
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int b = 0; b < NUM_BANKS; b++) begin
// Start at the threshold: neither biased toward holding nor
// toward closing, so the first few observations decide. Biasing
// it either way makes the policy's warm-up a hidden default.
ctr[b] <= CTR_W'(HOLD_THRESHOLD);
is_open[b] <= 1'b0;
open_row[b] <= '0;
had_closed[b] <= 1'b0;
closed_row[b] <= '0;
end
n_hh <= '0; n_hc <= '0; n_cr <= '0; n_co <= '0;
sat <= 1'b0; bad_close <= 1'b0;
end else begin
bad_close <= 1'b0;
// ── The outcome observation, BEFORE the new decision, so an
// access and a burst completion in the same cycle are ordered
// correctly: the access reports on the PREVIOUS decision.
if (acc_valid) begin
if (is_open[acc_bank]) begin
// The row was held. Did the bet pay?
if (open_row[acc_bank] == acc_row) begin
if (n_hh != {CNT_W{1'b1}}) n_hh <= n_hh + CNT_W'(1); else sat <= 1'b1;
if (ctr[acc_bank] != {CTR_W{1'b1}})
ctr[acc_bank] <= ctr[acc_bank] + CTR_W'(1);
end else begin
if (n_hc != {CNT_W{1'b1}}) n_hc <= n_hc + CNT_W'(1); else sat <= 1'b1;
if (ctr[acc_bank] != '0)
ctr[acc_bank] <= ctr[acc_bank] - CTR_W'(1);
end
end else if (TRACK_COUNTERFACTUAL && had_closed[acc_bank]) begin
// §8: the row was closed. The retained address reveals
// whether closing was wrong -- the ONLY way this state
// produces a learning signal at all.
if (closed_row[acc_bank] == acc_row) begin
// Closing was WRONG: this would have been a hit.
if (n_cr != {CNT_W{1'b1}}) n_cr <= n_cr + CNT_W'(1); else sat <= 1'b1;
if (ctr[acc_bank] != {CTR_W{1'b1}})
ctr[acc_bank] <= ctr[acc_bank] + CTR_W'(1);
end else begin
if (n_co != {CNT_W{1'b1}}) n_co <= n_co + CNT_W'(1); else sat <= 1'b1;
if (ctr[acc_bank] != '0)
ctr[acc_bank] <= ctr[acc_bank] - CTR_W'(1);
end
end
// The access opens its row either way.
is_open[acc_bank] <= 1'b1;
open_row[acc_bank] <= acc_row;
had_closed[acc_bank] <= 1'b0;
end
// ── The decision, at the burst-completion point of §1.
if (burst_done) begin
if (!is_open[burst_bank] && !acc_valid) bad_close <= 1'b1;
if (!hold_now) begin
is_open[burst_bank] <= 1'b0;
had_closed[burst_bank] <= 1'b1;
closed_row[burst_bank] <= burst_row;
end
end
end
end
assign close_request = burst_done && !hold_now;
assign close_bank = burst_bank;
assign close_after_write = burst_done && !hold_now && burst_was_write;
assign n_held_and_hit = n_hh;
assign n_held_and_conflict = n_hc;
assign n_closed_and_reused = n_cr;
assign n_closed_and_other = n_co;
assign ctr_of_bank = ctr[acc_valid ? acc_bank : burst_bank];
assign predict_hold = hold_now;
assign any_saturated = sat;
assign err_close_unopened = bad_close;
assign err_mode_invalid = 1'b0; // MODE is checked at elaboration
endmoduleThree decisions in that block are the chapter rather than the implementation.
The counter initialises at the threshold. Starting it at zero biases the policy toward closing during warm-up; starting it at maximum biases toward holding. Starting at the threshold means the first observations decide, and the policy has no hidden default — which matters because a short-lived workload may never leave its warm-up.
The outcome observation is ordered before the decision. An access and a burst completion can occur in the same cycle, and the access is reporting on the previous decision while the completion is making the next one. Ordering them the other way attributes an outcome to the decision that had not been made yet.
n_closed_and_reused is the output that makes §8 tractable. It counts the times closing was wrong — an access arrived wanting the row that had just been precharged — and it is only observable because closed_row was retained. Without it the predictor learns in one direction only, and a policy stuck in close mode has no input that could ever move it.
11. The Counterfactual Accountant
// ---------------------------------------------------------------------
// policy_counterfactual -- what would open-page, close-page and a
// perfect predictor each have cost on THIS access stream?
//
// CLASSIFICATION: educational, synthesisable.
//
// WHY IT EXISTS: §4 shows the winner depends on the workload's hit
// probability, and §3 gives the crossover in closed form. A design can
// therefore decide which policy to use IF it knows h -- and this block
// measures the three costs directly instead, which is stronger because
// it needs no distributional assumption at all.
//
// WHAT IT DOES NOT MODEL:
// - the device, the deployed policy, or the scheduler
// - the hidden-precharge assumption (§1's callout). Close-page's
// cost here assumes the precharge lands in an idle interval; if it
// does not, the real cost is higher and this understates it.
// ---------------------------------------------------------------------
module policy_counterfactual #(
parameter int NUM_BANKS = 16,
parameter int ROW_W = 16,
// VERIFIED weights from 23.3 §2 / 23.1 §2.
parameter int T_RCD = 11,
parameter int T_RP = 11,
parameter int BK_W = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS),
parameter int ACC_W = 32
) (
input logic clk,
input logic rst_n,
input logic acc_valid,
input logic [BK_W-1:0] acc_bank,
input logic [ROW_W-1:0] acc_row,
// The NEXT access's row for this bank, if known. A perfect predictor
// needs it, which is exactly why a perfect predictor is not
// implementable -- this input exists only to compute the CEILING.
input logic lookahead_valid,
input logic [ROW_W-1:0] lookahead_row,
// ── The three costs, in cycles, on the observed stream.
output logic [ACC_W-1:0] cost_open,
output logic [ACC_W-1:0] cost_close,
output logic [ACC_W-1:0] cost_perfect,
output logic [ACC_W-1:0] n_accesses,
// ── The crossover, evaluated: is the stream's behaviour above or
// below tRCD/(tRP+tRCD)? Reported as the comparison rather than
// as a ratio, so nothing divides.
output logic open_is_better,
output logic counts_valid,
output logic err_weights_invalid
);
initial begin
if (T_RCD < 1 || T_RP < 1)
$fatal(1, "policy_counterfactual: weights must be positive");
if (ACC_W < 16)
$fatal(1, "policy_counterfactual: ACC_W too small to accumulate");
end
logic [ROW_W-1:0] open_row [NUM_BANKS];
logic valid_r [NUM_BANKS];
logic [ACC_W-1:0] c_open, c_close, c_perf, n_acc;
logic sat;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int b = 0; b < NUM_BANKS; b++) begin
open_row[b] <= '0;
valid_r[b] <= 1'b0;
end
c_open <= '0; c_close <= '0; c_perf <= '0; n_acc <= '0; sat <= 1'b0;
end else if (acc_valid) begin
if (n_acc == {ACC_W{1'b1}}) begin
sat <= 1'b1;
end else begin
n_acc <= n_acc + ACC_W'(1);
// ── OPEN-PAGE: hit if the row matches, else a conflict.
if (!valid_r[acc_bank])
c_open <= c_open + ACC_W'(T_RCD); // first touch
else if (open_row[acc_bank] == acc_row)
c_open <= c_open; // hit: zero
else
c_open <= c_open + ACC_W'(T_RP + T_RCD); // conflict
// ── CLOSE-PAGE: always a miss. Constant, independent of the
// stream -- which is §3's whole point, in one line.
c_close <= c_close + ACC_W'(T_RCD);
// ── PERFECT: a hit costs nothing; anything else costs a miss,
// because the row was closed in advance. §5's ceiling.
if (valid_r[acc_bank] && (open_row[acc_bank] == acc_row))
c_perf <= c_perf;
else
c_perf <= c_perf + ACC_W'(T_RCD);
open_row[acc_bank] <= acc_row;
valid_r[acc_bank] <= 1'b1;
end
end
end
assign cost_open = c_open;
assign cost_close = c_close;
assign cost_perfect = c_perf;
assign n_accesses = n_acc;
// The crossover test, as a comparison rather than a ratio.
assign open_is_better = (c_open < c_close);
assign counts_valid = !sat && (n_acc != '0);
assign err_weights_invalid = 1'b0; // checked at elaboration
endmoduleThis block is the most directly useful thing in the chapter, and the reason is in its header. §3 gives the crossover in closed form, so a design that knows its hit probability can choose a policy analytically. A design that does not know it can measure all three costs instead — and that is stronger, because it needs no assumption about the workload's distribution at all.
The cost_perfect output is the ceiling of §5, and it is computed from a lookahead input that no real controller has. That is deliberate: the block computes an upper bound on what adaptation could buy, so a design can see whether the achievable gain justifies the predictor before building one.
The precharge timing gate
§10's engine requests a close and explicitly defers when it may happen. §2 established that the delay differs by direction — tRTP = 6 cycles after a read, tWR = 12 cycles after a write, both VERIFIED — so something has to apply the right one.
// ---------------------------------------------------------------------
// precharge_timing_gate -- applies tRTP after a read and tWR after a
// write before a requested precharge becomes eligible.
//
// CLASSIFICATION: educational, synthesisable. Supplies the timing
// page_policy_engine requests and deliberately does not own.
//
// WHAT IT DOES NOT MODEL:
// - the device, or command-bus arbitration
// - LEGALITY (13.4) or the commit point (17.1). Eligible is not
// issued, and this block never claims otherwise.
// - whether the precharge lands in an idle interval, which is §1's
// callout's assumption and a scheduling outcome.
//
// §2: the two delays differ by 2x, so a gate applying one value to
// both would either violate tWR after writes or needlessly delay
// precharges after reads.
// ---------------------------------------------------------------------
module precharge_timing_gate #(
parameter int NUM_BANKS = 16,
// VERIFIED at DDR4-1600K: tRTP = 6 nCK, tWR = 15 ns = 12 nCK.
parameter int T_RTP = 6,
parameter int T_WR = 12,
parameter int BK_W = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS),
parameter int D_W = (T_WR > T_RTP)
? ((T_WR <= 1) ? 1 : $clog2(T_WR + 1))
: ((T_RTP <= 1) ? 1 : $clog2(T_RTP + 1)),
parameter int OCC_W = $clog2(NUM_BANKS + 1)
) (
input logic clk,
input logic rst_n,
// -- A close request from page_policy_engine, with its direction.
input logic close_request,
input logic [BK_W-1:0] close_bank,
input logic close_after_write,
// -- Eligibility, per bank. NOT an issue signal.
output logic [NUM_BANKS-1:0] pre_eligible,
output logic [D_W-1:0] wait_of_bank,
output logic [OCC_W-1:0] n_waiting,
// -- Observability: cycles spent waiting, split by direction, so the
// §2 asymmetry's real cost on this workload is visible.
output logic [23:0] cy_waiting_read,
output logic [23:0] cy_waiting_write,
output logic any_saturated,
output logic err_request_while_waiting
);
initial begin
if (NUM_BANKS < 1)
$fatal(1, "precharge_timing_gate: NUM_BANKS must be at least 1");
if (T_RTP < 1 || T_WR < 1)
$fatal(1, "precharge_timing_gate: delays must be positive");
if (T_WR < T_RTP)
// §2: write recovery is the LONGER of the two. A parameterisation
// claiming otherwise inverts the asymmetry the block exists for.
$fatal(1, "precharge_timing_gate: T_WR (%0d) below T_RTP (%0d)",
T_WR, T_RTP);
end
logic [D_W-1:0] cnt [NUM_BANKS];
logic pend [NUM_BANKS];
logic was_wr [NUM_BANKS];
// Declared above the assigns that read them.
logic [23:0] cy_rd, cy_wr;
logic sat, bad_req;
logic [OCC_W-1:0] n_wait;
always_comb begin
n_wait = '0;
for (int b = 0; b < NUM_BANKS; b++)
if (pend[b] && (cnt[b] != '0)) n_wait = n_wait + OCC_W'(1);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int b = 0; b < NUM_BANKS; b++) begin
cnt[b] <= '0;
pend[b] <= 1'b0;
was_wr[b] <= 1'b0;
end
cy_rd <= '0; cy_wr <= '0; sat <= 1'b0; bad_req <= 1'b0;
end else begin
bad_req <= 1'b0;
// -- Age every pending bank, charging the wait to the right
// direction so §2's asymmetry is measurable rather than
// merely asserted.
for (int b = 0; b < NUM_BANKS; b++) begin
if (pend[b] && (cnt[b] != '0)) begin
cnt[b] <= cnt[b] - D_W'(1);
if (was_wr[b]) begin
if (cy_wr == 24'hFFFFFF) sat <= 1'b1; else cy_wr <= cy_wr + 24'd1;
end else begin
if (cy_rd == 24'hFFFFFF) sat <= 1'b1; else cy_rd <= cy_rd + 24'd1;
end
end
end
// -- A new request loads the direction-appropriate delay.
if (close_request) begin
if (pend[close_bank] && (cnt[close_bank] != '0)) bad_req <= 1'b1;
pend[close_bank] <= 1'b1;
was_wr[close_bank] <= close_after_write;
cnt[close_bank] <= close_after_write ? D_W'(T_WR) : D_W'(T_RTP);
end
end
end
always_comb
for (int b = 0; b < NUM_BANKS; b++)
pre_eligible[b] = pend[b] && (cnt[b] == '0);
assign wait_of_bank = cnt[close_bank];
assign n_waiting = n_wait;
assign cy_waiting_read = cy_rd;
assign cy_waiting_write = cy_wr;
assign any_saturated = sat;
assign err_request_while_waiting = bad_req;
endmoduleTwo assertions belong with it:
// P11 -- the loaded delay matches the direction. §2's asymmetry, and
// the check that a single value is not being applied to both.
property p_delay_matches_direction;
@(posedge clk) disable iff (!rst_n)
close_request
|=> (wait_of_bank == ($past(close_after_write, 1)
? D_W'(T_WR) : D_W'(T_RTP)));
endproperty
assert property (p_delay_matches_direction);
// P12 -- eligibility requires the counter to have expired. A gate that
// asserted early would violate tWR, which is a device rule and not a
// policy preference.
property p_eligible_only_when_expired;
@(posedge clk) disable iff (!rst_n)
pre_eligible[close_bank] |-> (wait_of_bank == '0);
endproperty
assert property (p_eligible_only_when_expired);
// -- Cover: a write-direction wait, which a read-only trace never
// exercises and which is the longer of the two.
cover property (@(posedge clk) disable iff (!rst_n)
close_request && close_after_write);
// Several banks waiting at once -- the multi-bank case.
cover property (@(posedge clk) disable iff (!rst_n) n_waiting > OCC_W'(1));The two cycle counters split by direction on purpose. §2 claims the write case is twice as slow to start; cy_waiting_write against cy_waiting_read measures it on the actual workload, which is what turns §2 from a parameter table into a figure a design can act on. A write-heavy workload shows the write counter dominating, and §15's eighth row is the diagnostic that follows.
12. What the Assertions Prove
// Bind unit note: P1-P7 reference page_policy_engine's internals,
// P8-P10 policy_counterfactual's; both are written as though bound
// into their module, so clk and rst_n are visible directly.
// P1 -- MODE 0 never closes. Open-page as a degenerate case (§9), and
// the check that the mode parameter is real rather than decorative.
property p_open_mode_never_closes;
@(posedge clk) disable iff (!rst_n)
(MODE == 0) |-> !close_request;
endproperty
assert property (p_open_mode_never_closes);
// P2 -- MODE 1 closes on every burst completion. Close-page, likewise.
property p_close_mode_always_closes;
@(posedge clk) disable iff (!rst_n)
((MODE == 1) && burst_done) |-> close_request;
endproperty
assert property (p_close_mode_always_closes);
// P3 -- a close is requested only at a burst completion. §1: the
// decision has one moment, and closing at any other point would
// precharge a row that may still be carrying a transfer.
property p_close_only_at_burst_end;
@(posedge clk) disable iff (!rst_n)
close_request |-> burst_done;
endproperty
assert property (p_close_only_at_burst_end);
// P4 -- the adaptive decision follows the counter and the threshold.
property p_adaptive_follows_counter;
@(posedge clk) disable iff (!rst_n)
((MODE == 2) && burst_done)
|-> (predict_hold == (ctr[burst_bank] >= CTR_W'(HOLD_THRESHOLD)));
endproperty
assert property (p_adaptive_follows_counter);
// P5 -- a held row that hit increments; a held row that conflicted
// decrements. The update rule of §7, checkable.
property p_counter_update_direction;
@(posedge clk) disable iff (!rst_n)
(acc_valid && is_open[acc_bank] && (open_row[acc_bank] == acc_row)
&& (ctr[acc_bank] != {CTR_W{1'b1}}))
|=> (ctr[$past(acc_bank, 1)] == $past(ctr[acc_bank], 1) + 1);
endproperty
assert property (p_counter_update_direction);
// P6 -- THE hysteresis property of §6's callout. A single observation
// cannot move the counter across the threshold when it starts more
// than one step away. Without this the policy is an oscillator.
property p_single_observation_cannot_flip;
@(posedge clk) disable iff (!rst_n)
((MODE == 2) && acc_valid
&& (ctr[acc_bank] >= CTR_W'(HOLD_THRESHOLD) + CTR_W'(1)))
|=> (ctr[$past(acc_bank, 1)] >= CTR_W'(HOLD_THRESHOLD));
endproperty
assert property (p_single_observation_cannot_flip);
// P7 -- the counterfactual counter only moves when tracking is
// enabled. §8: without it the predictor is blind in close mode, and
// that blindness must be visible rather than silently absent.
property p_counterfactual_requires_tracking;
@(posedge clk) disable iff (!rst_n)
(n_closed_and_reused != $past(n_closed_and_reused, 1))
|-> (TRACK_COUNTERFACTUAL == 1'b1);
endproperty
assert property (p_counterfactual_requires_tracking);
// P8 -- close-page's cost is exactly tRCD per access. §3's "independent
// of h", as an invariant rather than an observation.
property p_close_cost_is_linear;
@(posedge clk) disable iff (!rst_n)
counts_valid |-> (cost_close == n_accesses * ACC_W'(T_RCD));
endproperty
assert property (p_close_cost_is_linear);
// P9 -- a perfect predictor is never worse than either fixed policy.
// §5's ceiling, and the check that the ceiling is computed correctly.
property p_perfect_bounds_both;
@(posedge clk) disable iff (!rst_n)
counts_valid |-> ((cost_perfect <= cost_open)
&& (cost_perfect <= cost_close));
endproperty
assert property (p_perfect_bounds_both);
// P10 -- open-page's cost never exceeds (tRP+tRCD) per access, and
// close-page's never falls below tRCD. The two bounds of §4's table.
property p_costs_within_bounds;
@(posedge clk) disable iff (!rst_n)
counts_valid |-> (cost_open <= n_accesses * ACC_W'(T_RP + T_RCD));
endproperty
assert property (p_costs_within_bounds);
// ── Cover.
cover property (@(posedge clk) disable iff (!rst_n) close_request);
cover property (@(posedge clk) disable iff (!rst_n)
burst_done && predict_hold && (MODE == 2));
// The counterfactual fired: closing was WRONG and the predictor learned
// it. §8's mechanism actually exercised -- without this cover, the
// retained row address is untested.
cover property (@(posedge clk) disable iff (!rst_n)
n_closed_and_reused != $past(n_closed_and_reused, 1));
// The counter saturated at each end -- the hysteresis limits.
cover property (@(posedge clk) disable iff (!rst_n)
(MODE == 2) && acc_valid && (ctr[acc_bank] == '0));
cover property (@(posedge clk) disable iff (!rst_n)
(MODE == 2) && acc_valid && (ctr[acc_bank] == {CTR_W{1'b1}}));
// Open-page better, and close-page better, on the same instance across
// two runs -- §4's flip actually observed.
cover property (@(posedge clk) disable iff (!rst_n)
counts_valid && open_is_better);
cover property (@(posedge clk) disable iff (!rst_n)
counts_valid && !open_is_better);
// A close after a write: §2's tWR path, which a read-only trace misses.
cover property (@(posedge clk) disable iff (!rst_n) close_after_write);P6 is the property this chapter exists to make possible, and it is the one that distinguishes a predictor from §6's oscillator. Without hysteresis a single observation flips the decision, and §6's table showed that an anti-correlated predictor is 5.5× worse than open-page and 11× worse than a perfect one.
P9 is worth having as a self-check on the counterfactual block: a perfect predictor must bound both fixed policies, and if it does not, the ceiling is being computed wrongly and every conclusion drawn from it is suspect.
13. Corner Cases
| Case | Behaviour | Why |
|---|---|---|
MODE = 0 | Never closes; close_request flat low | Open-page as a degenerate case — P1 |
MODE = 1 | Closes on every burst | Close-page — P2 |
MODE = 2, CTR_W = 1 | $fatal at elaboration | One bit has no hysteresis — §6's oscillator |
| Counter at the threshold on reset | First observations decide; no hidden default | §10 — biasing warm-up either way is a silent policy |
| Access and burst completion same cycle | Outcome observed first, then the decision | The access reports on the previous decision |
| Close after a write | close_after_write set; tWR applies | §2 — twice the delay of a read |
TRACK_COUNTERFACTUAL = 0 | n_closed_and_reused stays zero | P7; the predictor is blind in close mode and it shows |
| Bank never accessed | Counter stays at its initial value | No observations, no adaptation |
| Alternating two rows in one bank | Counter oscillates within hysteresis | §6's pattern; the counter should settle low |
| Workload exactly at the crossover | Both fixed costs equal; adaptation gains nothing | §4's tie row; the predictor tracks noise |
| Every access to a fresh bank | Open-page charged tRCD, not a conflict | A first touch has no open row to conflict with |
| Counter saturated high, then a phase change | Takes CTR_W observations to respond | The cost of hysteresis, and it is the point |
cost_perfect exceeding either fixed cost | Impossible — P9 | The ceiling is mis-computed if it happens |
Row twelve is the honest cost of §6's fix. Hysteresis makes the predictor slow to respond to a genuine phase change — it takes as many observations to cross the threshold as the counter is wide. That is a real cost, deliberately paid, and it is why CTR_W is a parameter: two bits is the minimum that works and more is not obviously better.
14. DV — Replay the Crossover
The checker reproduces §3's algebra, §4's table and §6's thrash from the RTL, using a reference that computes costs by closed form rather than by accumulation.
// Independent policy cost reference. Computes each policy's cost from
// CLASS COUNTS by closed form, rather than by per-access accumulation
// -- a different method, so agreement is evidence.
// SIMULATION-ONLY.
class policy_ref;
int unsigned t_rcd, t_rp;
int unsigned n_hit, n_other; // over the observed stream
function int unsigned cost_open();
return n_other * (t_rp + t_rcd);
endfunction
function int unsigned cost_close();
return (n_hit + n_other) * t_rcd;
endfunction
function int unsigned cost_perfect();
return n_other * t_rcd;
endfunction
// §3's crossover, as a comparison with no division.
function bit open_better();
return cost_open() < cost_close();
endfunction
// The crossover hit rate, as a numerator/denominator pair.
function void crossover(output int unsigned num, output int unsigned den);
num = t_rcd; den = t_rp + t_rcd;
endfunction
endclass| Check | What it establishes |
|---|---|
| All three costs match the RTL over a 1000-access stream | Closed form agrees with accumulation |
h = 90%: expect 2200 / 11000 / 1100 | §4's and §5's published rows |
h = 50%: expect 11000 / 11000, and open_is_better low | §4's exact tie, and the boundary's direction |
h = 10%: expect 19800 / 11000 | §4's low-locality row |
Sweep h from 0 to 100% in 5% steps; find where open_is_better flips | §3's crossover located empirically at 50% |
Set t_rp = 22, t_rcd = 11; expect the crossover at 1/3 | The general form, not the bin-specific value |
Anti-correlated predictor on h = 90%: expect 12100 | §6's thrash, from the RTL |
CTR_W = 2, alternating rows: counter settles low, no oscillation | P6 doing its job |
TRACK_COUNTERFACTUAL = 0 on a close-mode workload: counter never rises | §8's blindness, demonstrated |
cost_perfect never exceeds either fixed cost, over 10,000 streams | P9 as a bound |
The sixth and ninth checks are the two worth running, and the ninth produces the chapter's closing failure report:
THE PREDICTOR THAT CANNOT LEARN ITS WAY OUT
MODE = 2, CTR_W = 2, HOLD_THRESHOLD = 2, counter starts at 2.
TRACK_COUNTERFACTUAL = 0.
workload: h = 90% -- open-page would cost 2200 cy per 1000 accesses.
but the stream BEGINS with a burst of 3 different-row accesses.
access 1-3 : row held, conflict each time -> counter 2,1,0
access 4 : counter 0 < threshold 2 -> CLOSE
access 5+ : every access is a MISS. The row is closed before each
one, so the access never reveals that it WANTED the
row that was just precharged.
with TRACK_COUNTERFACTUAL = 0 there is no observation
at all -> the counter NEVER MOVES.
outcome over 1000 accesses:
actual cost : 3 conflicts x 22 + 997 misses x 11 = 11033 cy
open-page would : 2200 cy
close-page would : 11000 cy
perfect would : 1100 cy
the adaptive policy performed WORSE THAN BOTH fixed policies
(11033 > 11000 > 2200) on a workload with 90% locality.
diagnosis : three unrepresentative accesses drove the counter to
zero, and §8's blindness meant no subsequent evidence could ever
contradict it. The policy is stuck, correctly by its own rules.
what still passes : P1-P6 all hold. P4 holds -- the decision
follows the counter. P6 holds -- no single observation flipped
anything. The engine is working exactly as specified.
caught by : this check, and by n_closed_and_reused being
permanently zero -- which P7 explains is expected when tracking
is disabled, and which is therefore the signature rather than
the bug.
the fix : TRACK_COUNTERFACTUAL = 1. With it, access 5 reveals that
the precharged row was wanted, the counter rises, and the policy
returns to holding within CTR_W observations.That is the right note for the module's verification to end on. Every property holds, the engine is correct by its specification, and the policy is 5× worse than the fixed alternative it replaced — because a structural blindness in one of its two states meant three early observations became permanent. §8 named the blindness; this measures what it costs.
15. Debugging
| Symptom | Likely cause | How to confirm |
|---|---|---|
| Adaptive worse than both fixed policies | Predictor stuck — §14 | n_closed_and_reused permanently zero; enable tracking |
| Adaptive gains nothing | Workload near the 50% crossover — §4 | The counterfactual costs will be nearly equal |
| Policy oscillates every access | No hysteresis, or CTR_W = 1 | P6; §6's callout |
| Slow to respond to a phase change | Hysteresis, working as designed — §13 | Expected; CTR_W observations to cross |
| Open-page chosen and throughput poor | Hit probability below the crossover | Run the counterfactual; §11 needs no assumption |
| Close-page chosen and latency poor | Every access pays tRCD — §1 | Above 50% hit probability, open-page halves it |
| Close-page's predicted benefit not realised | Precharge not landing in an idle interval | §1's callout; §11's header states the assumption |
| Close-page worse than expected on writes | tWR = 12 against tRTP = 6 — §2 | Write-heavy workloads narrow the hiding window |
| Counter pinned at one end | A sustained phase, or unrepresentative warm-up | §14's report; check the first observations |
err_close_unopened | A close requested for a bank with no open row | Scheduler and policy disagree on bank state |
| Crossover appears to be other than 50% | Correct — it is tRCD/(tRP+tRCD) | §3's callout; 50% is bin-specific |
Adaptive better than cost_perfect | Impossible — P9 | The ceiling is mis-computed |
Row seven is the one most likely to be reported as a policy failure when it is a scheduling interaction. Close-page's entire benefit rests on the precharge happening while nothing waits for it (§1's callout), and if the command bus is busy or the next request to that bank arrives early, the precharge lands on the critical path after all — at which point close-page has paid tRCD and gained nothing.
Row eleven is worth expecting rather than investigating. The crossover is tRCD / (tRP + tRCD), and any bin where those differ produces a different threshold.
16. Misconceptions
“A row stays open because that is how DRAM works.” §1. It stays open because a controller chose not to close it. Closing is a policy with a cost model.
“Close-page eliminates the precharge.” §1's callout. It moves it out of a waiting request's latency into an otherwise idle interval. The saving is exactly tRP, and only if that interval really was idle.
“Open-page is the better default.” §4. It costs 8800 cycles more than close-page on a 10%-locality workload over 1000 accesses. It is the higher-variance policy, spanning a factor of 9 across the workload range.
“The crossover must be measured.” §3. It is tRCD / (tRP + tRCD), derivable from two published numbers, and it depends only on the device.
“The crossover is 50%.” §3's callout. It is 50% at this bin, where tRP = tRCD. Quoting the value without the expression is quoting an artefact.
“Adaptation can only help.” §6. An anti-correlated predictor is 5.5× worse than open-page and 11× worse than a perfect one, because it combines both policies' failure modes.
“Hysteresis is a refinement.” §6's callout. Without it, an alternating access pattern makes the predictor systematically wrong rather than merely noisy.
“The predictor should track the bank's hit rate.” §7. It should predict whether the next access wants the row currently open, which is a different and more local question.
“A page predictor is a branch predictor.” §8's callout. The structure is the same; the difference is that closing a row destroys the evidence needed to evaluate closing it, which a branch never does.
“More counter bits make a better predictor.” §13's row twelve. More bits mean more hysteresis, which means slower response to real phase changes. Two is the minimum that works and more is a trade.
“Closing after a write is the same as after a read.” §2. tWR is 12 cycles against tRTP's 6, so the precharge starts twice as late and the hiding window is half as wide.
“If the assertions pass, the policy is right.” §12's callout and §14's report. Every property held while the policy performed 5× worse than the fixed alternative it replaced.
17. Interview Reasoning
What is the page-policy decision, and when is it made? At the cycle a burst completes: close the row or leave it open. Leaving it open bets the next access to that bank wants the same row.
Give the two policies' costs. Open-page: 0 on a same-row access, tRP + tRCD on a different-row one. Close-page: tRCD always, because the row was already closed.
Derive the crossover. Open-page's expected cost is (1-h)(tRP + tRCD) and close-page's is tRCD. Setting them equal gives h = tRCD / (tRP + tRCD) — 50% where tRP = tRCD, and it depends only on the device.
Does close-page eliminate the precharge? No, it moves it. The precharge happens when the previous burst ends rather than when a request is waiting, so the saving is exactly tRP — and only if that interval was genuinely idle.
What is the ceiling on adaptation? A perfect predictor converts every conflict into a miss, so it beats open-page by the conflict-to-miss ratio, (tRP + tRCD)/tRCD — a factor of 2 at this bin, on the non-hit accesses only.
Can an adaptive policy be worse than both fixed ones? Yes, and badly. An anti-correlated predictor pays close-page's premium where open-page was free and open-page's penalty where close-page was cheap — 5.5× worse than open-page on a 90%-locality workload.
Why is hysteresis mandatory? Because a memoryless predictor on an alternating access pattern is systematically wrong: the outcome it just observed is precisely the wrong guide to the next one.
What should the predictor predict? Not the bank's hit rate — whether the next access to this bank will want the row currently open. That is local, per-burst, and it is what the decision needs.
What is structurally hard about learning it? Closing the row destroys the evidence. In close mode every access is a miss, and whether it would have hit is unobservable unless the precharged row's address was retained.
Your adaptive policy is worse than close-page on a high-locality workload. First hypothesis? That it is stuck — an unrepresentative warm-up drove the counter to the closing side, and without counterfactual tracking no later evidence can move it back.
18. Exercises
-
Recompute §4's five-row table for a device with
tRP = 22andtRCD = 11. Where is the crossover, which policy benefits, and what is the intuition for the direction of the shift? -
Derive the general crossover expression and evaluate it for
tRP/tRCDratios of 1/2, 1, and 2. State the limit astRPgrows without bound and explain it in one sentence. -
§5 gives adaptation's ceiling as the conflict-to-miss ratio on non-hit accesses. Derive the ceiling as a function of
hand show it vanishes ashapproaches 1. What does that say about when a predictor is worth building? -
Construct the access pattern that makes a
CTR_W = 2predictor maximally wrong, and compute its cost against both fixed policies. Then find the smallestCTR_Wthat defeats your pattern, and the pattern that defeats that. -
§14's report has the predictor stuck after three unrepresentative accesses. Compute how many counterfactual observations are needed to recover with
CTR_W = 2andHOLD_THRESHOLD = 2, and design a warm-up rule that prevents the situation without biasing the policy. -
§2 notes
tWRis twicetRTP. Model a workload that is 50% writes and recompute §4's table, stating how you account for the difference in when the precharge may start. Which policy's figure changes? -
§11's counterfactual block needs a lookahead input for
cost_perfect. Argue whether a real controller could approximate it from its own queue, and what the approximation would be worth given 23.4 §5's reordering. -
The counterfactual block assumes close-page's precharge is hidden. Modify it to take a hidden fraction parameter and recompute §4's crossover for fractions of 1.0, 0.5 and 0.0. At which fraction does close-page stop winning anywhere?
19. Module 23, Complete
Five chapters, one question: where does the performance go, and which decisions get it back?
Latency is a decomposition, and one component is unbounded. Chapter 23.1 split a request into six disjoint, exhaustive components that sum exactly, and found that 26 of 33 cycles on a row miss are device timing no controller can reduce. It also found that queue wait has no upper bound in any specification — so all the variance in a latency distribution comes from one component, the mean is the wrong statistic for a right-skewed distribution, and the maximum is nearly free to measure and the single highest-value addition.
Peak is attainable, which changes the question. 23.2 derived 12.8 GB/s and then simulated four workload models spanning 10.3% to exactly 100% of it — so the gap is a property of the workload rather than of DDR. It built measure D, which 12.4 named and did not construct: every cycle charged to exactly one named cause under a strict priority order, with exhaustiveness asserted. And it established that independent fractional costs multiply rather than add, and that a priority-ordered attribution undercounts every lower-priority cause where a higher one coincides — a projection, not a partition of causes.
Locality has a cause, a structure, and a 127× range. 23.3 showed that class counts are a function of the sequence plus the mapping plus the bank state, that two mappings can produce identical counts with opposite temporal structure, and that the pathological property is gcd(stride, blocks-per-row) rather than stride magnitude. It also found the largest single effect in the module: two streams each with perfect locality cost 127.5× more when they collide in one bank — and the collapse is a cliff at two streams, not a gradient.
Reordering is the largest lever, and it needs a bound. 23.4 built FR-FCFS from its published rules and measured both numbers: 87.5% of row work recovered, and a request displaced without bound. A bypass cap of 4 retains 85.7% of the gain with displacement bounded provably — and a regression cannot discover a bound that does not exist, which is why the guarantee has to be an invariant over state rather than an observation over a trace.
And the last decision is algebra. This chapter found that the open-versus-close crossover is tRCD / (tRP + tRCD) — derivable from two published numbers, dependent only on the device — that neither fixed policy wins across workloads, that a perfect predictor's ceiling is the conflict-to-miss ratio, and that a wrong predictor is worse than both fixed policies because it combines their failure modes.
The discipline every chapter repeated, and the one that made a quantitative module possible at all: every number derived from a stated model with verified inputs, and every model's assumptions published alongside its result. Modules 19 to 22 refused to compute what they could not observe. This module had to compute, so the honesty moved from whether to how — three categories on every figure, sum checks on every decomposition, a denominator named for every ratio, and a simulation run rather than an expectation asserted. That last discipline earned its place: the simulations corrected me twice, at 23.3 §3 where two mappings I expected to differ turned out identical, and at 23.3 §8 where the multi-stream collapse proved to be a cliff rather than the gradient I assumed.
And the limit every chapter restated: a counter cannot separate irreducible from avoidable. 23.2 §8 put it structurally — a cycle charged to tCCD_L was genuinely unavailable, and whether the workload should have been in that bank group is a question no counter can reach. Every measurement in five chapters says where the cycles went. None of them says who should have done otherwise, and each chapter's closing callout marks exactly where that boundary falls.
Module 24 turns from performance to power and mobility: what changes when the same architecture has to run on a battery, why LPDDR is a different device rather than a tuned one, and how low-power states interact with everything this module just quantified.
Continue learning
Related tutorials
- Related topic
Row-Buffer Question
Three outcomes, not two — and the cost of an access is a property of the stream rather than of the access. Includes the starvation case the optimisation itself produces, and a property that proves something adjacent to its claim.
- 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.
- Related topic
Restore Operations
Sensing consumed the stored state, so something must put it back. What restoration drives, why it covers a whole row, why a restored row is then cheap to access again, and an educational control model that cannot skip a prerequisite the array is unable to enforce.
- Related topic
Rows
A DRAM row is not an address range. It is the group of cells one shared selection conductor connects at the same instant — and that physical fact is where row granularity, controller-visible row state and state-dependent access cost all come from.
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.
