By access pattern. Dynamic array — size known at runtime, contiguous indices, fixed once allocated: a data payload. Associative array — sparse or non-integer keys, allocated per element on first write: a memory model keyed by address. Queue — ordered collection that grows and shrinks at the ends: anything FIFO-ish, which is most of a testbench (scoreboards, generators, monitors).
byte payload[]; // dynamic: payload = new[len];
logic [31:0] mem [logic [31:0]]; // associative: sparse 4GB space,
// only written addresses use memory
fifo_txn sb_q[$]; // queue: push_back / pop_front
// the selection table:
// known size, dense → dynamic
// sparse / keyed → associative (mem.exists(addr) before read!)
// ordered, grow/shrink → queueFollow-up: "What does reading a non-existent associative element return?" — The type's default (0 for 2-state, X for 4-state) and it does NOT allocate — but reading a non-existent element through a [] on the left side does. Guard reads with exists().
Junior vs senior: a junior defines each type. A senior answers with the access-pattern decision rule and volunteers the exists() guard unprompted.
Push at one end, pop at the other: push_back() to enqueue, pop_front() to dequeue gives in-order FIFO semantics. This two-method pattern is the reference model inside virtually every scoreboard: expected transactions go in on the input monitor, and get popped and compared on the output monitor.
fifo_txn exp_q[$];
// input monitor side
function void write_in(fifo_txn t);
exp_q.push_back(t);
endfunction
// output monitor side
function void write_out(fifo_txn t);
fifo_txn exp;
if (exp_q.size() == 0) begin
$error("DUT output with no expected txn (spurious pop?)");
return;
end
exp = exp_q.pop_front();
if (!t.compare(exp)) $error("Mismatch: exp %p got %p", exp, t);
endfunctionFollow-up: "How do you make it a LIFO? Bounded?" — LIFO: pop_back() instead of pop_front(). Bounded: declare fifo_txn q[$:7] for max 8 entries — pushes beyond the bound are silently discarded, so in a model you should check size() yourself and flag overflow instead of relying on the bound.
Junior vs senior: a junior shows push/pop. A senior shows it inside the scoreboard pattern with the empty-queue guard, and knows bounded queues drop silently.
Array locator and ordering methods with the with clause replace hand-written loops. They are interview favorites because each is one line — and because the return types trip people: locator methods return a queue (possibly empty), never a scalar.
fifo_txn q[$];
fifo_txn errs[$] = q.find with (item.is_err); // all matches
int idxs[$] = q.find_index with (item.len > 8); // their indices
int total = q.sum with (int'(item.len)); // accumulate field
fifo_txn big[$] = q.max with (item.len); // queue of size 0/1!
int lens[$] = q.unique with (item.len); // dedupe by field
q.sort with (item.addr); // in-place ascending
q.rsort with (item.len); // descending
q.shuffle(); // random order
// trap: max/min/unique return QUEUES — guard before [0]
if (big.size()) $display("longest = %0d", big[0].len);Follow-up: "What does sum return on an empty queue, and what type?" — 0, with the type of the with expression — which is why the int cast matters: summing a 4-bit field without it wraps at 16.
Junior vs senior: a junior writes the loop. A senior writes the one-liner, casts inside sum, and guards the queue-returning methods before indexing.
By default SystemVerilog passes arguments by value — the entire array is copied on every call. For a megabyte memory image that is a megabyte memcpy per call. ref passes a reference (no copy, callee can modify the original); const ref gives the no-copy benefit while making the array read-only — the right default for checkers and printers.
// copies 1MB per call — slow, and writes don't reach the caller
function automatic int count_bad(byte img[]);
...
endfunction
// no copy, read-only — preferred for analysis
function automatic int count_bad_fast(const ref byte img[]);
...
endfunction
// no copy, writable — for in-place modification
function automatic void scrub(ref byte img[]);
foreach (img[i]) if (img[i] == 8'hXX) img[i] = 0;
endfunction
// note: ref requires the subroutine to be automaticFollow-up: "Does this apply to passing objects?" — Class handles are already references; passing a handle by value copies only the handle (pointer), not the object. ref on a handle means the callee can reassign which object the caller's handle points to.
Junior vs senior: a junior knows ref avoids the copy. A senior reaches for const ref by default, knows ref requires automatic, and can contrast array copying with handle copying.
Fixed and dynamic arrays allocate contiguously for all elements, used or not. Associative arrays allocate per element on demand plus key-lookup overhead — the only sane way to model a 4GB address space in which a test touches a few kilobytes. Queues allocate to current size and grow as needed; an unbounded queue that is pushed but never popped is the classic testbench memory leak.
MODELING A 4GB MEMORY
logic [7:0] mem [0:4294967295]; // fixed: 4GB allocated → sim dies
logic [7:0] mem [logic [31:0]]; // assoc: KBs for touched bytes only
THE QUEUE LEAK
monitor ──push_back──► sb.exp_q ──pop_front──► compare
│
output path dead? pops never happen →
exp_q grows for the whole sim → memory climbs, sim crawls
defense: end-of-test check if (exp_q.size()) $error("leftover");
+ periodic watermark check on long-running testsFollow-up: "Your regression slows down over hours and memory climbs — first suspects?" — Unpopped scoreboard queues and unbounded mailbox growth: a checker path that stopped consuming. Watermark-print queue sizes periodically; the leaking one is obvious.
Junior vs senior: a junior compares allocation strategies abstractly. A senior maps each to its testbench use case and names the unpopped-queue leak with its end-of-test defense.
Select by access pattern: dense→dynamic, sparse/keyed→associative, ordered grow/shrink→queue.
push_back + pop_front is the scoreboard FIFO idiom; guard empty before popping.
Locator methods return queues — guard size() before [0]; cast inside sum.
const ref for large read-only arrays; ref requires automatic subroutines.
End-of-test queue-empty checks catch both checker leaks and lost transactions.
Reading associative entries without exists() — default values masquerade as data.
q.max(...)[0] on an empty result queue — index of nothing.
Passing big arrays by value in hot paths — invisible megabyte copies per call.
Unbounded queues with a dead consumer — the slow-leak sim that dies at hour six.