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.
// 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);
endfunctionRetained 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.
// 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
endfunctionMonitoring 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.
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
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
endtaskKey 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.