Each DRAM bank keeps one open row in its row buffer. A request to the open row is a hit, to a closed bank a miss, and to a different row a conflict that pays precharge plus activate.
Banks work in parallel, so consecutive requests to different banks overlap. Consecutive requests to one bank serialize on its row cycle.
Case | Bank state | Extra cost |
|---|---|---|
Row hit | Row already open | None: serve from the buffer |
Row miss | Bank closed | One activate |
Row conflict | Another row open | Precharge plus activate |
FR-FCFS serves ready commands first and oldest first among them. It harvests row hits by pulling forward requests to open rows.
Page policy picks the default: open page keeps rows open for locality, closed page closes them to bound conflict cost. Adaptive policies switch on measured hit rates.
Policy | Wins when | Risk |
|---|---|---|
Open page | Bursty reuse of few rows | Conflicts stall streaming mixes |
Closed page | Random access across rows | Throws away easy row hits |
FR-FCFS reorder | Queue holds parallel ready work | Hot rows starve cold streams |
Turnaround loss on the random trace below (stated parameters):
base cycles: 16 accesses x 10 = 160
miss: 1 x 20 = 20
conflicts: 15 x 40 = 600
turnarounds: 15 x 15 = 225
total: 160 + 20 + 600 + 225 = 1005 cycles
turnaround share: 225 / 1005 = 0.2239 ~ 22% of cycles.Write drains batch writes to cut turnarounds, but long drains block reads. Refresh steals whole-bank windows on a fixed schedule and cannot be reordered away.
The model below counts hits, conflicts and turnarounds for three 16-access traces; it models the accounting only, and timing parameters stay in the DRAM course.
module row_model #(
parameter int TRACE = 0,
parameter int N = 16,
parameter int BASE_CYC = 10,
parameter int MISS_ADD = 20,
parameter int CONFLICT_ADD = 40,
parameter int RW_TURN = 15
) (
output logic [31:0] hits,
output logic [31:0] misses,
output logic [31:0] conflicts,
output logic [31:0] turns,
output logic [31:0] total_cyc
);
function automatic int get_row(int i);
if (TRACE == 0) return 5;
else if (TRACE == 1) return (i % 2);
else return i;
endfunction
function automatic bit get_write(int i);
if (TRACE == 2) return ((i % 2) != 0);
else return 1'b0;
endfunction
always_comb begin
int open_row;
int r;
bit w;
bit prev_w;
hits = 0; misses = 0; conflicts = 0; turns = 0;
open_row = -1; prev_w = 1'b0;
for (int i = 0; i < N; i++) begin
r = get_row(i);
w = get_write(i);
if (open_row == -1) misses++;
else if (r == open_row) hits++;
else conflicts++;
open_row = r;
if ((i > 0) && (w != prev_w)) turns++;
prev_w = w;
end
total_cyc = N*BASE_CYC + misses*MISS_ADD + conflicts*CONFLICT_ADD + turns*RW_TURN;
end
endmodule
module tb_row_model;
logic [31:0] h0, m0, c0, t0, cy0;
logic [31:0] h1, m1, c1, t1, cy1;
logic [31:0] h2, m2, c2, t2, cy2;
int errors = 0;
row_model #(.TRACE(0)) s0 (.hits(h0), .misses(m0), .conflicts(c0), .turns(t0), .total_cyc(cy0));
row_model #(.TRACE(1)) s1 (.hits(h1), .misses(m1), .conflicts(c1), .turns(t1), .total_cyc(cy1));
row_model #(.TRACE(2)) s2 (.hits(h2), .misses(m2), .conflicts(c2), .turns(t2), .total_cyc(cy2));
initial begin
#1;
if (m0 !== 32'd1 || h0 !== 32'd15 || c0 !== 32'd0 || t0 !== 32'd0 || cy0 !== 32'd180) errors++;
if (m1 !== 32'd1 || h1 !== 32'd0 || c1 !== 32'd15 || t1 !== 32'd0 || cy1 !== 32'd780) errors++;
if (m2 !== 32'd1 || h2 !== 32'd0 || c2 !== 32'd15 || t2 !== 32'd15 || cy2 !== 32'd1005) errors++;
if (!(cy0 < cy1 && cy1 < cy2)) errors++;
if (errors == 0) $display("PASS: streaming 180c beats strided 780c beats random 1005c with matching counts");
else $display("FAIL: %0d row accounting checks wrong", errors);
$finish;
end
endmoduleStreaming takes 180 cycles, strided 780 and random 1005 for the same 1024 bytes. At 1 GHz that is about 5.69, 1.31 and 1.02 GB/s in the same order.
Hits collapse into conflicts: 1 miss with 15 hits becomes 1 miss with 15 conflicts, and cycles rise from 180 to 780.
It favors ready row hits, so a steady hot stream can delay cold requests without bound. Cap reordering distance or age priorities.