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

systemverilog
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  → queue

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

systemverilog
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);
endfunction

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

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

systemverilog
// 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 automatic

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

diagram
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 tests

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