DDR · Module 23
Scheduling Optimisation
Chapter 17.1 said legality admits and policy chooses, and deferred the quantification here. FR-FCFS saves 87% of row work and displaces a request without bound — and a bounded cap recovers most of the gain.
Chapter 17.1 built the commit point and drew a line it refused to cross. Its clue states the division in six words — “legality admits; policy chooses” — and one of its closing observations names this chapter explicitly: turnarounds on the shared data bus are expensive, and a real scheduler batches to amortise them, which it calls “a policy dimension 17.4 opens and Module 23 quantifies.”
This is that quantification, and the two preceding chapters have already established what is at stake. Chapter 23.3 §8 showed two streams with perfect individual locality costing 127× more when interleaved in one bank, and 23.3 §9 showed batching recovering 99.6% of the damage. Chapter 23.1 §8 showed why that recovery is not free: queue wait is the one latency component with no upper bound, and batching is precisely the mechanism that makes it large.
The module's central law:
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 entirely about the second clause, and it reports both numbers for every decision — because a heuristic that improves throughput and unboundedly delays a request has not obviously improved anything.
1. What 17.1 Refused to Decide
Chapter 17.1's division is worth restating precisely, because this chapter lives entirely on one side of it.
| Owned by | Question | |
|---|---|---|
| Legality | 13.4, 17.1 | May this command issue now? |
| Policy | 17.4, this chapter | Which legal command should issue? |
17.1's own §5 is titled “May Issue Is Not Should Issue”, and its clue observes that with one legal candidate the arbiter's sophistication is irrelevant. That is the condition under which policy does not matter, and it is also the condition under which throughput is whatever the workload gives you.
Policy matters exactly when several commands are simultaneously legal, and 16.1 §2 established the structural reason that happens: many banks can be ready and there is one issue slot. So the scheduler is a chooser, and this chapter is about what it should choose.
One thing it must never do, and 17.1 is emphatic about it: a policy cannot make an illegal command legal. The arbiter below produces a candidate; 17.1's commit gate decides whether it issues. Every result in this chapter assumes that gate is in place and unchanged.
2. The Heuristic, and Where It Comes From
The canonical DRAM scheduling heuristic is FR-FCFS — first-ready, first-come-first-served. It was introduced in “Memory Access Scheduling” by Rixner, Dally, Kapasi, Mattson and Owens, ISCA 2000, which is also where memory access scheduling as a technique was first presented.
Its three priority rules, in order:
| Rule | Prefer | Rationale |
|---|---|---|
| 1. Ready-first | Commands that can issue now | An unready command cannot be issued; considering it wastes the slot |
| 2. Column-first | Column accesses over row commands | A column access on an open row does zero row work (23.3 §2) |
| 3. Oldest-first | Older requests over newer | The only fairness the base policy has |
Rule 2 is the one that produces the gain, and 23.3 already supplies its arithmetic: a row hit costs 0 cycles of row work, a miss 11, a conflict 22. Preferring hits is preferring the zero.
Rule 3 is the one that produces the bound, and §6 shows it is far too weak to provide one.
3. The Trace
Every comparison below uses one trace, stated completely so that nothing is hidden in it.
ILLUSTRATIVE, and constructed rather than sampled. Three streams arrive round-robin, twenty-four requests total:
| Stream | Bank | Row | Requests |
|---|---|---|---|
| X | 0 | 0 | 8 |
| Y | 0 | 5 | 8 |
| Z | 1 | 3 | 8 |
Arrival order is X, Y, Z, X, Y, Z, … — strictly interleaved.
The trace is built to have the property 23.3 §8 identified: X and Y collide in bank 0 on different rows, so serving them alternately makes every access a conflict, while Z on bank 1 is unaffected. Each stream in isolation has near-perfect locality.
Why a constructed trace rather than a captured one. A captured trace would carry properties nobody stated — a particular mix of banks, a particular degree of collision — and a gain measured on it would be a property of the capture. This trace's structure is declared, so the result below is attributable to that structure, and §9's exercises vary it.
4. The Comparison, Both Numbers
Here is the result, DERIVED by simulation using 23.3 §2's verified cost weights. Displacement is how many positions a request was passed over — the fairness cost, measured in queue positions rather than cycles, because 23.1 §8 established that the cycle cost of waiting has no bound.
| Policy | Row work | Hits | Max displacement | Avg displacement |
|---|---|---|---|---|
| FCFS (strict arrival order) | 352 cycles | 7 / 24 | 0 | 0.00 |
| FR-FCFS (uncapped) | 44 cycles | 21 / 24 | 14 | 3.00 |
The gain: 308 cycles of row work saved, which is 87.5%. The hit count rises from 7 to 21 of 24.
The cost: one request was passed over fourteen times. In a 24-request trace, a request waited behind more than half the queue.
That pair is the entire chapter in two rows. FCFS is perfectly fair and leaves 87.5% of the available row work on the table. Uncapped FR-FCFS captures nearly all of it and has no fairness property at all.
5. The Bound, and What It Costs
The fix is a bypass cap: count how many times each request has been passed over, and when a request reaches the cap, force it to the front regardless of rule 2.
That converts an unbounded wait into a provable one, and the proof is short enough to state. If every request is forced after at most C bypasses, then a request at queue position p can be passed over at most C times by each of the p requests ahead of it before each of them is itself forced — so its displacement is bounded by C, and its wait is bounded by the time to serve the requests ahead of it plus C interlopers.
The bound is C, in displacement, and it holds regardless of trace length. That is the property uncapped FR-FCFS does not have at any value.
DERIVED by simulation, same trace, same weights:
| Policy | Row work | Hits | Max displacement | Saving retained |
|---|---|---|---|---|
| FCFS | 352 | 7 / 24 | 0 | 0% |
| FR-FCFS, cap = 2 | 132 | 17 / 24 | 2 | 71.4% |
| FR-FCFS, cap = 4 | 88 | 19 / 24 | 4 | 85.7% |
| FR-FCFS, uncapped | 44 | 21 / 24 | 14 | 100% |
The saving retained is (352 − work) / (352 − 44), recomputed for each row.
A cap of 4 retains 85.7% of the available saving with displacement bounded at 4 rather than unbounded. That is the result worth carrying out of this chapter: the fairness/throughput trade is strongly asymmetric. Most of the throughput benefit comes from a small amount of reordering freedom, and the last fraction is what costs the bound.
| Uncapped | Cap = 4 | |
|---|---|---|
| Row work saved | 308 cycles | 264 cycles |
| Fraction of the benefit | 100% | 85.7% |
| Worst displacement | unbounded | 4, provably |
Giving up 14.3% of the throughput gain buys a bound. Stated that way it is not a close decision, and it is the reason real schedulers are capped rather than pure.
6. Why Rule 3 Is Not a Fairness Property
It is worth being explicit about why FR-FCFS's own oldest-first rule does not provide the bound, because it looks like it should.
Rule 3 applies within a priority class. The rules are ordered, so the comparison is:
candidate A : ready, row HIT, age 2
candidate B : ready, row MISS, age 40
rule 1 (ready) : tie
rule 2 (column) : A wins -- and the comparison STOPS
rule 3 (oldest) : never consultedB's age of 40 is never examined, because rule 2 decided. Age only breaks ties among candidates that are equal on rules 1 and 2 — so among several row hits, the oldest hit wins, which is fair among hits and says nothing about the request that is not a hit.
That is why the cap of §5 is a separate mechanism rather than a tuning of rule 3. It does not reorder the rules; it overrides them when a counter expires. And the override has to be unconditional, because any version that still consults rule 2 can be starved by the same sustained hit stream.
7. The Policy State Machine
The three-way exit from SCAN is the policy in one picture. Two of the three exits implement FR-FCFS's rules; the third exists because those rules have no fairness property (§6), and it overrides them rather than being ranked among them.
And COMMIT hands off rather than issuing. Chapter 17.1 owns what happens next, and this machine deliberately has no state for rejected — if the commit gate refuses, that is legality, and legality is not this chapter's.
8. The FR-FCFS Arbiter
// ---------------------------------------------------------------------
// frfcfs_arbiter -- FR-FCFS selection with a bypass cap.
//
// CLASSIFICATION: educational, synthesisable. Implements the three
// priority rules of Rixner et al., ISCA 2000, plus the starvation
// override those rules do not provide (§6).
//
// WHAT IT DOES NOT MODEL:
// - LEGALITY. `ready` is an INPUT, computed by whoever owns timing
// (13.4). This block never decides whether a command may issue,
// and 17.1's commit gate still has the final say (§1).
// - the queue's storage or its ordering structure (17.2 owns it)
// - the device, or any timing
//
// THE RULES, in order: ready-first, column-first (row hits), then
// oldest-first. And an OVERRIDE, which is not a fourth rule: it
// replaces the decision entirely when a request reaches its cap,
// because §6 shows rule 3 can never rescue a starving request.
// ---------------------------------------------------------------------
module frfcfs_arbiter #(
parameter int ENTRIES = 16,
// Bypass cap: how many times a request may be passed over before it
// is forced. §5's bound is exactly this value, in displacement.
// A cap of 0 degenerates to FCFS; a very large cap to uncapped
// FR-FCFS -- both legal, and §5 quantifies the trade between them.
parameter int BYPASS_CAP = 4,
parameter int IDX_W = (ENTRIES <= 1) ? 1 : $clog2(ENTRIES),
// A COUNT of bypasses up to BYPASS_CAP needs clog2(BYPASS_CAP+1).
parameter int CAP_W = (BYPASS_CAP <= 0) ? 1 : $clog2(BYPASS_CAP + 1),
// A COUNT of entries up to ENTRIES needs clog2(ENTRIES+1).
parameter int OCC_W = $clog2(ENTRIES + 1),
parameter int AGE_W = 16
) (
input logic clk,
input logic rst_n,
// ── The queue, as parallel vectors. 17.2 owns the storage; this is
// the view a policy needs.
input logic [ENTRIES-1:0] valid,
// Legality, from 13.4's checker. NOT computed here.
input logic [ENTRIES-1:0] ready,
// Row hit, from 23.3's locality monitor. Rule 2's input.
input logic [ENTRIES-1:0] is_row_hit,
// Arrival order. Lower index = older, maintained by the queue.
input logic advance, // a selection was consumed
// ── The selection.
output logic sel_valid,
output logic [IDX_W-1:0] sel_index,
// Which rule produced it: 0 = hit, 1 = oldest ready, 2 = forced.
output logic [1:0] sel_reason,
// ── Observability. The two numbers §4 reports, live.
output logic [CAP_W-1:0] worst_bypass,
output logic [AGE_W-1:0] cnt_selected,
output logic [AGE_W-1:0] cnt_hits_selected,
output logic [AGE_W-1:0] cnt_forced,
output logic [OCC_W-1:0] occupancy,
// Any request currently at the cap. §9's P3 requires it to win.
output logic starving,
output logic err_select_not_ready,
output logic err_cap_exceeded
);
initial begin
if (ENTRIES < 1)
$fatal(1, "frfcfs_arbiter: ENTRIES must be at least 1");
if (BYPASS_CAP < 0)
$fatal(1, "frfcfs_arbiter: BYPASS_CAP must not be negative");
end
localparam logic [1:0] R_HIT = 2'd0;
localparam logic [1:0] R_OLD = 2'd1;
localparam logic [1:0] R_FORCE = 2'd2;
logic [CAP_W-1:0] bypass [ENTRIES];
// Declared above the assigns that read them.
logic [IDX_W-1:0] pick;
logic [1:0] reason;
logic found;
// ── Candidate sets. A candidate must be valid AND ready: rule 1,
// and it is a filter rather than a preference -- an unready
// entry is not ranked lower, it is not a candidate at all.
logic [ENTRIES-1:0] cand, cand_hit, at_cap;
always_comb begin
for (int i = 0; i < ENTRIES; i++) begin
cand[i] = valid[i] && ready[i];
cand_hit[i] = cand[i] && is_row_hit[i];
at_cap[i] = valid[i] && (BYPASS_CAP > 0)
&& (bypass[i] >= CAP_W'(BYPASS_CAP));
end
end
// ── THE SELECTION. Priority: override, then rule 2, then rule 3.
// Each search takes the LOWEST index, which is the oldest --
// that is rule 3, applied inside each class rather than across
// classes (§6).
always_comb begin
pick = '0;
reason = R_OLD;
found = 1'b0;
// Override first. A starving request wins outright, and note it
// must also be a candidate -- forcing an UNREADY request would
// hand 17.1's gate something it must reject, wasting the slot.
for (int i = ENTRIES - 1; i >= 0; i--)
if (at_cap[i] && cand[i]) begin
pick = IDX_W'(i); reason = R_FORCE; found = 1'b1;
end
// Rule 2: oldest ready row hit.
if (!found)
for (int i = ENTRIES - 1; i >= 0; i--)
if (cand_hit[i]) begin
pick = IDX_W'(i); reason = R_HIT; found = 1'b1;
end
// Rule 3: oldest ready anything.
if (!found)
for (int i = ENTRIES - 1; i >= 0; i--)
if (cand[i]) begin
pick = IDX_W'(i); reason = R_OLD; found = 1'b1;
end
end
logic [CAP_W-1:0] worst;
logic [AGE_W-1:0] n_sel, n_hit, n_force;
logic [OCC_W-1:0] occ;
logic bad_sel, cap_over;
always_comb begin
occ = '0;
for (int i = 0; i < ENTRIES; i++) if (valid[i]) occ = occ + OCC_W'(1);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < ENTRIES; i++) bypass[i] <= '0;
worst <= '0; n_sel <= '0; n_hit <= '0; n_force <= '0;
bad_sel <= 1'b0; cap_over <= 1'b0;
end else begin
bad_sel <= 1'b0;
cap_over <= 1'b0;
if (advance && found) begin
// ── Charge a bypass to every OLDER valid entry that was not
// selected. Only older ones: a newer entry being skipped is
// not being passed over, it simply has not waited yet.
for (int i = 0; i < ENTRIES; i++) begin
if (valid[i] && (IDX_W'(i) < pick)) begin
if (bypass[i] == {CAP_W{1'b1}}) cap_over <= 1'b1;
else bypass[i] <= bypass[i] + CAP_W'(1);
if ((bypass[i] + CAP_W'(1)) > worst) worst <= bypass[i] + CAP_W'(1);
end
end
// The selected entry's counter clears: it is being served.
bypass[pick] <= '0;
if (n_sel != {AGE_W{1'b1}}) n_sel <= n_sel + AGE_W'(1);
if (reason == R_HIT && n_hit != {AGE_W{1'b1}}) n_hit <= n_hit + AGE_W'(1);
if (reason == R_FORCE && n_force != {AGE_W{1'b1}}) n_force <= n_force + AGE_W'(1);
if (!ready[pick]) bad_sel <= 1'b1;
end
end
end
assign sel_valid = found;
assign sel_index = pick;
assign sel_reason = reason;
assign worst_bypass = worst;
assign cnt_selected = n_sel;
assign cnt_hits_selected = n_hit;
assign cnt_forced = n_force;
assign occupancy = occ;
assign starving = |at_cap;
assign err_select_not_ready = bad_sel;
assign err_cap_exceeded = cap_over;
endmoduleThree details carry the chapter's argument.
The search loops run downward and take the lowest index. Iterating from high to low and overwriting means the lowest surviving index wins, which is the oldest entry — rule 3, applied inside each class. Running them upward with a break would be equivalent; running them upward without one selects the newest, which is rule 3 inverted and produces a scheduler that starves its oldest request by construction.
The override requires cand[i], not just at_cap[i]. Forcing a request that is not ready hands 17.1's commit gate something it must reject, which wastes the issue slot and does not serve the starving request either. A starving-but-unready request has to wait for legality, and §12's corner-case table says so.
Only older entries are charged a bypass. An entry newer than the selection was not passed over — it had not arrived in priority terms. Charging every unselected entry inflates the counters, trips the cap early, and degrades the policy toward FCFS for a reason that is purely a bookkeeping error.
9. What the Assertions Prove
// Bind unit note: P1-P7 reference frfcfs_arbiter's internals and are
// written as though bound into it, so clk and rst_n are visible.
// P8-P10 reference starvation_bound likewise.
// P1 -- a selection is always a candidate: valid and ready. Rule 1 is
// a filter, not a preference, and this is what makes it one.
property p_selection_is_candidate;
@(posedge clk) disable iff (!rst_n)
sel_valid |-> (valid[sel_index] && ready[sel_index]);
endproperty
assert property (p_selection_is_candidate);
// P2 -- when a ready row hit exists and nothing is starving, a hit is
// selected. Rule 2, stated as a checkable obligation rather than an
// intention.
property p_rule2_honoured;
@(posedge clk) disable iff (!rst_n)
(sel_valid && !starving && (|cand_hit)) |-> (sel_reason == 2'd0);
endproperty
assert property (p_rule2_honoured);
// P3 -- THE fairness property. When a request is at its cap and is a
// candidate, it is selected. Without this the override is decorative
// and §5's bound does not exist.
property p_starving_request_wins;
@(posedge clk) disable iff (!rst_n)
(sel_valid && starving && (|(at_cap & cand)))
|-> (sel_reason == 2'd2);
endproperty
assert property (p_starving_request_wins);
// P4 -- rule 3 within a class: the selected hit is the oldest hit.
// Lower index is older, so no candidate hit may have a lower index.
property p_oldest_hit_selected;
@(posedge clk) disable iff (!rst_n)
(sel_valid && (sel_reason == 2'd0))
|-> ((cand_hit & ((1 << sel_index) - 1)) == '0);
endproperty
assert property (p_oldest_hit_selected);
// P5 -- the selected entry's bypass counter clears. A counter that
// survived service would force the same entry repeatedly.
property p_served_entry_clears;
@(posedge clk) disable iff (!rst_n)
(advance && sel_valid) |=> (bypass[$past(sel_index, 1)] == '0);
endproperty
assert property (p_served_entry_clears);
// P6 -- no bypass counter ever exceeds the cap. §5's bound, as an
// invariant over the state rather than an observation about a trace.
property p_bypass_within_cap;
@(posedge clk) disable iff (!rst_n)
(BYPASS_CAP > 0) |-> (worst_bypass <= CAP_W'(BYPASS_CAP));
endproperty
assert property (p_bypass_within_cap);
// P7 -- a newer entry is never charged a bypass. §8's third point:
// charging everything degrades the policy toward FCFS silently.
property p_only_older_charged;
@(posedge clk) disable iff (!rst_n)
(advance && sel_valid && (sel_index == '0))
|=> (worst_bypass == $past(worst_bypass, 1));
endproperty
assert property (p_only_older_charged);
// ── Cover.
cover property (@(posedge clk) disable iff (!rst_n) sel_reason == 2'd0);
cover property (@(posedge clk) disable iff (!rst_n) sel_reason == 2'd1);
// The override actually fired -- a suite that never starves a request
// has not tested the only mechanism providing a bound.
cover property (@(posedge clk) disable iff (!rst_n) sel_reason == 2'd2);
// A request at the cap that is NOT ready: §12's corner case, where the
// override cannot fire and the request waits on legality instead.
cover property (@(posedge clk) disable iff (!rst_n)
starving && ((at_cap & cand) == '0));
// A hit available and passed over because something was starving --
// the precise moment throughput is traded for the bound.
cover property (@(posedge clk) disable iff (!rst_n)
sel_valid && (|cand_hit) && (sel_reason == 2'd2));
// BYPASS_CAP = 0: the FCFS degenerate case.
cover property (@(posedge clk) disable iff (!rst_n)
(BYPASS_CAP == 0) && sel_valid);
// Full queue with a selection: the arbiter under pressure.
cover property (@(posedge clk) disable iff (!rst_n)
sel_valid && (occupancy == OCC_W'(ENTRIES)));P3 is the property this chapter exists to make possible. Without it the cap is decorative — the counters increment, the starving flag asserts, and the policy still prefers hits, so the bound of §5 does not exist and §4's callout's unbounded displacement returns.
The fifth cover is the one worth watching in a regression, because it captures the exact cycle on which the trade is made: a row hit was available and was passed over to serve a starving request. Every such cycle is throughput given up for the bound, and counting them quantifies §5's 14.3%.
10. The Starvation Bound, Separately
§8's arbiter enforces the cap inline. It is worth having the bound as a separate, auditable block as well, because a fairness guarantee that is entangled with a policy is hard to reason about and easy to weaken by accident.
// ---------------------------------------------------------------------
// starvation_bound -- an independent watchdog on how long a request
// may go unserved, auditable separately from the policy.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHY SEPARATE: a fairness guarantee entangled with a policy is hard
// to reason about and easy to weaken by accident. This block observes
// selections and knows nothing about the rules that produced them, so
// it holds whatever policy is installed -- including a future one.
//
// WHAT IT DOES NOT MODEL:
// - the policy, the queue's storage, or legality
// - a CYCLE bound. §9's callout: the provable bound is positional,
// because converting positions to cycles needs the workload.
// ---------------------------------------------------------------------
module starvation_bound #(
parameter int ENTRIES = 16,
// Limit in SELECTIONS, not cycles. §9's callout says why.
parameter int MAX_WAIT = 32,
parameter int IDX_W = (ENTRIES <= 1) ? 1 : $clog2(ENTRIES),
parameter int W_W = (MAX_WAIT <= 1) ? 1 : $clog2(MAX_WAIT + 2),
parameter int OCC_W = $clog2(ENTRIES + 1)
) (
input logic clk,
input logic rst_n,
input logic [ENTRIES-1:0] valid,
input logic sel_valid,
input logic [IDX_W-1:0] sel_index,
// ── Per-entry wait, in selections observed while that entry was
// valid and not chosen.
output logic [W_W-1:0] worst_wait,
output logic [IDX_W-1:0] worst_entry,
output logic over_limit,
output logic [ENTRIES-1:0] at_limit_mask,
output logic [15:0] cnt_violations,
output logic err_select_invalid_entry
);
initial begin
if (MAX_WAIT < 1)
$fatal(1, "starvation_bound: MAX_WAIT must be at least 1");
if (ENTRIES < 1)
$fatal(1, "starvation_bound: ENTRIES must be at least 1");
end
logic [W_W-1:0] wait_cnt [ENTRIES];
logic [W_W-1:0] worst;
logic [IDX_W-1:0] worst_i;
logic [15:0] n_viol;
logic bad_sel;
logic [ENTRIES-1:0] over;
always_comb
for (int i = 0; i < ENTRIES; i++)
over[i] = valid[i] && (wait_cnt[i] >= W_W'(MAX_WAIT));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < ENTRIES; i++) wait_cnt[i] <= '0;
worst <= '0; worst_i <= '0; n_viol <= '0; bad_sel <= 1'b0;
end else begin
bad_sel <= 1'b0;
if (sel_valid) begin
if (!valid[sel_index]) bad_sel <= 1'b1;
for (int i = 0; i < ENTRIES; i++) begin
if (!valid[i]) begin
// An invalid slot has no wait. Not clearing it means a
// freshly allocated entry inherits the previous
// occupant's wait -- and is forced immediately.
wait_cnt[i] <= '0;
end else if (IDX_W'(i) == sel_index) begin
wait_cnt[i] <= '0;
end else begin
if (wait_cnt[i] != {W_W{1'b1}}) begin
wait_cnt[i] <= wait_cnt[i] + W_W'(1);
if ((wait_cnt[i] + W_W'(1)) > worst) begin
worst <= wait_cnt[i] + W_W'(1);
worst_i <= IDX_W'(i);
end
if ((wait_cnt[i] + W_W'(1)) == W_W'(MAX_WAIT)
&& (n_viol != 16'hFFFF))
n_viol <= n_viol + 16'd1;
end
end
end
end
end
end
assign worst_wait = worst;
assign worst_entry = worst_i;
assign over_limit = |over;
assign at_limit_mask = over;
assign cnt_violations = n_viol;
assign err_select_invalid_entry = bad_sel;
endmodule// P8 -- the served entry's wait clears. The same discipline as P5, in
// the watchdog: a wait that survived service would report a permanent
// violation.
property p_watchdog_clears_on_service;
@(posedge clk) disable iff (!rst_n)
sel_valid |=> (wait_cnt[$past(sel_index, 1)] == '0);
endproperty
assert property (p_watchdog_clears_on_service);
// P9 -- an invalid entry never carries a wait, so a newly allocated
// entry starts at zero rather than inheriting its predecessor's.
property p_invalid_entry_has_no_wait;
@(posedge clk) disable iff (!rst_n)
sel_valid |=> ((|(~valid)) -> (wait_cnt[0] == '0) || valid[0]);
endproperty
assert property (p_invalid_entry_has_no_wait);
// P10 -- a violation is counted once per crossing, not once per cycle
// above the limit. Counting per cycle makes the figure a duration
// rather than an event count.
property p_violation_counted_once;
@(posedge clk) disable iff (!rst_n)
(cnt_violations != $past(cnt_violations, 1)) |-> sel_valid;
endproperty
assert property (p_violation_counted_once);
// ── Cover: a violation actually occurred with the arbiter's cap
// disabled -- proving the watchdog catches what the cap would prevent.
cover property (@(posedge clk) disable iff (!rst_n) over_limit);
cover property (@(posedge clk) disable iff (!rst_n)
(worst_wait > '0) && !over_limit);The watchdog exists to be independent, and P9 is why. Clearing an invalid entry's wait sounds like housekeeping; omitting it means a freshly allocated queue slot inherits the previous occupant's accumulated wait and is forced immediately — which looks like aggressive fairness and is a bookkeeping bug that degrades the policy toward FCFS for entries in frequently reused slots.
11. The Other Batching — Amortising Turnaround
Everything so far has batched by row. Chapter 17.1's handoff actually pointed at a different batching, and it is worth quoting again precisely: turnarounds on the shared data bus are expensive, and “a real scheduler batches to amortise them.”
That is a second, independent policy dimension, and 23.2 §7 already established the cost it addresses: alternating reads and writes in one bank group achieves 50.0% of peak, with half the window spent on direction changes.
The mechanism is the same shape as §5's cap, with a different objective: serve B reads, then B writes, so one direction change is amortised over 2B bursts instead of over 2.
DERIVED, using 23.2 §7's components — tWTR_L = 6 VERIFIED, plus the read-to-write gap of 2 which that section labelled ILLUSTRATIVE and explained why no single verified value exists:
Batch B | Data cycles | Turnaround | Total | Utilisation | Achieved | A write waits behind |
|---|---|---|---|---|---|---|
| 1 (alternating) | 8 | 8 | 16 | 50.0% | 6.400 GB/s | 1 read |
| 2 | 16 | 8 | 24 | 66.7% | 8.533 GB/s | 2 reads |
| 4 | 32 | 8 | 40 | 80.0% | 10.240 GB/s | 4 reads |
| 8 | 64 | 8 | 72 | 88.9% | 11.378 GB/s | 8 reads |
| 16 | 128 | 8 | 136 | 94.1% | 12.047 GB/s | 16 reads |
Three results, and the third is the one worth carrying.
The returns diminish geometrically while the latency cost doubles. Each doubling of B buys less than the last — 16.67, 13.33, 8.89, then 5.23 percentage points — and each doubling exactly doubles how long a write waits. DERIVED, and it is the same asymmetry §5 found in the fairness trade, arriving independently.
The ceiling is 100% and it is never reached. As B grows, turnaround amortises toward nothing, so the limit is peak. But every increment costs latency, so the curve is approached and not attained — which is why a batch depth is a tuning parameter rather than a maximisation.
And at B = 4 turnaround stops being the binding constraint. A batch of four reaches exactly 80.0% — which is precisely 23.2 §3's model B figure, the reads-only ceiling imposed by tCCD_L within one bank group. Two independently derived numbers coinciding at that value means something specific: beyond B = 4, further batching is trading latency for a gain that tCCD_L will absorb, unless the workload also alternates bank groups. Batching past the crossover optimises a constraint that is no longer binding.
// ---------------------------------------------------------------------
// direction_batcher -- gates which access direction is eligible, so a
// bus turnaround is amortised over a batch instead of paid per burst.
//
// CLASSIFICATION: educational, synthesisable. Implements the batching
// 17.1 named and deferred to Module 23.
//
// WHAT IT DOES NOT MODEL:
// - the bus or its electrical handover (22.1 owns termination)
// - LEGALITY (13.4), or selection WITHIN a direction (§8's arbiter)
// - the read-to-write interval, which 23.2 §7 establishes is not a
// single published parameter and is therefore a caller value
//
// COMPOSITION: this block chooses the DIRECTION; frfcfs_arbiter
// chooses WITHIN it (§11's callout). Merging them would make the two
// batching objectives compete inside one priority chain.
// ---------------------------------------------------------------------
module direction_batcher #(
// Bursts served in one direction before a switch is allowed.
// §11: the returns diminish geometrically and the latency cost
// doubles, so this is a tuning parameter with no optimal value.
parameter int BATCH = 4,
// A starvation cap on the OPPOSITE direction, in batches. Without
// it a continuous stream in one direction starves the other --
// exactly §4's unbounded displacement, one level up.
parameter int OPP_CAP = 2,
parameter int B_W = (BATCH <= 1) ? 1 : $clog2(BATCH + 1),
parameter int C_W = (OPP_CAP <= 1) ? 1 : $clog2(OPP_CAP + 2)
) (
input logic clk,
input logic rst_n,
// ── What is pending, by direction.
input logic rd_pending,
input logic wr_pending,
// A burst was served in the current direction.
input logic burst_served,
// ── The gate. Exactly one is high when anything is pending.
output logic rd_eligible,
output logic wr_eligible,
output logic switching, // a turnaround is being paid
// ── Observability. The two numbers §11's table reports.
output logic [B_W-1:0] served_in_batch,
output logic [15:0] cnt_switches,
output logic [15:0] cnt_bursts,
output logic [C_W-1:0] opp_starved_batches,
output logic forced_switch,
output logic err_both_eligible,
output logic err_served_with_nothing_pending
);
initial begin
if (BATCH < 1)
$fatal(1, "direction_batcher: BATCH must be at least 1");
if (OPP_CAP < 1)
// A cap of zero would switch on every batch regardless of
// demand, which is BATCH=1 with extra steps.
$fatal(1, "direction_batcher: OPP_CAP must be at least 1");
end
logic dir_is_write;
logic [B_W-1:0] in_batch;
logic [C_W-1:0] opp_wait;
logic [15:0] n_sw, n_bu;
logic sw, forced;
// The opposite direction has something pending. Declared above the
// assigns that read it.
logic opp_pending;
assign opp_pending = dir_is_write ? rd_pending : wr_pending;
logic cur_pending;
assign cur_pending = dir_is_write ? wr_pending : rd_pending;
// ── A switch is due when the batch is full, or when the opposite
// direction has waited OPP_CAP batches, or when nothing is
// pending in the current direction and something is in the other.
logic batch_full, opp_starving, drained;
assign batch_full = (in_batch >= B_W'(BATCH));
assign opp_starving = (opp_wait >= C_W'(OPP_CAP));
assign drained = !cur_pending && opp_pending;
logic switch_due;
assign switch_due = opp_pending && (batch_full || opp_starving || drained);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dir_is_write <= 1'b0; // start on reads; arbitrary and stated
in_batch <= '0;
opp_wait <= '0;
n_sw <= '0; n_bu <= '0;
sw <= 1'b0; forced <= 1'b0;
end else begin
sw <= 1'b0;
forced <= 1'b0;
if (switch_due) begin
dir_is_write <= ~dir_is_write;
in_batch <= '0;
// The direction we are LEAVING has now waited zero batches;
// the one we are entering had its wait satisfied.
opp_wait <= '0;
sw <= 1'b1;
if (opp_starving) forced <= 1'b1;
if (n_sw != 16'hFFFF) n_sw <= n_sw + 16'd1;
end else if (burst_served) begin
if (in_batch != B_W'(BATCH)) in_batch <= in_batch + B_W'(1);
if (n_bu != 16'hFFFF) n_bu <= n_bu + 16'd1;
// Charge a batch of waiting to the opposite direction only when
// a full batch completes -- the unit of OPP_CAP is batches, so
// charging per burst would make the cap BATCH times tighter
// than it reads.
if ((in_batch + B_W'(1)) >= B_W'(BATCH) && opp_pending)
if (opp_wait != {C_W{1'b1}}) opp_wait <= opp_wait + C_W'(1);
end
end
end
assign rd_eligible = !dir_is_write && rd_pending;
assign wr_eligible = dir_is_write && wr_pending;
assign switching = sw;
assign served_in_batch = in_batch;
assign cnt_switches = n_sw;
assign cnt_bursts = n_bu;
assign opp_starved_batches = opp_wait;
assign forced_switch = forced;
assign err_both_eligible = rd_eligible && wr_eligible;
assign err_served_with_nothing_pending =
burst_served && !rd_pending && !wr_pending;
endmoduleTwo assertions belong with it:
// P11 -- exactly one direction is eligible at a time. The gate's whole
// purpose: if both were eligible the turnaround would not be batched
// at all, and §11's table would collapse to its first row.
property p_one_direction_eligible;
@(posedge clk) disable iff (!rst_n)
!(rd_eligible && wr_eligible);
endproperty
assert property (p_one_direction_eligible);
// P12 -- the opposite direction is never starved beyond its cap. The
// same bound as §5, applied to directions rather than to requests --
// and needed for the same reason.
property p_opposite_direction_bounded;
@(posedge clk) disable iff (!rst_n)
(opp_starved_batches <= C_W'(OPP_CAP));
endproperty
assert property (p_opposite_direction_bounded);
// ── Cover: a forced switch -- the direction-level starvation bound
// actually firing, which a read-heavy trace will never reach.
cover property (@(posedge clk) disable iff (!rst_n) forced_switch);
// A switch caused by the current direction draining rather than by the
// batch filling: the cheap switch, which costs the turnaround but no
// latency to anyone.
cover property (@(posedge clk) disable iff (!rst_n)
switching && !$past(batch_full, 1));OPP_CAP exists for exactly §4's reason, one level up. A continuous read stream fills batch after batch, and without a cap on how many batches the write direction may wait, writes are starved without bound — the same structural failure as uncapped FR-FCFS, with directions in place of requests. The bound is in batches, and the comment at the charging site explains why: charging per burst would make the cap BATCH times tighter than its name suggests.
12. Corner Cases
| Case | Behaviour | Why |
|---|---|---|
BYPASS_CAP = 0 | Degenerates to FCFS | Every entry is instantly at cap; §5's first row |
Very large BYPASS_CAP | Degenerates to uncapped FR-FCFS | §4's row, with unbounded displacement |
| Starving request not ready | Override cannot fire; it waits on legality | §8 — forcing it wastes the slot; the fourth cover |
| Two requests at the cap | Lowest index wins | Oldest-first applies within the override too |
| Row hit available, something starving | Override wins; the fifth cover fires | The cycle on which throughput is traded |
| No candidate ready | sel_valid low; nothing charged | An empty candidate set is not a selection |
| Selection at index 0 | No bypass charged to anyone | Nothing is older than the oldest — P7 |
| Entry freed and reallocated | Wait and bypass both clear | Otherwise it inherits the predecessor's — P9 |
| A bypass counter clamping | err_cap_exceeded | The bound is no longer being enforced |
| Full queue | Selection still proceeds | occupancy is observability, not a gate |
ENTRIES = 1 | Always selects entry 0 if ready | Policy is irrelevant with one candidate — 17.1's clue |
| All entries row hits | Rule 2 never discriminates; rule 3 decides | The oldest hit wins, which is FCFS among equals |
And the direction batcher of §11, whose corner cases are separate because it gates a different quantity:
| Case | Behaviour | Why |
|---|---|---|
BATCH = 1 | Switches every burst; 50% utilisation | §11's first row — alternating, with no amortisation |
BATCH very large | Approaches 100%; latency grows with it | The ceiling is never attained, only approached |
| Only one direction pending | No switch; switch_due requires opp_pending | Switching to an empty direction pays a turnaround for nothing |
| Current direction drains mid-batch | Switches early; the second cover fires | The cheap switch — a turnaround with no latency cost to anyone |
OPP_CAP reached | forced_switch; the batch is cut short | §11's bound, at direction granularity |
OPP_CAP = 0 | $fatal at elaboration | It would switch every batch regardless of demand — BATCH = 1 with extra steps |
| Both directions always pending | Strict alternation of full batches | The steady state §11's table describes |
burst_served with nothing pending | err_served_with_nothing_pending | A burst was attributed to a direction that had no request |
BATCH = 4 on a mixed workload | 80.0%, the tCCD_L crossover | §11 — beyond this, batching optimises a non-binding constraint |
Row three is the one that surprises people. A starving request that is not legal cannot be forced, because forcing it hands 17.1's gate a command it must reject — so the slot is wasted and the request is no better off. The bound of §5 is therefore a bound on displacement among candidates, and a request blocked by timing is waiting on 13.4, not on policy.
13. DV — Replay the Comparison
The checker reproduces §4's and §5's table from the RTL, using an independent selector that implements the rules by a different structure.
// Independent policy reference. Implements the three rules by building
// candidate LISTS and sorting, rather than by the DUT's priority
// loops -- so agreement is evidence rather than tautology.
// SIMULATION-ONLY.
class frfcfs_ref;
int unsigned cap;
int unsigned bypass [int];
// Returns the chosen index, or -1.
function int choose(bit [63:0] valid, bit [63:0] ready,
bit [63:0] hit, int n);
int forced = -1, best_hit = -1, best_any = -1;
for (int i = 0; i < n; i++) begin
bit c = valid[i] && ready[i];
if (!c) continue;
if (cap > 0 && bypass.exists(i) && bypass[i] >= cap && forced < 0)
forced = i;
if (hit[i] && best_hit < 0) best_hit = i;
if (best_any < 0) best_any = i;
end
if (forced >= 0) return forced;
if (best_hit >= 0) return best_hit;
return best_any;
endfunction
function void charge(int picked, bit [63:0] valid, int n);
for (int i = 0; i < n; i++)
if (valid[i] && i < picked)
bypass[i] = (bypass.exists(i) ? bypass[i] : 0) + 1;
bypass[picked] = 0;
endfunction
endclass| Check | What it establishes |
|---|---|
| Every selection matches the reference, over §3's trace | Two structures, one policy |
| §4: FCFS gives 352 cycles, 7 hits, displacement 0 | The published row, from the RTL |
| §4: uncapped gives 44 cycles, 21 hits, displacement 14 | The published row |
| §5: cap 2 gives 132 and displacement 2; cap 4 gives 88 and 4 | Both published rows |
| Saving retained: 71.4% and 85.7%, recomputed | §5's asymmetry |
| Worst displacement never exceeds the cap, over 10,000 selections | P6 as a bound, not an observation |
| Sustained hit stream with cap disabled: displacement grows unboundedly | §4's callout, demonstrated |
| Same stream with the cap: displacement plateaus at the cap | The bound doing its job |
| A trace with no collisions: FCFS and FR-FCFS produce the same work | The gain is a trace property |
| Charge bypasses to newer entries too; observe the degradation | §8's third point |
The seventh and eighth checks together are the ones worth running longest:
UNBOUNDED VERSUS BOUNDED DISPLACEMENT
A sustained hit stream to bank 0 row 0, plus one request to
bank 0 row 9 arriving first. 10,000 selections.
BYPASS_CAP = 0 (FCFS)
the row-9 request is served first. displacement 0
row work: every subsequent access to row 0 is a conflict.
BYPASS_CAP disabled (uncapped FR-FCFS)
selections 1..10000 : all row-0 hits preferred
the row-9 request is NEVER selected.
displacement after 10,000 selections : 9,999 and rising
-> it is not large. It is UNBOUNDED, and no trace length
reveals a limit because there is none.
BYPASS_CAP = 4
selections 1..4 : row-0 hits
selection 5 : row-9 request FORCED, reason = 2
displacement : exactly 4, and it plateaus there
-> and it plateaus for every trace length, which is the
difference between a measurement and a bound.
diagnosis : the uncapped case has no worst case to measure. A
regression running 1,000 selections reports displacement 999
and one running 10,000 reports 9,999 -- and neither number is
a property of the design.
what NOT to conclude : that a longer regression would find the
bound. There is no bound to find; §6 explains why rule 3
cannot supply one.
caught by : P6, which is an invariant over the counter state and
therefore holds at every trace length rather than being
confirmed by one.That is the most instructive result in this chapter. A regression cannot discover a bound that does not exist — it can only report the largest displacement it happened to observe, which grows with the run length. Only an invariant over the state distinguishes “large” from “unbounded”, and that is what P6 is.
14. Debugging
| Symptom | Likely cause | How to confirm |
|---|---|---|
| Throughput unchanged by FR-FCFS | No locality in the arrival order to recover | §13's ninth check; the gain is a trace property |
| Some requests take enormously long | Cap disabled or very large — §4 | worst_bypass; P6; cnt_forced will be zero |
| Worst displacement grows with run length | Unbounded, not merely large — §13 | There is no bound to find; add a cap |
| Displacement bounded but throughput poor | Cap too small — §5 | Sweep it; 71.4% at cap 2 versus 85.7% at cap 4 |
cnt_forced very high | The cap is binding constantly | The policy is effectively FCFS; raise the cap |
| Oldest request never served despite the cap | It is not ready — §12 | The override needs a candidate; check 13.4 |
| Selections rejected by the commit gate | Policy chose an illegal command | err_select_not_ready; ready is an input and must be correct |
| Policy degrades toward FCFS unexpectedly | Bypasses charged to newer entries — §8 | P7; or a wait inherited by a reused slot — P9 |
| Hit rate high, throughput still poor | Not a scheduling problem | 23.3 §19's table; check bank groups and turnaround |
| Latency improved, bandwidth fell | The cap was lowered | §5's trade, in the other direction |
err_cap_exceeded | A bypass counter clamped | The bound is no longer enforced; widen CAP_W |
| Two identical runs, different worst displacement | Expected when uncapped | Another symptom that there is no bound |
| Utilisation stuck near 50% on a mixed workload | BATCH = 1 — no turnaround amortisation | §11's first row; raise BATCH |
Raising BATCH past 4 gains nothing | tCCD_L is now binding, not turnaround | §11's crossover; alternate bank groups instead |
| Writes take enormously long | OPP_CAP too large, or absent | opp_starved_batches; P12 |
cnt_switches far exceeds cnt_bursts / (2 × BATCH) | Direction draining constantly | Not a bug; the workload is not batchable at this depth |
| The two batchers appear to fight | Expected — §11's callout | Direction gates the set; FR-FCFS orders within it |
Row three is the diagnostic that matters most, and it is easy to misread. A displacement figure that grows with run length is not a large bound — it is the absence of one, and no amount of further testing will produce the number. Row twelve is the same fact from another angle.
15. Misconceptions
“FR-FCFS is a fair policy.” §6. Rule 3 only breaks ties within a priority class, so an older request losing on rule 2 is never rescued. Uncapped displacement is unbounded.
“Reordering is free throughput.” §4. It cost one request fourteen positions on a 24-request trace, and unboundedly many on a longer one.
“The 14 in §4 is the worst case.” §4's callout. It is what happened on that trace. There is no worst case without a cap.
“A longer regression will find the bound.” §13. There is nothing to find. Only an invariant over the counter state distinguishes bounded from unbounded.
“A cap costs most of the benefit.” §5. A cap of 4 retains 85.7% of the available saving. The trade is strongly asymmetric in the bound's favour.
“FR-FCFS improves throughput by a known percentage.” §2's callout. The gain depends entirely on how much locality the arrival order was destroying — 23.3 §8 showed that ranges from nothing to 127×.
“The scheduler decides what is legal.” §1. 13.4 and 17.1 own legality; ready is an input here, and policy only chooses among legal candidates.
“A starving request is always served next.” §12. Only if it is also ready. Forcing an illegal command wastes the slot and helps nobody.
“Charge every unselected entry a bypass.” §8. Only older ones were passed over. Charging all of them trips the cap early and degrades the policy toward FCFS silently.
“The bound is in cycles.” §9's callout. It is in queue positions. Converting to cycles needs the intervening requests' access classes, which is workload-dependent.
“A fairness watchdog is redundant if the arbiter caps.” §10. Entangling the guarantee with the policy makes it easy to weaken by accident; an independent watchdog holds for whatever policy is installed.
“High hit rate means the scheduler is working.” §14's ninth row. A high hit rate with poor throughput points at bank groups or turnaround, which 23.2 owns.
16. Interview Reasoning
What are FR-FCFS's rules? Ready-first, column-first, oldest-first — from Rixner et al., ISCA 2000. Ready is a filter; column-first prefers row hits, which cost zero row work; oldest-first breaks ties.
Which rule produces the gain? Column-first. A hit does no row work, a miss costs tRCD, a conflict tRP + tRCD — so preferring hits is preferring the zero.
Is FR-FCFS fair? No. Oldest-first only applies within a priority class, so a request that keeps losing on column-first is never reached. Displacement is unbounded.
Quantify the trade on a colliding trace. On a 24-request trace with two streams colliding in one bank: FCFS costs 352 cycles of row work with zero displacement; uncapped FR-FCFS costs 44 with a displacement of 14. An 87.5% saving, and one request passed over fourteen times.
How do you bound it? A bypass cap: count how often each request is passed over and force it at the limit. The bound is the cap, in queue positions, and it holds at any trace length.
What does the cap cost? Less than expected. A cap of 4 retains 85.7% of the saving; a cap of 2 retains 71.4%. Most of the throughput benefit comes from a small amount of reordering freedom.
Why isn't the bound in cycles? Because converting positions to cycles requires knowing how long the intervening requests take, which depends on their access classes and therefore on the workload. The provable bound is positional.
A starving request reaches its cap but is not ready. What happens? It waits. Forcing an illegal command hands the commit gate something it must reject — the slot is wasted and the request is no better off. It is blocked by timing, not by policy.
Your worst-observed displacement grows with regression length. What does that tell you? That there is no bound. A growing observation is the signature of an unbounded quantity, and no longer run will produce the limit — only an invariant over the counter state can.
FR-FCFS gives you no throughput gain at all. Is it broken? Probably not. The gain depends on how much locality the arrival order destroyed; if requests already arrive grouped by row, there is nothing to recover and FR-FCFS reduces to FCFS.
17. Exercises
-
Construct a 24-request trace on which FR-FCFS and FCFS produce identical row work. State the property your trace has, and relate it to 23.3 §9's batching result.
-
Sweep
BYPASS_CAPfrom 0 to 16 on §3's trace and plot row work against worst displacement. Identify the knee, and argue whether 4 is the right choice or an artefact of a 24-request trace. -
§8 charges bypasses only to older entries. Change it to charge all valid entries and re-run §5's table. Which property fires, and by how much does the retained saving fall at each cap?
-
Implement rule 2 as a preference within oldest-first rather than above it — oldest ready request, with hits as a tiebreak. Re-run §4. Explain the result from §6, and say which policy you would ship.
-
§9's callout says the bound is positional. Derive the cycle bound for §3's trace given 23.3 §2's weights and a cap of 4, stating every assumption. Which assumption is the one that makes it workload-dependent?
-
Add a second priority class — a high-priority requester that must be served within 8 positions regardless of class — and state what it does to §5's bound for everyone else. Is the result still provable?
-
§13's seventh check runs 10,000 selections without finding a bound. Write the formal argument that no bound exists for uncapped FR-FCFS, and identify the single assumption about the workload it requires.
-
The arbiter's override requires a starving request to be ready. Construct the scenario where this permanently starves a request despite the cap, and say which chapter owns the fix.
18. Where This Goes
Scheduling policy now has both numbers. FR-FCFS's three published rules recover 87.5% of the row work a colliding arrival order destroys, and uncapped they displace a request without bound — not by a large amount, but with no limit that any regression can discover. A bypass cap of 4 retains 85.7% of the gain and bounds displacement provably, which makes the trade asymmetric enough that the decision is not close.
Two results transfer. The gain is a property of the trace, not of the heuristic — 23.3 §8 showed the available recovery ranging from nothing to 127×, so a scheduler's value cannot be stated without the workload. And the provable bound is positional rather than temporal, because converting queue positions to cycles requires the intervening requests' access classes.
What every chapter so far has assumed is that a row, once opened, stays open until something else needs the bank. That assumption is doing more work than it appears to. Chapter 23.3 §2's cost model charges a conflict tRP + tRCD precisely because the old row was still open when the new request arrived — and if the controller had closed it earlier, that access would have been a miss at tRCD instead, costing 11 cycles rather than 22.
So there is a decision nobody has made yet: when a burst completes, should the row stay open or close immediately? Keeping it open bets that the next access to that bank wants the same row. Closing it bets that it does not. Chapter 23.5 takes that up — open-page against close-page, why neither wins across workloads, what an adaptive policy needs to predict, and why a policy that changes its mind on every access performs worse than either fixed choice.
Continue learning
Related tutorials
- Related topic
Arbitration
Arbitration runs after legality has already decided. It narrows subsets rather than comparing priority numbers, advances its pointer only on commit, and needs a bounded bypass because row-hit-first genuinely starves.
- Related topic
Bank-Aware Scheduling
A bank-aware controller evaluates every bank's candidate at once and produces a legal-candidate vector. The discipline that makes it correct is legality before policy — and the bug that defeats it is replacing timing legality with a bank comparison.
- Related topic
Performance Optimisation
Two legal scheduling choices, identical makespan, identical row-hit rate — and one request waits seventeen cycles instead of five. Row-hit rate and bank-level parallelism are different quantities that conflict.
- 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.
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.
