Part 8 · Senior & Interview Prep · Intermediate
Q&A: Arrays & Queues
Dynamic vs associative vs queue selection, queue as FIFO, array-method one-liners, ref passing, and memory implications.
Q: Dynamic array vs associative array vs queue — how do you choose?
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.
Q: How do you model a FIFO with a queue?
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.
Q: Give me array-method one-liners — find all errors, sum a field, unique values, sort by field.
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.
Q: How do you pass a large array to a function — and why ref?
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.
Q: What are the memory implications of each array type?
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.
Key takeaways
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.
Common pitfalls
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.