Skip to content
VLSI Mentor

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.

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

A sequence diagram with six participants showing what happens to a load. The core issues a load to its cache. In the hit case the cache returns data immediately and the sequence ends there, with a dashed return arrow and no further messages, which is the common case and generates no DDR traffic at all. In the miss case the cache allocates miss-handling state so that the core can continue issuing other accesses, then issues a line-sized read to the interconnect. Where the path is coherent, the fabric may resolve the request by snooping peer caches, and a snoop that finds the line returns data without any DDR request being made. Otherwise the request reaches the memory controller, which maps the address, classifies the row state and issues commands to DRAM. The device returns the line, the controller returns it through the fabric, the cache fills the line and finally returns data to the core. The important structural points the diagram makes are that the hit path terminates before the interconnect, that a coherent snoop hit also terminates before DRAM, and that the miss-handling state is what allows several misses to be outstanding at once, which is the difference between a latency-bound and a bandwidth-bound workload.A load that misses, and one that does notCoreCacheFabricControllerDRAMPeer cacheloadHIT: data — pathendsMISS: allocate missstateline-sized readsnoop, if coherenthit: no DDR requestotherwise: memoryrequestmap, row state,commandsline datafilldata

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.

PropertyConsequence for DDR
Timing decided by replacementwrites arrive at times uncorrelated with the program's stores
Address decided by what is evictedthe write's address is unrelated to the access that caused the eviction
Caused by a read missa read-only workload can generate writes
Batched by replacement policywrites 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:

QuantityMeaningLimited by
Single-request latencyhow long one miss takes end to endthe path — 29.1 §14
Memory-level parallelismhow many misses are in flight at oncemiss-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:

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

DimensionValue
Parallelism1 — structural, not a tuning failure
Localitytypically poor; node addresses unrelated
Read/write mixread-dominated, plus scattered writebacks
DDR-visible request rateone at a time
Row-hit opportunityessentially none — no second candidate to match
Bank parallelismunusable
Bottlenecklatency, 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.

DimensionValue
Parallelismhigh — limited by miss-handling capacity, not dependencies
Localitystrong and sequential
Read/write mixread-dominated plus writebacks; or read-modify-write if stores miss
DDR-visible request ratemany concurrent, addresses marching
Row-hit opportunityhigh — consecutive lines usually share a row
Bank parallelismexploitable, depending on the map
Bottleneckbandwidth, 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 linesConsequence
In the same rowhigh row-hit rate — 23.3
Across banksbank 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.

DimensionValue
Parallelismhigh in aggregate, per-core unchanged
Localityeach stream's locality survives; the interleaved stream's does not
Read/write mixmixed, and the mix varies over time
DDR-visible request ratehigh
Row-hit opportunitydegraded by interleaving
Bank parallelismgood, if the streams land in different banks
Bottleneckinterference, 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.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 measureA: pointer chaseB: streamingC: multicore
Queue occupancynear zerodeepdeep
Concurrent requests1manymany
Row-hit rate~0highdegraded — §7
Bank distributionone at a timespread or concentrated, per mapspread
Read/write mixreads + scattered writebacksreads + writebacksmixed and varying
Data-bus utilisationlowhighmoderate to high
Reordering opportunitynonehighhigh
Dominant bottlenecklatencybandwidthinterference

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:

HarmMechanismDDR-visible signature
Wrong predictionsfetched lines are never usedextra requests, no reduction in demand misses
Cache pollutiona prefetched line evicts a useful onemore demand misses than without prefetch
Bandwidth consumptioncorrect but untimely prefetches competehigh 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:

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

PropertyConsequence
Caused by an access, not written by the programtraffic with no corresponding instruction
Several reads possible per missone program access can become several DDR requests
Serialised with the access it servesthe original request cannot proceed until translation completes
Address determined by the translation structurelocality 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-dependent29.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

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

Simultaneous 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

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

14. DV — Testing the Characterisation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
CheckWhat it establishes
Replay a pointer-chasing stream (one outstanding, dependent)outstanding_max = 1; reference average ≈ 1; both agree
Replay a streaming stream at depth 16outstanding_max = 16; averages agree within rounding
30,000 random arrive/depart interleavingsCounter and timeline integration agree on the average
Arrive and depart in the same cycle, repeatedlyCount unchanged each time — P2; averages still agree
Depart with nothing outstandingoverflow_seen sets — P5
Arrive at MAX_OUT then once moreoverflow_seen sets; count clamps — P1
Constant stride streamstride_regular rises, stride_irregular ≈ 0; reference agrees
Random-address streamstride_irregular dominates
Stream alternating two stridesClassified irregular by both — the documented limitation
Fewer than two requestsNo stride classification — P9
Tie used_valid lowused_ever_reported stays 0; ratio reported unavailable
Report used_bytes above req_bytesP11 fires
Run long enough to saturate conc_sumoverflow_seen sets; P7's escape holds
Run with the interface idleAll 13 properties pass; all 14 covers empty

Two runs are worth publishing, and the second is this chapter's central claim in numbers:

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

SymptomCandidate causesDiscriminating measurement
Low bandwidth, low queue occupancy, low utilisationScenario A — dependency-limitedaverage concurrency near 1 — §12
Low bandwidth, high occupancy, low utilisationbank conflicts, possibly §7's interleavingrow-hit rate plus sched_blocked29.2 §11
One core slow when others are busyinterference, or its own localityper-source service and bandwidth — 29.2 §10
Bandwidth high, program slowdelivered bytes far exceed used bytes§10's efficiency ratio
Adding cores reduces per-core throughput more than expectedlocality destruction by interleaving — §7row-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. §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.

  2. §3 argues writeback locality is unrelated to read locality. Construct the replacement policy under which that is false, and say what it would cost.

  3. §4's model assumes latency independent of parallelism. Derive the concurrency at which that assumption fails, given a service rate and an arrival rate.

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

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

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

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

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

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.