SystemVerilog Arrays and Queues: Fixed, Dynamic, Associative and Queue

In short: SystemVerilog has four kinds of unpacked array. A fixed array has a size set when you write the code. A dynamic array gets its size at run time with new[]. An associative array only stores the keys you actually use, which suits sparse data like memory. A queue grows and shrinks at either end with push_back() and pop_front(). Pick by one question: do you know the size up front, and how will you add and remove items?

Every testbench I’ve written stores lists of things.

Expected transactions. Register values. A model of memory. A list of errors to print at the end.

Pick the right list type, and the rest of the code stays simple.

Pick the wrong one, and you write index math all day.

Four everyday pictures

  • Fixed array = an egg carton. It always has 12 slots, even when some are empty. You can’t add a 13th.
  • Dynamic array = a bookshelf you order by size. Want a bigger one? You get a new shelf, and you have to move the books over yourself, or they’re gone. That’s new[], and it’s behind mistake 3 below.
  • Associative array = a hotel. Room 1203 exists only because a guest checked in. The hotel doesn’t build 1,202 empty rooms first.
  • Queue = the line at a ticket counter. People join at the back and get served from the front. Sometimes someone cuts in, and that’s insert().
Four SystemVerilog array types drawn as everyday objects: an egg carton for a fixed array, a bookshelf for a dynamic array, hotel rooms for an associative array, and a ticket line for a queue
Figure 1. Four array types, four everyday pictures: fixed is an egg carton, dynamic is a bookshelf, associative is a hotel, and a queue is a ticket line.

The four types in code

module tb;
  int fixed_arr[4];          // fixed size: always 4 slots
  int dyn_arr[];             // dynamic: size set at run time
  int assoc_arr[int];        // associative: only used keys exist
  int q[$];                  // queue: grows and shrinks at either end

  initial begin
    dyn_arr = new[3];
    assoc_arr[1000] = 7;
    assoc_arr[5]    = 9;
    q.push_back(10);
    q.push_back(20);
    q.push_front(5);

    $display("fixed: %0d slots", $size(fixed_arr));
    $display("dynamic: %0d slots", dyn_arr.size());
    $display("associative: %0d entries, keys 5 and 1000 only", assoc_arr.num());
    $display("queue: %p", q);
    $finish;
  end
endmodule

Output (tested on Verilator 5.020, 6 Nov 2026):

fixed: 4 slots
dynamic: 3 slots
associative: 2 entries, keys 5 and 1000 only
queue: '{'h5, 'ha, 'h14}

What to notice:

  • The queue line reads 5, 10, 20. Verilator prints %p in hex; your simulator may print decimal.
  • Key 1000 is set, but the associative array holds just 2 entries, not 1,001. That’s the hotel.

Growing a dynamic array without losing data

module tb;
  int buf_a[];
  initial begin
    buf_a = new[3];
    buf_a = '{11, 22, 33};

    buf_a = new[5];                 // new size, old values gone
    $display("new[5]          : %p", buf_a);

    buf_a = '{11, 22, 33};
    buf_a = new[5](buf_a);          // new size, old values copied in
    $display("new[5](buf_a)   : %p", buf_a);

    buf_a.delete();                 // back to size 0
    $display("after delete()  : size %0d", buf_a.size());
    $finish;
  end
endmodule

Output (Verilator 5.020):

new[5]          : '{'h0, 'h0, 'h0, 'h0, 'h0}
new[5](buf_a)   : '{'hb, 'h16, 'h21, 'h0, 'h0}
after delete()  : size 0
  • Line 1: new[5] alone wiped the values.
  • Line 2: 11, 22, 33, 0, 0. The old values survived because I passed the array in brackets: new[5](buf_a).

An associative array as a memory model

This is where associative arrays earn their place.

  • A 32-bit address space has 4 billion locations.
  • A test might write ten of them.
  • A fixed array that big would never fit in memory.
// A sparse memory model: only written addresses take up space.
module tb;
  bit [31:0] mem [bit [31:0]];
  bit [31:0] addr;
  initial begin
    mem[32'h0000_0010] = 32'hDEAD_BEEF;
    mem[32'h8000_0000] = 32'h1234_5678;
    mem[32'hFFFF_FFF0] = 32'hCAFE_F00D;

    $display("entries stored: %0d", mem.num());
    $display("0x10 written? %0d   0x20 written? %0d", mem.exists(32'h10), mem.exists(32'h20));

    if (mem.first(addr))
      do $display("  mem[0x%08h] = 0x%08h", addr, mem[addr]);
      while (mem.next(addr));

    mem.delete(32'h8000_0000);
    $display("after delete: %0d entries", mem.num());
    $finish;
  end
endmodule

Output (Verilator 5.020):

entries stored: 3
0x10 written? 1   0x20 written? 0
  mem[0x00000010] = 0xdeadbeef
  mem[0x80000000] = 0x12345678
  mem[0xfffffff0] = 0xcafef00d
after delete: 2 entries
  • Three writes, three entries, across the whole 4 GB range.
  • exists() tells you if an address was ever written.
  • first() and next() walk the keys in order.

A queue, step by step

module tb;
  int q[$];
  int x;
  initial begin
    q = '{1, 2, 3};
    q.push_back(4);        // 1 2 3 4
    q.push_front(0);       // 0 1 2 3 4
    q.insert(2, 99);       // 0 1 99 2 3 4
    x = q.pop_front();     // takes 0
    q.delete(1);           // removes 99
    $display("popped %0d, queue now %p, size %0d", x, q, q.size());
    $display("first %0d, last %0d", q[0], q[$]);
    $finish;
  end
endmodule

Output (Verilator 5.020):

popped 0, queue now '{'h1, 'h2, 'h3, 'h4}, size 4
first 1, last 4

q[$] always means the last item, whatever the size.

All four types side by side

FixedDynamicAssociativeQueue
Declareint a[8];int a[];int a[int];int a[

Still not sure? Answer three questions:

#93;;
Size known whenYou write the codeRun time, with new[]Never: it grows key by keyNever: it grows and shrinks
PictureEgg cartonBookshelfHotelTicket line
Best forRegister banks, fixed buffersPayloads whose length is randomizedSparse memory, lookups by ID or nameExpected transactions in a scoreboard, FIFOs
Cost of adding one itemNot possibleRebuild the whole arrayCheapCheap at the front or back

Still not sure? Answer three questions:

Flowchart: if you know the size when you write the code, use a fixed array. If keys are sparse or not numbers in a row, use an associative array. If items arrive and leave one at a time, use a queue. Otherwise use a dynamic array
Figure 2. Three questions that pick the right array type.

SystemVerilog array methods table

MethodFixedDynamicAssociativeQueue
size()Use $size(a)YesYes (same as num())Yes
new[N] / new[N](old)NoYes: resize, optionally keep valuesNoNo
delete()NoEmpties the arrayEmpties itEmpties it
delete(x)NoNoRemoves key xRemoves index x
exists(key)NoNoYesNo
first() last() next() prev()NoNoWalk the keys in orderNo
push_back() push_front()NoNoNoYes
pop_back() pop_front()NoNoNoYes
insert(i, x)NoNoNoYes
sort() rsort() reverse() shuffle()YesYesNoYes
sum() product() min() max()YesYesYesYes
find() find_index() find_first()YesYesYesYes

About the last three rows:

  • They’re the array manipulation methods from IEEE 1800-2017, section 7.12.
  • They work on any unpacked array.
  • One exception: the ordering methods (sort and friends) don’t apply to associative arrays.

Here they are on a fixed array of latency samples:

module tb;
  int lat[6] = '{40, 12, 75, 12, 33, 90};   // fixed: 6 latency samples
  int q[$];
  int idx[$];
  initial begin
    foreach (lat[i]) q.push_back(lat[i]);   // copy into a queue
    q.sort();                      // smallest first
    idx = lat.find_index(x) with (x > 50);
    $display("sum %0d, sorted %p", lat.sum(), q);
    $display("slow samples at index %p", idx);
    $finish;
  end
endmodule

Output (Verilator 5.020):

sum 262, sorted '{'hc, 'hc, 'h21, 'h28, 'h4b, 'h5a}
slow samples at index '{'h2, 'h5}
  • Sorted: 12, 12, 33, 40, 75, 90.
  • The two slow samples (75 and 90) sit at index 2 and 5.
  • find_index() hands back a queue, because it can’t know ahead of time how many matches there will be.

A working example: an out-of-order AXI scoreboard

This is the pattern I’d show anyone who asks why queues and associative arrays matter.

The AXI rule, in two lines (Arm AMBA AXI specification, IHI 0022):

  • Writes with different IDs may finish in any order.
  • Writes with the same ID must finish in the order they were sent.

Why should IDs differ in the first place? See array of unique random values.

So the scoreboard:

  • keeps one queue per ID,
  • stores those queues in an associative array keyed by the ID,
  • and pops the oldest write for that ID when a write response comes back.
AXI write scoreboard: writes for ID 3 and ID 9 go into an associative array of queues keyed by ID. ID 9 finishes first, the two ID 3 writes leave oldest first, and a response for ID 5 raises an error
Figure 3. An out-of-order AXI scoreboard: one queue per ID, stored in an associative array keyed by AWID.
// Out-of-order AXI write scoreboard: one queue of expected writes per ID.
class axi_wr;
  bit [3:0]  id;
  bit [31:0] addr;
  function new(bit [3:0] id, bit [31:0] addr);
    this.id = id; this.addr = addr;
  endfunction
endclass

class wr_scoreboard;
  axi_wr expected[int][$];        // key = AWID, value = queue of writes with that ID

  function void sent(axi_wr w);   // called when the master sends a write
    expected[w.id].push_back(w);
  endfunction

  function void got_bresp(bit [3:0] id);   // called on each B response
    axi_wr w;
    if (!expected.exists(id) || expected[id].size() == 0) begin
      $error("B response for ID %0d, but no write with that ID is open", id);
      return;
    end
    w = expected[id].pop_front();          // same ID: oldest write finishes first
    $display("ID %0d done: write to 0x%08h", id, w.addr);
    if (expected[id].size() == 0) expected.delete(id);
  endfunction

  function void report();
    $display("writes still open: %0d IDs", expected.num());
  endfunction
endclass

module tb;
  wr_scoreboard sb = new();
  initial begin
    axi_wr w;
    w = new(3, 32'h1000); sb.sent(w);
    w = new(9, 32'h2000); sb.sent(w);
    w = new(3, 32'h3000); sb.sent(w);   // second write on ID 3
    sb.got_bresp(9);                    // ID 9 finishes first: allowed
    sb.got_bresp(3);                    // ID 3: must be 0x1000, the older one
    sb.got_bresp(3);                    // then 0x3000
    sb.report();
    sb.got_bresp(5);                    // no open write on ID 5: error
    $finish;
  end
endmodule

Output (Verilator 5.020):

ID 9 done: write to 0x00002000
ID 3 done: write to 0x00001000
ID 3 done: write to 0x00003000
writes still open: 0 IDs
[0] %Error: ex5_sb.sv:20: Assertion failed in TOP.$unit.wr_scoreboard.got_bresp: B response for ID 5, but no write with that ID is open
%Error: ex5_sb.sv:20: Verilog $stop

Three types, three jobs:

  • The associative array finds the right ID instantly.
  • The queue keeps same-ID writes in order.
  • The exists() check catches a response the DUT should never have sent.

Verilator stops at $error. Most simulators log the error and keep running.

In a UVM testbench:

  • sent() is fed by the monitor on the AW channel.
  • got_bresp() is fed by the monitor on the B channel.
  • The data structure stays exactly the same.

Common mistakes (and the fix for each)

These five come up again and again on the Verification Academy forums.

  • Each case is retold in my own words, with a link to the original thread.
  • Mistakes 1 to 4 are tested in one file on Verilator 5.020.
module tb;
  int q[$];
  int arr[];
  int cnt[string];
  initial begin
    // Mistake 1: deleting inside foreach skips elements
    q = '{7, 7, 1, 7};
    foreach (q[i]) if (q[i] == 7) q.delete(i);
    $display("1 wrong : %p", q);
    q = '{7, 7, 1, 7};
    for (int i = q.size() - 1; i >= 0; i--) if (q[i] == 7) q.delete(i);
    $display("1 fixed : %p", q);

    // Mistake 2: popping while the loop checks size()
    q = '{1, 2, 3, 4, 5};
    for (int i = 0; i < q.size(); i++) void'(q.pop_front());
    $display("2 wrong : %0d left", q.size());
    q = '{1, 2, 3, 4, 5};
    while (q.size() != 0) void'(q.pop_front());
    $display("2 fixed : %0d left", q.size());

    // Mistake 3: growing a dynamic array with new[] loses data
    arr = '{5, 6};
    arr = new[4];
    $display("3 wrong : %p", arr);
    arr = '{5, 6};
    arr = new[4](arr);
    $display("3 fixed : %p", arr);

    // Mistake 4: reading a key that was never written
    $display("4 read  : %0d, exists = %0d", cnt["read"], cnt.exists("read"));
    cnt["read"]++;
    $display("4 after ++ : %0d", cnt["read"]);
    $finish;
  end
endmodule

Output (Verilator 5.020):

1 wrong : '{'h7, 'h1}
1 fixed : '{'h1}
2 wrong : 2 left
2 fixed : 0 left
3 wrong : '{'h0, 'h0, 'h0, 'h0}
3 fixed : '{'h5, 'h6, 'h0, 'h0}
4 read  : 0, exists = 0
4 after ++ : 1

1. Deleting from a queue inside foreach

  • What happened: someone wanted to drop every matching entry, and some were left behind (Verification Academy, 2016).
  • Why: delete index 0 and everything slides down one slot. The loop moves on to index 1 anyway, so the second 7 slips past.
  • Picture it: removing people from a ticket line as you walk along it. The person behind steps into a spot you already checked.
  • Fix: walk the queue backwards.

2. Popping while the loop re-checks size()

  • What happened: a for loop with pop_front() drained only part of the queue (Verification Academy, 2020).
  • Why: each pass, i goes up by one and the size goes down by one. They meet in the middle. Our run left 2 of 5.
  • Fix: while (q.size() != 0).

3. Growing a dynamic array with plain new[]

  • What happened: adding room to an existing array wiped its contents (Verification Academy, 2024).
  • Why: new[4] builds a brand new, empty bookshelf. The old books are gone.
  • Fix: new[4](arr) copies the old contents in first.
  • Tip: growing one item at a time? Use a queue instead.

4. Reading an associative array key that was never written

  • What happened: a warning about a “nonexistent associative array entry” (Verification Academy, 2022).
  • Why: the read returns the default value (0 for an int), and tools may warn about it (IEEE 1800-2017, section 7.8.6).
  • Watch out: the read doesn’t add the key, so exists() is still 0.
  • Fix: check exists() first. Counting with ++ is fine, because the write creates the key.

5. Using a wildcard index [*] for a lookup table

  • What happened: someone wanted a queue of transactions per ID and tried a wildcard index (Verification Academy, 2017).
  • Why: wildcard keys are stored as bit patterns, and you can’t use them in foreach.
  • Fix: say what the key is: trans_q[int][$], just like the scoreboard above.

Frequently asked questions

What is the difference between a dynamic array and a queue in SystemVerilog?

  • Both can change size at run time.
  • A dynamic array changes size only when you call new[], which rebuilds it. Use it when you know the size once, like a randomized payload length.
  • A queue grows and shrinks one item at a time with push_back() and pop_front(). Use it when items arrive and leave over time.

When should I use an associative array?

When the keys are sparse or not numbers in a row. Good fits:

  • a memory model for a 4 GB address space,
  • a lookup by transaction ID,
  • a count per string name.

You only pay for the keys you actually write.

Can I randomize a queue or a dynamic array?

  • Yes. Declare it rand and constrain its size().
  • The solver picks both the size and the values.
  • You can’t constrain the size or keys of an associative array, so for random stimulus a dynamic array or queue is the usual pick.

How do I find the size of each type?

  • Fixed: $size(a)
  • Dynamic and queue: a.size()
  • Associative: a.num() or a.size()

Is q[$] the last element?

Yes. Inside a queue index, $ means the last position. q[$] is the last item; q[0] is the first.

What to read next

References

  • IEEE 1800-2017, SystemVerilog standard: section 7.4 (packed and unpacked arrays), 7.5 (dynamic arrays), 7.8 (associative arrays), 7.9 (associative array methods), 7.10 (queues), 7.12 (array manipulation methods).
  • Arm AMBA AXI and ACE Protocol Specification (IHI 0022): transaction ordering by ID.
  • Verification Academy forum threads linked in the mistakes section (accessed 6 Nov 2026).
  • All code tested on Verilator 5.020 on 6 Nov 2026.