In short: A SystemVerilog queue is a variable-size, ordered array with O(1) access at both ends, and in testbenches you use push_back and pop_front to model a FIFO of expected transactions that a scoreboard compares against what the monitor observes.
Why queues matter in verification
Hardware is full of FIFOs, pipelines and reorder buffers, so a verification environment needs a software structure that behaves the same way. A queue, declared as int q[$];, is exactly that: an unpacked array whose size changes at run time, indexed from 0, with the last element reachable through the $ symbol. Unlike a dynamic array you never call new[], and unlike an associative array the index is contiguous and ordered. If you are comparing the container families, our article on Verilog vs SystemVerilog explains why these types were added to the language in the first place, and the sister article on dynamic and associative arrays covers the alternatives.
The reason interviewers love queues is the combination of simplicity and traps. Everybody knows push_back; far fewer can explain why q.delete(i) is not the same as q.delete(), or what happens when you pop from an empty queue.
The core methods, one by one
A queue gives you a small, orthogonal method set. Learn these seven and you can model nearly any buffer.
| Method | Effect | Cost (typical) |
|---|---|---|
push_back(x) | Append x at the tail | O(1) |
push_front(x) | Insert x at index 0 | O(1) |
pop_front() | Remove and return index 0 | O(1) |
pop_back() | Remove and return last element | O(1) |
insert(i, x) | Place x at index i, shift the rest up | O(n) |
delete(i) | Remove element i only | O(n) |
delete() | Empty the whole queue | O(n) |
size() returns the element count. The O(1) claims for the ends are what the LRM intends and what commercial simulators such as Synopsys VCS, Cadence Xcelium and Siemens Questa implement, but the middle operations shift elements, so a scoreboard that repeatedly deletes from the middle of a 100,000-entry queue will visibly slow a regression.
Key fact: q.delete() with no argument clears every element, while q.delete(2) removes only the third one. Mixing them up is the single most common queue bug in code reviews.
A minimal, correct example
int q[$] = {10, 20, 30};
q.push_back(40); // 10 20 30 40
q.push_front(5); // 5 10 20 30 40
q.insert(2, 15); // 5 10 15 20 30 40
int a = q.pop_front(); // a = 5
int b = q.pop_back(); // b = 40
q.delete(1); // removes 15 -> 10 20 30
$display("size=%0d last=%0d", q.size(), q[$]);
Notice that slicing also works: q[1:$] returns everything except the head, a neat way to drop the first element without a pop. Because queues may be assigned and compared as whole objects, q2 = q; makes a full copy, which is useful for snapshotting expected data before a reset.
Empty queues and bounded queues
Calling pop_front() on an empty queue does not stop the simulation by itself. The LRM requires a warning and a default value of the element type: 0 for int, X for four-state types. A quiet 0 that you then compare against a legitimate 0 from the DUT can produce a false pass. Always guard with if (q.size() > 0) or, better, report a scoreboard error when a monitor transaction arrives and the expected queue is empty, since that means the DUT produced output no stimulus justified.
You can also bound a queue, int q[$:7]; holds at most 8 elements. Pushing into a full bounded queue is ignored with an error, which neatly models a FIFO of depth 8, though real testbenches usually track depth explicitly so they can check the RTL's full flag.
Key fact: Popping an empty queue returns the type's default value (0 for two-state, X for four-state), so a naive check can pass against a DUT that returned zeros.
Queues inside a scoreboard
The standard in-order scoreboard keeps one expected queue. The predictor (or reference model) pushes expected items on push_back when a stimulus enters the DUT; the output monitor delivers each observed item, and the scoreboard does pop_front and compares. Order is preserved because the DUT is a FIFO-like path. For the UVM flavour of this pattern, see our guide to the UVM scoreboard, and for field-by-field matching details read transaction comparison in a scoreboard.
class sb;
my_txn exp_q[$];
int errors;
function void write_exp(my_txn t);
exp_q.push_back(t);
endfunction
function void write_act(my_txn t);
my_txn e;
if (exp_q.size() == 0) begin
$error("Unexpected output %p", t); errors++; return;
end
e = exp_q.pop_front();
if (!e.compare(t)) begin
$error("Mismatch exp=%p act=%p", e, t); errors++;
end
endfunction
function void check_empty();
if (exp_q.size() != 0)
$error("%0d expected items never arrived", exp_q.size());
endfunction
endclass
The check_empty call at end of test is the part juniors forget. A DUT that silently drops packets leaves entries stranded in the queue, and without that final check the test passes. In UVM you would place it in check_phase.
Out-of-order DUTs: insert and delete(i) earn their keep
When a DUT can reorder, such as an AXI interconnect returning reads with different IDs, strict pop_front fails. You then keep the queue as a search pool: scan with find_first_index using a with clause, then remove the match with delete(idx). The O(n) scan is acceptable at typical depths of a few dozen outstanding transactions; beyond thousands, switch to an associative array keyed by ID. Keep a queue per ID and you retain per-ID ordering, which is what the AXI protocol actually guarantees. Mind the cast rules if you store different transaction types, covered in casting in SystemVerilog.
Performance, memory and debug habits
- Cap growth. A monitor that pushes forever with no consumer will eat gigabytes in a long constrained-random soak; assert an upper bound on
size(). - Store handles, not copies, for class objects:
push_back(t)stores a handle, so clone the transaction in the monitor if it will be reused, or later writes will corrupt queued entries. - Prefer queues to
new[]resizing when elements arrive one at a time; dynamic arrays reallocate and copy on every growth. - Use assertions for protocol-level checks and queues for data checks; see SystemVerilog Assertions for the complement.
- Dump queues with
%pin$display; waveform viewers such as Verdi, SimVision and Visualizer also show queue contents.
Key fact: Queue elements holding class objects are handles. Reusing one object in the monitor without cloning makes every queued entry point at the same, latest data.
Learn it hands-on
If you want guided practice writing scoreboards and chasing these bugs in a simulator, explore the verification tracks on our CourseTron courses page. Teams that need structured upskilling can also look at ChipXpert training, while project-side verification help is available from Avecas design services.
Ready to go deeper? Join the CourseTron verification course and build a full UVM environment with scoreboards, coverage and assertions.
Frequently asked questions
What is the difference between a queue and a dynamic array?
A dynamic array needs new[n] to size it and copies on every resize. A queue grows and shrinks one element at a time with push and pop methods and is optimised for access at both ends.
Does pop_front on an empty queue stop the simulation?
No. It issues a warning and returns the default value of the element type, so you must check size() yourself.
How is delete(i) different from delete()?
delete(i) removes only element i and shifts later ones down. delete() with no argument empties the entire queue.
Which structure suits an out-of-order scoreboard?
Use queues indexed per ID inside an associative array. That keeps O(1) lookup by ID and preserves per-ID ordering.