Skip to content
VLSI Mentor

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 byQuestion
Legality13.4, 17.1May this command issue now?
Policy17.4, this chapterWhich 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:

RulePreferRationale
1. Ready-firstCommands that can issue nowAn unready command cannot be issued; considering it wastes the slot
2. Column-firstColumn accesses over row commandsA column access on an open row does zero row work (23.3 §2)
3. Oldest-firstOlder requests over newerThe 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:

StreamBankRowRequests
X008
Y058
Z138

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.

PolicyRow workHitsMax displacementAvg displacement
FCFS (strict arrival order)352 cycles7 / 2400.00
FR-FCFS (uncapped)44 cycles21 / 24143.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:

PolicyRow workHitsMax displacementSaving retained
FCFS3527 / 2400%
FR-FCFS, cap = 213217 / 24271.4%
FR-FCFS, cap = 48819 / 24485.7%
FR-FCFS, uncapped4421 / 2414100%

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.

UncappedCap = 4
Row work saved308 cycles264 cycles
Fraction of the benefit100%85.7%
Worst displacementunbounded4, 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 consulted

B'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 scheduling policy state machine, with six states. The machine starts in idle, where no request is pending. When a request is pending it moves to scan, which examines the queue and classifies every candidate as ready or not ready and as a row hit or not. Scan has three exits, corresponding to the priority rules and the override. If a ready row hit exists and no request has reached its bypass cap, it moves to pick hit, which selects the oldest such hit. If no ready hit exists, it moves to pick oldest, which selects the oldest ready request regardless of class. And if any request has reached its bypass cap, it moves to force oldest, which selects that request unconditionally and overrides the hit preference entirely. All three selection states converge on commit, which hands the chosen candidate to the commit gate of chapter seventeen point one, where legality is decided and this machine has no say. Commit returns to idle. The three-way exit from scan is the whole policy: two priority rules and one override that exists because the rules alone have no fairness property.IDLESCANQUEUEPICK HITPICKOLDESTFORCEOLDESTCOMMITrequest pendingrequest pendinga ready row hit existsa ready rowhit existsno hit: take the oldestno hit: take the oldestno hit:take the…a request hit its capa request hit its capcandidate selectedcandidate selectedcandidate selectedcandidate selectedoverride selectedoverride selectedhanded to 17.1's gatehanded to 17.1's gate

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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;

endmodule

Three 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 BData cyclesTurnaroundTotalUtilisationAchievedA write waits behind
1 (alternating)881650.0%6.400 GB/s1 read
21682466.7%8.533 GB/s2 reads
43284080.0%10.240 GB/s4 reads
86487288.9%11.378 GB/s8 reads
16128813694.1%12.047 GB/s16 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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;

endmodule

Two assertions belong with it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

CaseBehaviourWhy
BYPASS_CAP = 0Degenerates to FCFSEvery entry is instantly at cap; §5's first row
Very large BYPASS_CAPDegenerates to uncapped FR-FCFS§4's row, with unbounded displacement
Starving request not readyOverride cannot fire; it waits on legality§8 — forcing it wastes the slot; the fourth cover
Two requests at the capLowest index winsOldest-first applies within the override too
Row hit available, something starvingOverride wins; the fifth cover firesThe cycle on which throughput is traded
No candidate readysel_valid low; nothing chargedAn empty candidate set is not a selection
Selection at index 0No bypass charged to anyoneNothing is older than the oldest — P7
Entry freed and reallocatedWait and bypass both clearOtherwise it inherits the predecessor's — P9
A bypass counter clampingerr_cap_exceededThe bound is no longer being enforced
Full queueSelection still proceedsoccupancy is observability, not a gate
ENTRIES = 1Always selects entry 0 if readyPolicy is irrelevant with one candidate — 17.1's clue
All entries row hitsRule 2 never discriminates; rule 3 decidesThe 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:

CaseBehaviourWhy
BATCH = 1Switches every burst; 50% utilisation§11's first row — alternating, with no amortisation
BATCH very largeApproaches 100%; latency grows with itThe ceiling is never attained, only approached
Only one direction pendingNo switch; switch_due requires opp_pendingSwitching to an empty direction pays a turnaround for nothing
Current direction drains mid-batchSwitches early; the second cover firesThe cheap switch — a turnaround with no latency cost to anyone
OPP_CAP reachedforced_switch; the batch is cut short§11's bound, at direction granularity
OPP_CAP = 0$fatal at elaborationIt would switch every batch regardless of demand — BATCH = 1 with extra steps
Both directions always pendingStrict alternation of full batchesThe steady state §11's table describes
burst_served with nothing pendingerr_served_with_nothing_pendingA burst was attributed to a direction that had no request
BATCH = 4 on a mixed workload80.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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
CheckWhat it establishes
Every selection matches the reference, over §3's traceTwo structures, one policy
§4: FCFS gives 352 cycles, 7 hits, displacement 0The published row, from the RTL
§4: uncapped gives 44 cycles, 21 hits, displacement 14The published row
§5: cap 2 gives 132 and displacement 2; cap 4 gives 88 and 4Both published rows
Saving retained: 71.4% and 85.7%, recomputed§5's asymmetry
Worst displacement never exceeds the cap, over 10,000 selectionsP6 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 capThe bound doing its job
A trace with no collisions: FCFS and FR-FCFS produce the same workThe 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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

SymptomLikely causeHow to confirm
Throughput unchanged by FR-FCFSNo locality in the arrival order to recover§13's ninth check; the gain is a trace property
Some requests take enormously longCap disabled or very large — §4worst_bypass; P6; cnt_forced will be zero
Worst displacement grows with run lengthUnbounded, not merely large — §13There is no bound to find; add a cap
Displacement bounded but throughput poorCap too small — §5Sweep it; 71.4% at cap 2 versus 85.7% at cap 4
cnt_forced very highThe cap is binding constantlyThe policy is effectively FCFS; raise the cap
Oldest request never served despite the capIt is not ready — §12The override needs a candidate; check 13.4
Selections rejected by the commit gatePolicy chose an illegal commanderr_select_not_ready; ready is an input and must be correct
Policy degrades toward FCFS unexpectedlyBypasses charged to newer entries — §8P7; or a wait inherited by a reused slot — P9
Hit rate high, throughput still poorNot a scheduling problem23.3 §19's table; check bank groups and turnaround
Latency improved, bandwidth fellThe cap was lowered§5's trade, in the other direction
err_cap_exceededA bypass counter clampedThe bound is no longer enforced; widen CAP_W
Two identical runs, different worst displacementExpected when uncappedAnother symptom that there is no bound
Utilisation stuck near 50% on a mixed workloadBATCH = 1 — no turnaround amortisation§11's first row; raise BATCH
Raising BATCH past 4 gains nothingtCCD_L is now binding, not turnaround§11's crossover; alternate bank groups instead
Writes take enormously longOPP_CAP too large, or absentopp_starved_batches; P12
cnt_switches far exceeds cnt_bursts / (2 × BATCH)Direction draining constantlyNot a bug; the workload is not batchable at this depth
The two batchers appear to fightExpected — §11's calloutDirection 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

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

  2. Sweep BYPASS_CAP from 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.

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

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

  5. §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?

  6. 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?

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

  8. 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 heuristic23.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

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.