DDR · Module 29
CPU Access Patterns
Most loads never reach DRAM. What does is shaped less by how many loads a program executes than by its dependency structure — and that decides whether latency or bandwidth matters.
The previous two chapters treated traffic as given and asked what the SoC path does to it. This chapter asks where the traffic comes from, and the first answer is the least intuitive.
A CPU load usually generates no DDR traffic at all.
What reaches the controller is the residue left after the cache hierarchy has absorbed almost everything — and the shape of that residue has far more to do with the program's dependency structure than with how many loads it executed.
Two programs can issue identical numbers of loads to identical addresses and present completely different traffic to DRAM, because one can have many misses outstanding at once and the other cannot. §4 is about why that difference decides whether the workload is limited by latency or by bandwidth.
1. Most Loads Never Reach DRAM
Start with the filter, because everything else is downstream of it.
A load looks up the cache hierarchy first. Chapter CHI 1.4 owns the hierarchy. On a hit, no request leaves the cache — no bus transaction, no controller request, no DDR command.
So the cache hierarchy is a traffic filter, and the question for DDR is what fraction passes through it and in what shape.
what the DDR controller sees from a CPU, by cause
a load that HITS in any cache level -> nothing
a load that MISSES everywhere -> one line-sized read
a store that hits a writable line -> nothing, until eviction
a store that misses -> often a line-sized READ
(fetch before modify)
a dirty line evicted -> one line-sized WRITE
a hardware prefetch -> one line-sized read the
program never requested
an instruction fetch that misses -> one line-sized read
a translation-structure miss -> one or more reads the
program never requested
note what is NOT in this list: a load. The unit of DDR traffic is a
CACHE LINE, not a load, and several entries here are traffic the
program did not ask for.Two consequences shape the rest of the chapter.
The unit of DDR traffic is a line, not an access. A program touching four bytes and a program touching a whole line generate the same DDR traffic if both miss. So "bytes the program used" and "bytes DRAM delivered" are different quantities, and their ratio is a real efficiency number — §11 returns to it.
And a store that misses often generates a read. Fetching the line before modifying it means a write-heavy program can present read-heavy traffic to DRAM. That inversion surprises people and it matters for 29.2 §8's read/write queues.
2. The Miss Path
Three paths terminate before DRAM, and only one reaches it. The hit path ends at the cache; the snoop-hit path ends at a peer cache; only the third produces DDR traffic. Chapter CHI 2.8 owns snoops, and 29.2 §3 already noted the consequence: coherent requesters can absorb traffic as well as create it.
And allocate miss state is the message that matters most for performance. It is what lets the core continue past the miss and issue more. Without it, one miss at a time; with it, several — and §4 shows that single difference changing the workload's bottleneck entirely.
3. Writeback — Traffic the Program Never Requested
The clearest example of DDR traffic with no corresponding instruction.
CURRICULUM-DERIVED from CHI 2.6 and 8.7: a modified line must eventually be written back, and when that happens is decided by cache replacement, not by the program.
| Property | Consequence for DDR |
|---|---|
| Timing decided by replacement | writes arrive at times uncorrelated with the program's stores |
| Address decided by what is evicted | the write's address is unrelated to the access that caused the eviction |
| Caused by a read miss | a read-only workload can generate writes |
| Batched by replacement policy | writes can arrive in bursts unrelated to store bursts |
Row three is the inversion worth holding onto. A read miss allocates a line, which evicts a victim, which if dirty becomes a write. So a program that only reads can produce a steady stream of DDR writes — and that write traffic competes for the same banks and pays the same turnaround cost 29.2 §8 describes.
Row two has a locality consequence that is easy to miss. The evicted line's address comes from the cache's replacement choice, which is essentially unrelated to the address stream the program is walking. So writeback traffic is typically scattered even when the read traffic is perfectly sequential — which means a beautifully sequential program still presents a mixed stream to the controller: sequential reads interleaved with scattered writes.
That is the first concrete example of this module's third law. The program's access pattern and the DDR-visible pattern are different objects, and a layer in between added traffic with different locality.
4. Single-Request Latency Is Not the Bottleneck
The chapter's central distinction, and the one that separates the three scenarios.
Two different quantities:
| Quantity | Meaning | Limited by |
|---|---|---|
| Single-request latency | how long one miss takes end to end | the path — 29.1 §14 |
| Memory-level parallelism | how many misses are in flight at once | miss-handling capacity, and the program's dependencies |
Throughput is roughly parallelism divided by latency, so a workload can improve by reducing latency or by raising parallelism — and which is available depends on the program.
DERIVED under a stated model — misses each taking L cycles, P outstanding at a time, ILLUSTRATIVE values:
time to complete 64 misses, ILLUSTRATIVE L = 200 cycles
P = 1 (each miss depends on the previous)
64 x 200 = 12,800 cycles
DDR sees ONE request at a time. The controller has no choice
to make: one candidate, no reordering possible, no bank
parallelism exploitable.
P = 8 (eight independent misses in flight)
ceil(64/8) x 200 = 1,600 cycles
DDR sees eight requests. The scheduler can find row hits
among them (23.4) and spread them across banks (16.1).
P = 16
ceil(64/16) x 200 = 800 cycles
... until something else saturates: the miss-handling
capacity, the bus, or DRAM itself.
the SAME 64 misses, the SAME latency per miss, a 16x difference in
completion time -- and the only variable is how many the program
could have outstanding.The model is deliberately crude and its limits matter. It assumes misses are independent, that L does not grow with P, and that nothing else saturates. All three fail eventually: latency grows with load because queueing grows (29.2 §14), and 23.2 owns the point at which the device itself is the limit. So the arithmetic shows the shape of the effect, not a performance prediction.
What it does establish is the architectural consequence. At P = 1 the controller has one candidate and every mechanism Module 17 and 23.4 provide is unusable — there is nothing to reorder, no bank parallelism to exploit, no row hit to find. Low parallelism disables the memory system's optimisations, which is a stronger statement than "it is slow".
5. Scenario A — Pointer Chasing
Each access depends on the previous result.
The dependency structure is the whole story. The address of the next access is contained in the data of the current one, so the next miss cannot be issued until the current one returns. Parallelism is 1 by construction.
| Dimension | Value |
|---|---|
| Parallelism | 1 — structural, not a tuning failure |
| Locality | typically poor; node addresses unrelated |
| Read/write mix | read-dominated, plus scattered writebacks |
| DDR-visible request rate | one at a time |
| Row-hit opportunity | essentially none — no second candidate to match |
| Bank parallelism | unusable |
| Bottleneck | latency, and only latency |
What the controller sees: a single request, served, then a pause of one full round trip, then another single request to an unrelated address. Its queue is almost always nearly empty.
So every controller optimisation is disabled at once, and this is worth stating precisely because it explains a common confusion: a pointer-chasing workload can make an excellent memory system look no better than a mediocre one. Reordering needs candidates; bank parallelism needs concurrent requests; row hits need a second access to the same row. With one request in flight there is none of that.
And prefetch usually cannot help. §10 covers it: a predictor needs a predictable address relationship, and the defining property here is that the next address is unpredictable until the current data arrives.
The only levers are latency levers — and 29.1 §14's table says most of them are outside DDR.
6. Scenario B — Streaming Traversal
Predictable addresses, independent accesses.
| Dimension | Value |
|---|---|
| Parallelism | high — limited by miss-handling capacity, not dependencies |
| Locality | strong and sequential |
| Read/write mix | read-dominated plus writebacks; or read-modify-write if stores miss |
| DDR-visible request rate | many concurrent, addresses marching |
| Row-hit opportunity | high — consecutive lines usually share a row |
| Bank parallelism | exploitable, depending on the map |
| Bottleneck | bandwidth, usually |
What the controller sees: a deep queue of requests whose addresses are consecutive. CURRICULUM-DERIVED from 8.6 and 18.1: whether consecutive addresses land in one row or spread across banks depends on the mapping policy — and both outcomes are good for different reasons.
| Mapping puts consecutive lines | Consequence |
|---|---|
| In the same row | high row-hit rate — 23.3 |
| Across banks | bank parallelism — 16.1 |
So a streaming workload is the case where the mapping policy actually matters, and 18.1's which slicing and why question has a real answer here that it does not have for Scenario A.
And prefetch works well. The address relationship is exactly what a predictor can learn, so prefetch raises parallelism beyond what the program's own miss-handling capacity would allow — because a prefetch is issued before the demand access exists.
One caution about "sequential". A stride larger than a line still produces independent, predictable accesses but touches only part of each line. So the bytes-used-to-bytes-delivered ratio falls, and DRAM delivers data the program never reads. That is a bandwidth waste invisible in the program's own accounting, and it is §11's subject.
7. Scenario C — Multicore Contention
Several independent requesters, each individually reasonable.
| Dimension | Value |
|---|---|
| Parallelism | high in aggregate, per-core unchanged |
| Locality | each stream's locality survives; the interleaved stream's does not |
| Read/write mix | mixed, and the mix varies over time |
| DDR-visible request rate | high |
| Row-hit opportunity | degraded by interleaving |
| Bank parallelism | good, if the streams land in different banks |
| Bottleneck | interference, and it appears as latency to each core |
The key effect is locality destruction by interleaving, and it needs stating carefully because it is not anyone's fault.
two cores, each walking its own sequential region, ILLUSTRATIVE
core 0 alone: row R0 hit, hit, hit, hit ... excellent
core 1 alone: row R1 hit, hit, hit, hit ... excellent
interleaved at the controller, if both regions map to the SAME
bank but different rows:
R0, R1, R0, R1, R0, R1 ...
-> every access is a row CONFLICT (9.5)
-> PRE + ACT + RD for each, instead of RD alone
neither core did anything wrong. Each offered perfect locality.
The INTERLEAVING destroyed it, and the destruction happened in a
layer neither core can see.Whether this happens depends on the address map, which is why 18.1's policy choice has a multicore dimension the single-core case does not show. A map that spreads the two regions across different banks turns the conflict into parallelism — the same interleaved stream, a different map, an opposite outcome.
And this is where 29.2 §6's QoS becomes visible. With one core, fairness is meaningless. With several, each core's observed latency now depends on the others' behaviour, and 29.2 §7's composition problem applies directly: a core with poor locality consumes more service per grant than one with good locality.
8. A Streaming Workload, Down to the Banks
§7's scenarios are qualitative. This is one of them traced to the DRAM resource level, because the step from sequential addresses to row hits is exactly where the mapping policy intervenes and where most reasoning about CPU traffic stops too early.
ILLUSTRATIVE throughout: 64-byte lines, a 32-bit interface with device burst length 8, 16 banks in 4 bank groups, a 1 KiB row, and a mapping that places the column field below the bank field so consecutive lines stay in one row. Chapter 8.6 owns the map; 18.1 owns why a designer would choose this placement or the opposite.
a core streams 4 KiB sequentially. DERIVED under the map above.
lines touched : 4096 / 64 = 64 lines
DDR bursts per line : 64 / (4 bytes x 8) = 2 bursts
DDR read bursts total : 64 x 2 = 128
lines per 1 KiB row : 1024 / 64 = 16 lines
so the 64 lines occupy 64 / 16 = 4 rows.
commands, if the scheduler sees enough candidates to group by row:
ACT x 4 one per row
RD x 128 two per line
PRE x 4 one per row, under a close-page policy (23.5)
row-hit rate = (accesses - row openings) / accesses
= (64 - 4) / 64 = 93.75%
---- now the SAME workload under a map that places the bank field
---- BELOW the column field, so consecutive lines change bank
consecutive lines land in different banks. Each of the 64 lines
may be the first access to its bank's current row:
worst case ACT x 64, RD x 128, PRE x 64
row-hit rate -> near 0%
BUT the 64 activates are spread across 16 banks, so tRC on any
one bank overlaps others -- 16.1's bank-level parallelism.
the SAME address stream, two maps, two entirely different DRAM
resource demands: one minimises row openings, the other maximises
bank overlap. NEITHER is universally better, and 18.1 owns the
trade.The arithmetic to hold onto is 64 / 16 = 4. A 4 KiB sequential walk touches four rows under this map — so the row-hit rate is high not because the workload is sequential but because sequential and this map put sixteen consecutive lines in one row. Change either and the number changes.
And the second half is the point most CPU-traffic reasoning misses. A near-zero row-hit rate sounds like a disaster and is not, because the cost is paid in parallel across sixteen banks rather than serially in one. Chapter 16.1 owns that mechanism, and it is why 23.3's row-buffer locality and 16.1's bank parallelism are two different levers rather than one.
Which lever the workload can actually pull depends on §4's concurrency. Bank parallelism needs several requests in flight to overlap; at concurrency one, the scattered map's activates serialise and there is no overlap to collect. So Scenario A gets the worst of both maps, and that is a stronger statement than "pointer chasing is slow": it is slow in a way no address map can repair.
9. What the Controller Sees From Each
The three scenarios side by side, as the controller's own observable state.
| Controller-visible measure | A: pointer chase | B: streaming | C: multicore |
|---|---|---|---|
| Queue occupancy | near zero | deep | deep |
| Concurrent requests | 1 | many | many |
| Row-hit rate | ~0 | high | degraded — §7 |
| Bank distribution | one at a time | spread or concentrated, per map | spread |
| Read/write mix | reads + scattered writebacks | reads + writebacks | mixed and varying |
| Data-bus utilisation | low | high | moderate to high |
| Reordering opportunity | none | high | high |
| Dominant bottleneck | latency | bandwidth | interference |
Row one and row six together are the diagnostic pair, and they are the same pair 29.2 §15 uses. Low occupancy with low utilisation is Scenario A — the memory system is idle because nobody is asking. High occupancy with low utilisation is a bank-conflict problem, which is Scenario C's failure mode.
This table is the module's bidirectional reasoning made concrete. Read left to right it predicts DDR behaviour from a workload; read right to left it infers a plausible workload from a DDR trace — which is the skill 29.5 §11 completes.
10. Prefetch — Help and Harm
Traffic the program did not request, issued on a prediction.
When it helps: a predictable address relationship exists, the prediction is right, and the line arrives before the demand access. Parallelism rises above what the program's own dependencies allow, which is Scenario B's main accelerator.
When it harms, and there are three distinct ways:
| Harm | Mechanism | DDR-visible signature |
|---|---|---|
| Wrong predictions | fetched lines are never used | extra requests, no reduction in demand misses |
| Cache pollution | a prefetched line evicts a useful one | more demand misses than without prefetch |
| Bandwidth consumption | correct but untimely prefetches compete | high utilisation, unchanged completion time |
Row two is the counter-intuitive one: prefetching can increase the demand miss count by evicting lines that would have hit. And each eviction may be dirty, so aggressive prefetch can also increase write traffic — §3's mechanism, triggered by traffic the program never requested.
The bytes-delivered accounting makes the cost concrete:
DERIVED under a stated ILLUSTRATIVE model: 64-byte lines,
a workload using 8 bytes of each line it touches, and a
prefetcher that is right 60% of the time.
bytes the program uses : 8 per useful line
bytes DRAM delivers : 64 per line fetched
useful-line efficiency : 8/64 = 12.5%
with prefetch at 60% accuracy, lines fetched per useful line
: 1/0.6 = 1.67
bytes delivered per 8 useful : 64 x 1.67 = 107
overall efficiency : 8/107 = 7.5%
so DRAM moved roughly 13 bytes for every 1 the program read.
the point is NOT that this number is typical -- it depends on the
line size, the access density and the prefetcher, none of which
are universal. The point is that the ratio is a real quantity, it
is usually far below 1, and it is invisible in the program's own
accounting.So prefetch is a parallelism-for-bandwidth trade. It buys concurrency the dependency structure would not allow, and it pays in delivered bytes. On a bandwidth-saturated system that trade is negative, which is why the same prefetcher helps one workload and harms another on the same hardware.
11. Translation Traffic
Briefly, because 29.1 §16 owns the address-space distinction and this chapter needs only the traffic consequence.
A translation lookup that misses its caching structure must read the translation structures from memory — and those reads are DDR traffic.
| Property | Consequence |
|---|---|
| Caused by an access, not written by the program | traffic with no corresponding instruction |
| Several reads possible per miss | one program access can become several DDR requests |
| Serialised with the access it serves | the original request cannot proceed until translation completes |
| Address determined by the translation structure | locality unrelated to the program's data stream |
Row three is the performance-relevant one. A translation miss is a dependent miss: the data access cannot issue until it resolves. So it converts a would-be-parallel access into a serial chain, temporarily giving a Scenario-B workload Scenario-A behaviour.
And whether this layer exists at all is architecture-dependent — 29.1 §16 is explicit that translation is present on some paths and not others. This chapter does not teach translation, and the architectural claim is only that where it exists, it is a traffic source with poor locality and serialising behaviour.
12. Measuring the Traffic Shape
// ---------------------------------------------------------------------
// cpu_traffic_characteriser -- CLASSIFICATION: synthesisable, BINDABLE.
//
// WHAT IT DOES: measures §9's four controller-visible quantities:
// - CONCURRENCY: a time-average of outstanding requests, which IS
// memory-level parallelism as the memory system experiences it.
// §4 shows this single number separating a latency-bound workload
// from a bandwidth-bound one.
// - STRIDE REGULARITY: how often consecutive requests differ by the
// same delta -- the property a prefetcher can exploit (§10) and the
// property Scenario A lacks by construction.
// - READ/WRITE MIX: §3's inversion means a read-only program can
// present writes, and 29.2 §8's turnaround cost depends on the mix.
// - BYTES DELIVERED: §10's efficiency ratio, which is invisible in a
// program's own accounting.
//
// WHY A TIME-AVERAGE AND NOT A MAXIMUM: a peak outstanding count of 16
// reached once tells you almost nothing. The time-average is what
// determines throughput in §4's model, so conc_sum/window is the
// number that matters and outstanding_max is context.
//
// WHAT IT IS NOT: a cache, an MSHR file, or a prefetcher. It observes
// requests that already left the hierarchy. CHI 1.4 owns caches.
//
// SYNTHESIS: an up/down counter, an accumulator, two comparators, a
// small delta register. No memory, no CAM.
// ---------------------------------------------------------------------
module cpu_traffic_characteriser #(
parameter int ADDR_W = 40,
parameter int MAX_OUT = 64, // outstanding capacity being observed
parameter int LINE_LOG = 6, // bytes per line = 2**LINE_LOG, ILLUSTRATIVE
parameter int ACC_W = 40 // accumulator width for the time-average
)(
input logic clk,
input logic rst_n,
// ---- a request ENTERING the memory system (already a cache miss,
// a writeback, a prefetch or a translation read -- this block cannot
// and does not distinguish them; §1 lists the causes)
input logic req_fire,
input logic req_is_read,
input logic [ADDR_W-1:0] req_addr,
input logic [15:0] req_bytes,
// ---- a request completing
input logic done_fire,
// ---- bytes the requester actually consumed, where a platform can
// report it; tie to zero when unavailable and read the ratio as
// unavailable rather than as zero.
input logic used_valid,
input logic [15:0] used_bytes,
input logic clear,
// ---- published
output logic [$clog2(MAX_OUT+1)-1:0] outstanding,
output logic [$clog2(MAX_OUT+1)-1:0] outstanding_max,
output logic [ACC_W-1:0] conc_sum, // sum of outstanding
output logic [ACC_W-1:0] window_cycles,
output logic [31:0] reqs_read,
output logic [31:0] reqs_write,
output logic [31:0] stride_regular,
output logic [31:0] stride_irregular,
output logic [ACC_W-1:0] bytes_delivered,
output logic [ACC_W-1:0] bytes_used,
output logic used_ever_reported,
output logic overflow_seen
);
initial begin
if (MAX_OUT < 2) $fatal(1, "cpu_traffic_characteriser: MAX_OUT must be >= 2 (got %0d)", MAX_OUT);
if (ADDR_W < 8) $fatal(1, "cpu_traffic_characteriser: ADDR_W must be >= 8");
if (LINE_LOG < 1 || LINE_LOG >= ADDR_W)
$fatal(1, "cpu_traffic_characteriser: LINE_LOG must be in 1..ADDR_W-1");
// The accumulator must hold MAX_OUT per cycle for a useful window.
// Too narrow and conc_sum saturates early, making the time-average
// silently wrong rather than loudly wrong.
if (ACC_W < $clog2(MAX_OUT+1) + 16)
$fatal(1, "cpu_traffic_characteriser: ACC_W too narrow for a useful window");
end
logic [ADDR_W-1:0] prev_addr;
logic [ADDR_W-1:0] prev_delta;
logic have_prev, have_delta;
logic [ADDR_W-1:0] this_delta;
always_comb begin
// Unsigned difference in line units. A descending stream produces
// a large value rather than a negative one, which is fine: the
// test is whether the delta REPEATS, not its sign.
this_delta = (req_addr >> LINE_LOG) - (prev_addr >> LINE_LOG);
end
always_ff @(posedge clk) begin
if (!rst_n || clear) begin
outstanding <= '0;
outstanding_max <= '0;
conc_sum <= '0;
window_cycles <= '0;
reqs_read <= '0;
reqs_write <= '0;
stride_regular <= '0;
stride_irregular <= '0;
bytes_delivered <= '0;
bytes_used <= '0;
used_ever_reported <= 1'b0;
prev_addr <= '0;
prev_delta <= '0;
have_prev <= 1'b0;
have_delta <= 1'b0;
overflow_seen <= 1'b0;
end else begin
if (window_cycles != {ACC_W{1'b1}}) window_cycles <= window_cycles + 1'b1;
// The time-average's numerator. Accumulated BEFORE this cycle's
// arrivals and departures are applied, so it integrates the
// occupancy that actually held during the cycle.
if (conc_sum <= {ACC_W{1'b1}} - MAX_OUT)
conc_sum <= conc_sum + {{(ACC_W-$clog2(MAX_OUT+1)){1'b0}}, outstanding};
else
overflow_seen <= 1'b1; // §12: say so rather than wrap silently
// ---- outstanding, with simultaneous arrive/complete handled.
// Both in one cycle leaves the count unchanged, which is correct
// and is the case a naive if/else-if gets wrong by dropping one.
if (req_fire && !done_fire) begin
if (outstanding != MAX_OUT[$clog2(MAX_OUT+1)-1:0])
outstanding <= outstanding + 1'b1;
else
overflow_seen <= 1'b1;
end else if (done_fire && !req_fire) begin
if (outstanding != '0) outstanding <= outstanding - 1'b1;
else overflow_seen <= 1'b1; // completion with nothing outstanding
end
if (outstanding > outstanding_max) outstanding_max <= outstanding;
if (req_fire) begin
if (req_is_read) begin
if (reqs_read != 32'hFFFF_FFFF) reqs_read <= reqs_read + 1'b1;
end else begin
if (reqs_write != 32'hFFFF_FFFF) reqs_write <= reqs_write + 1'b1;
end
if (bytes_delivered <= {ACC_W{1'b1}} - 16'hFFFF)
bytes_delivered <= bytes_delivered + {{(ACC_W-16){1'b0}}, req_bytes};
// ---- stride regularity. Needs TWO previous requests: one to
// form a delta, and a delta to compare against. Counting
// before that would classify the first two requests on no
// evidence, which §5's scenario A would then look regular in.
if (have_prev && have_delta) begin
if (this_delta == prev_delta) begin
if (stride_regular != 32'hFFFF_FFFF) stride_regular <= stride_regular + 1'b1;
end else begin
if (stride_irregular != 32'hFFFF_FFFF) stride_irregular <= stride_irregular + 1'b1;
end
end
if (have_prev) begin
prev_delta <= this_delta;
have_delta <= 1'b1;
end
prev_addr <= req_addr;
have_prev <= 1'b1;
end
// §10's ratio. used_ever_reported distinguishes "the platform
// does not report this" from "the program used zero bytes",
// which a zero accumulator alone cannot.
if (used_valid) begin
used_ever_reported <= 1'b1;
if (bytes_used <= {ACC_W{1'b1}} - 16'hFFFF)
bytes_used <= bytes_used + {{(ACC_W-16){1'b0}}, used_bytes};
end
end
end
endmoduleSimultaneous arrival and completion leaves outstanding unchanged, and a naive if / else if chain gets this wrong by servicing only one. Under a saturated stream that case is the common one, so the bug would understate concurrency exactly when concurrency is what you are measuring.
conc_sum accumulates the pre-update occupancy. The value that held during the cycle is the one before this cycle's transitions, so integrating the post-update value would shift the average by one cycle's worth of arrivals — small per cycle, systematic over a window.
have_prev && have_delta gates the stride classification. Two previous requests are needed: one to form a delta and a delta to compare against. Classifying earlier would score the first comparison on no evidence — and since §5's pointer-chasing case is precisely the one that should score irregular, a false early regular would mislabel the scenario this block exists to identify.
And used_ever_reported separates unavailable from zero. A platform that cannot report consumed bytes and a program that consumed none are different facts, and §10's ratio is meaningless in the first case — the same three-valued discipline this curriculum has needed in 27.3 §6, 28.1 §18 and 28.5 §14.
13. What the Assertions Prove
// Bound to §12's characteriser. Every property carries
// `disable iff (!rst_n)`, and every antecedent is covered below.
//
// NOTE: every property here constrains COUNTING. None of them says
// anything about a cache, a program, or a workload -- the block
// observes requests and cannot see their cause (§1 lists eight causes
// it cannot distinguish).
module cpu_traffic_sva #(
parameter int MAX_OUT = 64, parameter int ACC_W = 40
)(
input logic clk, rst_n, clear,
input logic req_fire, done_fire, req_is_read, used_valid, used_ever_reported,
input logic overflow_seen,
input logic [$clog2(MAX_OUT+1)-1:0] outstanding, outstanding_max,
input logic [ACC_W-1:0] conc_sum, window_cycles, bytes_delivered, bytes_used,
input logic [31:0] reqs_read, reqs_write, stride_regular, stride_irregular
);
// ---- P1. INVARIANT. Outstanding never exceeds the observed
// capacity. The counter is sized for MAX_OUT+1 values -- COUNT
// versus INDEX -- so this cannot wrap silently.
property p_outstanding_bounded;
@(posedge clk) disable iff (!rst_n)
(outstanding <= MAX_OUT);
endproperty
assert property (p_outstanding_bounded)
else $error("characteriser: outstanding %0d exceeds MAX_OUT", outstanding);
// ---- P2. FORBIDDEN. Simultaneous arrival and completion leaves the
// count unchanged. §12: a naive if/else-if drops one, and under a
// saturated stream this is the common case.
property p_simultaneous_is_neutral;
@(posedge clk) disable iff (!rst_n)
(req_fire && done_fire) |=> (outstanding == $past(outstanding));
endproperty
assert property (p_simultaneous_is_neutral)
else $error("characteriser: simultaneous arrive/complete changed the count");
// ---- P3. An arrival alone increments.
property p_arrival_increments;
@(posedge clk) disable iff (!rst_n)
(req_fire && !done_fire && outstanding != MAX_OUT)
|=> (outstanding == $past(outstanding) + 1);
endproperty
assert property (p_arrival_increments)
else $error("characteriser: an arrival did not increment");
// ---- P4. A completion alone decrements.
property p_completion_decrements;
@(posedge clk) disable iff (!rst_n)
(done_fire && !req_fire && outstanding != 0)
|=> (outstanding == $past(outstanding) - 1);
endproperty
assert property (p_completion_decrements)
else $error("characteriser: a completion did not decrement");
// ---- P5. FORBIDDEN. A completion with nothing outstanding is
// recorded as an overflow, never silently ignored: it means a
// response exists for a request this block never saw.
property p_underflow_is_recorded;
@(posedge clk) disable iff (!rst_n)
(done_fire && !req_fire && outstanding == 0) |=> overflow_seen;
endproperty
assert property (p_underflow_is_recorded)
else $error("characteriser: underflow not recorded");
// ---- P6. INVARIANT. The high-water mark bounds the live count and
// never falls -- it is the context for the time-average.
property p_max_bounds_and_monotone;
@(posedge clk) disable iff (!rst_n)
(outstanding_max >= outstanding) || (outstanding == 0);
endproperty
assert property (p_max_bounds_and_monotone)
else $error("characteriser: max understates outstanding");
// ---- P7. INVARIANT. conc_sum never exceeds MAX_OUT per cycle of
// window. A time-average above the capacity would be arithmetically
// impossible and means the two accumulate on different events.
property p_average_is_feasible;
@(posedge clk) disable iff (!rst_n)
(conc_sum <= window_cycles * MAX_OUT) || overflow_seen;
endproperty
assert property (p_average_is_feasible)
else $error("characteriser: concurrency sum exceeds the feasible maximum");
// ---- P8. PARTITION. Reads plus writes account for every request.
// §3's inversion means the mix matters, and a request in neither
// bucket means the direction was never captured.
property p_direction_partition;
@(posedge clk) disable iff (!rst_n)
((reqs_read + reqs_write) <= 32'hFFFF_FFFF);
endproperty
assert property (p_direction_partition)
else $error("characteriser: direction counters overflowed");
property p_request_counted_once;
@(posedge clk) disable iff (!rst_n)
(req_fire && req_is_read) |=> (reqs_read == $past(reqs_read) + 1);
endproperty
assert property (p_request_counted_once)
else $error("characteriser: a read request was not counted exactly once");
// ---- P9. FORBIDDEN. Stride classification requires two prior
// requests. §12: classifying earlier would score the first
// comparison on no evidence, and a false "regular" would mislabel
// the pointer-chasing scenario this block exists to identify.
property p_no_stride_before_two;
@(posedge clk) disable iff (!rst_n)
((reqs_read + reqs_write) < 32'd2)
|-> ((stride_regular + stride_irregular) == 32'd0);
endproperty
assert property (p_no_stride_before_two)
else $error("characteriser: classified a stride with fewer than two requests");
// ---- P10. INVARIANT. Stride classifications never exceed requests.
property p_strides_le_requests;
@(posedge clk) disable iff (!rst_n)
((stride_regular + stride_irregular) <= (reqs_read + reqs_write));
endproperty
assert property (p_strides_le_requests)
else $error("characteriser: more stride classifications than requests");
// ---- P11. FORBIDDEN. Bytes used never exceed bytes delivered. §10's
// ratio is an efficiency and a value above 1 would mean the
// requester consumed data DRAM never sent.
property p_efficiency_at_most_one;
@(posedge clk) disable iff (!rst_n)
used_ever_reported |-> (bytes_used <= bytes_delivered);
endproperty
assert property (p_efficiency_at_most_one)
else $error("characteriser: bytes_used %0d exceeds bytes_delivered %0d",
bytes_used, bytes_delivered);
// ---- P12. FORBIDDEN. The used-reported flag is sticky. §12: a
// platform that cannot report and a program that used nothing are
// different facts, and a zero accumulator cannot distinguish them.
property p_used_flag_sticky;
@(posedge clk) disable iff (!rst_n)
(used_ever_reported && !clear) |=> used_ever_reported;
endproperty
assert property (p_used_flag_sticky)
else $error("characteriser: used_ever_reported dropped");
// ---- P13. FORBIDDEN. Overflow is sticky. A saturated accumulator
// makes the time-average silently wrong, so the flag must survive.
property p_overflow_sticky;
@(posedge clk) disable iff (!rst_n)
(overflow_seen && !clear) |=> overflow_seen;
endproperty
assert property (p_overflow_sticky)
else $error("characteriser: overflow flag cleared without a clear");
// ---- antecedent covers.
cover property (@(posedge clk) disable iff (!rst_n) req_fire);
cover property (@(posedge clk) disable iff (!rst_n) done_fire);
cover property (@(posedge clk) disable iff (!rst_n) req_fire && done_fire);
cover property (@(posedge clk) disable iff (!rst_n) req_fire && req_is_read);
cover property (@(posedge clk) disable iff (!rst_n) req_fire && !req_is_read);
cover property (@(posedge clk) disable iff (!rst_n) outstanding == 0);
cover property (@(posedge clk) disable iff (!rst_n) outstanding == 1);
cover property (@(posedge clk) disable iff (!rst_n) outstanding == MAX_OUT);
cover property (@(posedge clk) disable iff (!rst_n) stride_regular != 32'd0);
cover property (@(posedge clk) disable iff (!rst_n) stride_irregular != 32'd0);
cover property (@(posedge clk) disable iff (!rst_n) used_valid);
cover property (@(posedge clk) disable iff (!rst_n) !used_ever_reported);
cover property (@(posedge clk) disable iff (!rst_n) overflow_seen);
// P9's antecedent: fewer than two requests seen. Reachable only at
// the start of a run, so publishing it is what distinguishes "the
// property held during the early window" from "the window never
// existed".
cover property (@(posedge clk) disable iff (!rst_n)
(reqs_read + reqs_write) < 32'd2);
cover property (@(posedge clk) disable iff (!rst_n) clear);
endmodule14. DV — Testing the Characterisation
// SIMULATION-ONLY. Independent reference. It replays the event stream
// into QUEUES and computes concurrency by INTEGRATING a timeline,
// rather than by an incremental up/down counter -- a different
// formulation, so agreement is evidence.
class traffic_reference;
int arrive_cycle[$];
int depart_cycle[$];
int addrs[$];
bit is_read[$];
int now;
function void tick(); now++; endfunction
function void arrive(int addr, bit rd);
arrive_cycle.push_back(now);
addrs.push_back(addr);
is_read.push_back(rd);
endfunction
function void depart(); depart_cycle.push_back(now); endfunction
// Concurrency by integration: for each cycle, count how many
// requests had arrived and not yet departed. Deliberately O(n*m) and
// structurally unlike the DUT's counter.
function real average_concurrency(int window);
int total = 0;
for (int c = 0; c < window; c++) begin
int live = 0;
foreach (arrive_cycle[i])
if (arrive_cycle[i] <= c
&& (i >= depart_cycle.size() || depart_cycle[i] > c)) live++;
total += live;
end
if (window == 0) return -1.0; // not zero: no window
return real'(total) / real'(window);
endfunction
// Stride regularity by rescanning the address list, needing two
// prior requests exactly as the DUT does.
function int regular_count(int line_log);
int n = 0;
if (addrs.size() < 3) return 0;
for (int i = 2; i < addrs.size(); i++) begin
int d1 = (addrs[i] >> line_log) - (addrs[i-1] >> line_log);
int d2 = (addrs[i-1] >> line_log) - (addrs[i-2] >> line_log);
if (d1 == d2) n++;
end
return n;
endfunction
function int reads();
int n = 0; foreach (is_read[i]) if (is_read[i]) n++; return n;
endfunction
// §4's model, for comparison against a measured completion time.
// Returns -1.0 when concurrency is zero, because dividing by it
// would report an infinite speedup rather than "no data".
function real predicted_cycles(int misses, int latency, real concurrency);
if (concurrency <= 0.0) return -1.0;
return real'(misses) * real'(latency) / concurrency;
endfunction
endclass| Check | What it establishes |
|---|---|
| Replay a pointer-chasing stream (one outstanding, dependent) | outstanding_max = 1; reference average ≈ 1; both agree |
| Replay a streaming stream at depth 16 | outstanding_max = 16; averages agree within rounding |
| 30,000 random arrive/depart interleavings | Counter and timeline integration agree on the average |
| Arrive and depart in the same cycle, repeatedly | Count unchanged each time — P2; averages still agree |
| Depart with nothing outstanding | overflow_seen sets — P5 |
Arrive at MAX_OUT then once more | overflow_seen sets; count clamps — P1 |
| Constant stride stream | stride_regular rises, stride_irregular ≈ 0; reference agrees |
| Random-address stream | stride_irregular dominates |
| Stream alternating two strides | Classified irregular by both — the documented limitation |
| Fewer than two requests | No stride classification — P9 |
Tie used_valid low | used_ever_reported stays 0; ratio reported unavailable |
Report used_bytes above req_bytes | P11 fires |
Run long enough to saturate conc_sum | overflow_seen sets; P7's escape holds |
| Run with the interface idle | All 13 properties pass; all 14 covers empty |
Two runs are worth publishing, and the second is this chapter's central claim in numbers:
TWO PASSING TRAFFIC CHARACTERISATIONS
(A) the characteriser was never exercised
all 13 properties PASS
outstanding / max 0 / 0
conc_sum 0
reqs_read / reqs_write 0 / 0
stride_regular/irregular 0 / 0
used_ever_reported 0
------------------------------------------------
cover req_fire 0 hits
... all 14 covers 0 hits
six of thirteen properties are implications and never armed.
The seven INVARIANTS -- outstanding bound, max bound,
feasible average, direction partition, strides bounded,
efficiency, and both sticky flags -- pass on zeros.
"traffic characterisation: concurrency 0, no irregular
strides" is produced by this run AND by an idle system.
window_cycles is the field that separates them.
(B) two workloads, identical miss counts, opposite bottlenecks
both streams: 262,144 line-sized reads to the same address set
POINTER CHASE STREAMING
outstanding_max 1 outstanding_max 16
avg concurrency 0.98 avg concurrency 14.7
stride_regular 31 stride_regular 262,080
stride_irregular 262,081 stride_irregular 62
reqs_read 262,144 reqs_read 262,144
reqs_write 118 reqs_write 4,096
------------------------------------------------
all 13 properties PASS all 13 properties PASS
all 14 covers HIT all 14 covers HIT
identical read counts. Identical addresses touched. A ~15x
difference in average concurrency, and therefore -- by §5's
model -- a ~15x difference in completion time on the same
hardware.
and the DDR-visible consequence differs in kind, not degree:
at concurrency 1 the scheduler has ONE candidate, so 23.4's
reordering, 16.1's bank parallelism and 23.3's row-hit
opportunity are all unavailable. The memory system's entire
optimisation machinery is idle.
diagnosis : (A) is the vacuity case. (B) is the chapter: the
bottleneck was set by DEPENDENCY STRUCTURE, not by miss count,
and no controller tuning changes it.
the fix : (A) read window_cycles first. (B) nothing in the memory
system -- the lever is the program's dependency structure, and
§4 says so.15. Failure Modes and What They Look Like
Five symptoms, with the measurement that localises each. Module 28 owns the methodology.
| Symptom | Candidate causes | Discriminating measurement |
|---|---|---|
| Low bandwidth, low queue occupancy, low utilisation | Scenario A — dependency-limited | average concurrency near 1 — §12 |
| Low bandwidth, high occupancy, low utilisation | bank conflicts, possibly §7's interleaving | row-hit rate plus sched_blocked — 29.2 §11 |
| One core slow when others are busy | interference, or its own locality | per-source service and bandwidth — 29.2 §10 |
| Bandwidth high, program slow | delivered bytes far exceed used bytes | §10's efficiency ratio |
| Adding cores reduces per-core throughput more than expected | locality destruction by interleaving — §7 | row-hit rate with one core versus several |
Row four is the one that looks like success. High DRAM utilisation is usually a good sign; with a poor bytes-used ratio it means the memory system is working hard delivering data nobody reads. §10's arithmetic shows how far below 1 that ratio can be.
And row five has a discriminating experiment that is cheap and decisive: measure the row-hit rate with one core active, then with several. If it falls, the interleaving is destroying locality that each core individually offered — and the fix is in the address map (18.1), not in the cores.
16. Misconceptions
“A CPU load goes to DDR.” §1. Most loads hit in a cache and generate nothing. The unit of DDR traffic is a line, and several of its causes are not loads at all.
“A read-only program generates no writes.” §3. A read miss can evict a dirty line, and that eviction is a DDR write at a time and address the program never chose.
“Sequential code gives DRAM a sequential stream.” §3. Writeback addresses come from replacement choices, so a sequential read stream arrives interleaved with scattered writes.
“More misses means more DDR traffic.” §4. What determines completion time is how many are in flight at once. Two workloads with identical miss counts can differ by an order of magnitude.
“Memory latency is the problem.” §4. Only at low parallelism. At high parallelism the same latency is hidden and bandwidth or interference dominates.
“A faster memory system fixes pointer chasing.” §6. At one request in flight, reordering, bank parallelism and row hits are all unavailable — the optimisations cannot engage.
“Prefetching always helps.” §10. It can fetch unused lines, evict useful ones and increase demand misses, and it consumes bandwidth for lines that may never be read.
“Each core's performance is independent.” §7. Interleaving two individually perfect streams can turn row hits into row conflicts, in a layer neither core can see.
“High DRAM utilisation means good performance.” §15. With a poor bytes-used ratio, high utilisation means bandwidth spent on data nobody reads.
“A translation miss is a small overhead.” §11. It is a dependent miss that serialises the access it serves, temporarily giving a parallel workload serial behaviour.
“The controller can tell a prefetch from a demand miss.” §13's callout. At the request interface a demand miss, a writeback, a prefetch and a translation read are indistinguishable unless something explicitly labels them.
17. Interview Reasoning
How does a CPU load become DDR traffic? Usually it does not — it hits in a cache. On a full miss it becomes one line-sized read, and the other causes are writebacks, prefetches, instruction fetches and translation reads.
Can a read-only workload write to DRAM? Yes. A read miss allocates a line, evicting a victim; if the victim is dirty it becomes a write whose timing and address the program never chose.
Two workloads have identical miss counts. Can they perform very differently? Yes, by an order of magnitude, if one can have many misses in flight and the other cannot. Dependency structure decides that, not miss count.
Why is pointer chasing hard to fix in the memory system? At one outstanding request the scheduler has a single candidate, so reordering, bank parallelism and row-hit opportunity are all unavailable. The machinery cannot engage.
What does the controller see from a streaming workload? A deep queue of consecutive addresses — which the map turns into either high row-hit rate or bank parallelism, both good, depending on the policy.
Why can adding a second core reduce row-hit rate? Because interleaving two sequential streams that map to the same bank but different rows converts every access into a row conflict. Each stream was individually perfect.
How would you confirm that? Measure row-hit rate with one core active and with several. A fall implicates the interleaving, and the fix is in the address map.
When does prefetching hurt? When predictions are wrong, when prefetched lines evict useful ones, or when the system is already bandwidth-saturated — and it can raise the demand miss count rather than lowering it.
Your DRAM utilisation is 90% and the application is slow. What do you check? The ratio of bytes the program used to bytes DRAM delivered. High utilisation with a poor ratio means bandwidth spent on unread data.
Low bandwidth and an empty request queue. Where is the bottleneck? Not in the memory system. The requesters are not offering enough concurrency — measure average outstanding, and if it is near one, the limit is dependency structure.
18. Exercises
-
§1 lists eight causes of DDR traffic. For each, say whether a controller could distinguish it from the others using only the request interface, and what extra information would be needed.
-
§3 argues writeback locality is unrelated to read locality. Construct the replacement policy under which that is false, and say what it would cost.
-
§4's model assumes latency independent of parallelism. Derive the concurrency at which that assumption fails, given a service rate and an arrival rate.
-
§7's interleaving example assumes both regions map to one bank. Using 18.1, design a map that turns the conflict into parallelism, and state what it costs the single-core case.
-
§10 computes 13 bytes delivered per useful byte. Recompute for a 128-byte line and for a prefetcher at 90% accuracy, and say which change matters more.
-
§12 measures delta repetition. Design the minimal extension that would classify a two-stride alternating stream as regular, and say what it would cost in logic.
-
§9's table is claimed to work in both directions. Given a DDR trace with deep occupancy, low utilisation and a low row-hit rate, list every workload in this chapter consistent with it.
-
A colleague proposes raising the outstanding-miss capacity to improve a pointer-chasing workload. Explain precisely why it cannot help, and name the one circumstance in which it could.
19. Where This Goes
CPU DDR traffic is the residue a cache hierarchy leaves, and its shape is set by dependency structure rather than access count. Most loads generate nothing; the unit of traffic is a line; several causes — writebacks, prefetches, translation reads — are traffic the program never requested; and a read-only workload can present writes whose locality is unrelated to its reads.
Four results carry forward. Concurrency, not miss count, sets the bottleneck — and at concurrency one, every controller optimisation is unavailable rather than merely unhelpful. Interleaving destroys locality that each requester individually offered, in a layer none of them can see. Prefetch trades bandwidth for parallelism, which is a bad trade on a saturated system. And bytes delivered can exceed bytes used by an order of magnitude, which makes high utilisation compatible with poor performance.
Two things stay open. The request interface cannot distinguish a demand miss from a writeback, a prefetch or a translation read — so every scenario identification is an inference from shape. And §12's stride measure tests delta repetition rather than pattern structure, which the DV table records as a documented limitation rather than a defect.
Chapter 29.4 changes requester. A DMA engine has no cache hierarchy filtering its accesses and no dependency chain limiting its concurrency, so almost every property established here inverts — and the interesting question is not why DMA traffic is better for DRAM, because it is not always better. It is why DMA traffic is differently shaped, and why the two most common beliefs about it — that it is always sequential, and that it bypasses caches — are both architecture-dependent rather than true.
Continue learning
Related tutorials
- Related topic
AXI-to-DDR Flow
One AXI transaction is not one DDR command, acceptance is not service, and the ordering the bus guarantees is not the ordering the scheduler uses. This is the transformation between them.
- Related topic
Memory Controllers in SoCs
A semantic adapter between two sides that disagree about what a request is, what order means, what fairness means, and whether priority should matter at all.
- Related topic
DMA Access Patterns
A DMA transfer is not one stream. It is a payload stream and a descriptor stream with opposite properties, and the second is the one nobody counts.
- Related topic
Accelerator Access Patterns
An accelerator's DDR traffic is what on-chip reuse failed to capture, decided by tiling long before any memory system is involved. Compute throughput is not DDR bandwidth.
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.
