Part 7 · Advanced & Integration · Intermediate

TB Memory Discipline

Unbounded queue growth, retained handles, associative arrays for sparse data, memory monitoring, and a leak-hunt walkthrough.

The classic leak: unbounded queues and mailboxes

SystemVerilog is garbage-collected, so you cannot leak memory the C way — but you can do something just as bad: keep references to objects you will never use again . The textbook case is a scoreboard that pushes expected transactions into a queue but pops only on a match. Every mismatch, dropped response, or filtered transaction leaves an entry behind forever, and a 12-hour soak test dies with the simulator at 60 GB.

systemverilog
// BEFORE — grows forever if any expected txn never matches
class scoreboard;
  bus_txn expected_q[$];

  function void write_expected(bus_txn t);
    expected_q.push_back(t);
  endfunction

  function void write_actual(bus_txn t);
    foreach (expected_q[i])
      if (expected_q[i].compare(t)) begin
        expected_q.delete(i);
        return;
      end
    // mismatch: error reported, but expected_q never shrinks ← LEAK
  endfunction
endclass

// AFTER — bound the queue and make growth an ERROR, not a slow death
function void write_expected(bus_txn t);
  expected_q.push_back(t);
  if (expected_q.size() > MAX_OUTSTANDING)
    $error("scoreboard expected_q exceeded %0d entries — leak or stalled DUT",
           MAX_OUTSTANDING);
endfunction

Retained handles prevent collection

  • An object is freed only when NO handle references it — one forgotten queue entry pins the txn and everything it references.

  • Static/global arrays of handles (debug logs of 'all txns ever seen') are deliberate leaks — cap or disable them in long runs.

  • Mailboxes with no consumer (a disabled checker still being fed) grow silently — guard the put side too.

  • Setting a handle to null does nothing if another copy of the handle survives elsewhere.


Associative arrays for sparse data

A 4 GB address-space memory model declared as a fixed array would allocate the whole range up front. An associative array allocates only the entries actually written — the standard tool for sparse address spaces, ID-indexed trackers, and per-tag reorder buffers.

systemverilog
// WRONG — tries to allocate 2^32 bytes of storage
byte mem_fixed [0:32'hFFFF_FFFF];          // simulator may refuse or thrash

// RIGHT — sparse: allocates only touched addresses
byte mem_sparse [bit [31:0]];

function void backdoor_write(bit [31:0] addr, byte data);
  mem_sparse[addr] = data;                 // entry created on first write
endfunction

function byte backdoor_read(bit [31:0] addr);
  if (!mem_sparse.exists(addr)) return 8'hxx;   // never written
  return mem_sparse[addr];
endfunction

// Cleanup matters for assoc arrays too — per-tag trackers must delete:
bus_txn in_flight [bit [7:0]];             // keyed by transaction ID
function void retire(bit [7:0] id);
  if (in_flight.exists(id)) in_flight.delete(id);   // free the entry AND the handle
endfunction

Monitoring memory and hunting a leak

Long-running sims should self-report container sizes periodically. When memory grows, the hunt is mechanical: find which container grows, then find why its entries are never removed.

diagram
LEAK-HUNT WALKTHROUGH

  Symptom: 12-hour soak test; simulator RSS grows 1 GB/hour.

  Step 1  Add a periodic watermark report (every 1M cycles):
            scb.expected_q     : 14    14    15    13   ← stable, innocent
            scb.posted_wr_q    : 210   8k    61k   480k ← GROWING — suspect
            mem_model entries  : 4k    4k    4k    4k   ← stable

  Step 2  Why does posted_wr_q only grow?
            push site: write_expected() on every posted write    expected
            pop site:  on write RESPONSE ... but the DUT does
                       not send responses for POSTED writes!    ← root cause

  Step 3  Fix: posted writes are checked against memory model
          directly and never enqueued. Re-run: RSS flat.

  Moral: the leak was a protocol misunderstanding, found by
  watching WHICH container grows — not by staring at code.

Watermark reporting pattern

systemverilog
task automatic memory_watermark();
  forever begin
    repeat (1_000_000) @(posedge vif.clk);
    $display("[MEMWATCH] t=%0t exp_q=%0d posted_q=%0d in_flight=%0d",
             $time, expected_q.size(), posted_wr_q.size(), in_flight.num());
  end
endtask

Key takeaways

  • GC frees only unreferenced objects — a forgotten queue entry pins memory forever.

  • Bound every scoreboard queue and make exceeding the bound a loud error.

  • Use associative arrays for sparse address spaces and ID trackers; delete() entries on retire.

  • Periodic container-size watermarks turn a mystery leak into a five-minute diagnosis.

Common pitfalls

  • Scoreboard queues that only shrink on a perfect match — every mismatch leaks an entry.

  • A 'log of all transactions' debug array left enabled in a soak test.

  • Fixed arrays sized to the full address space instead of associative arrays.

  • Assuming handle = null frees the object while another container still holds it.