SystemVerilog offers a lot of flexibility for building data structures through its different array types. Picking the right one matters: a testbench scoreboard, a sparse lookup table, and a fixed-width register file all call for a different kind of array, and each one is recognized by a distinct declaration syntax.
This article gives a quick overview of the four array categories — static, dynamic, associative, and queue — so you can recognize each one and know where to dig deeper.
6 min read | Intermediate Level
What You'll Learn
- The four array categories in SystemVerilog and how to recognize each one
- How static arrays split further into packed and unpacked forms
- When to reach for a dynamic array, an associative array, or a queue
- Where to go for a deeper dive into each array type
Static Arrays
A static array is one whose size is known before compilation. The example below declares an 8-bit wide static array, assigns it a value, and iterates over it to print each bit.
module tb;
bit [7:0] m_data; // A vector or 1D packed array
initial begin
// 1. Assign a value to the vector
m_data = 8'hA2;
// 2. Iterate through each bit of the vector and print value
for (int i = 0; i < $size(m_data); i++) begin
$display ("m_data[%0d] = %b", i, m_data[i]);
end
end
endmodule
Output
ncsim> run m_data[0] = 0 m_data[1] = 1 m_data[2] = 0 m_data[3] = 0 m_data[4] = 0 m_data[5] = 1 m_data[6] = 0 m_data[7] = 1 ncsim: *W,RNQUIE: Simulation is complete.
Static arrays are further categorized into packed and unpacked arrays, based on whether the dimensions are declared before or after the variable name.
bit [2:0][7:0] m_data; // Packed
bit [15:0] m_mem [10:0]; // Packed + Unpacked
Learn more about SystemVerilog Packed Arrays
Unpacked arrays may be fixed-size arrays, dynamic arrays, associative arrays, or queues — the rest of this article covers those non-fixed-size forms.
Learn more about SystemVerilog Unpacked Arrays
Static arrays (packed or unpacked-fixed-size) are the default choice for RTL and for any testbench structure whose size you already know, like a fixed-depth register file or a bus with a known number of lanes.
Quick Check
Q: Is bit [15:0] m_mem [10:0]; a packed array, an unpacked array, or both?
Show Answer
Both. [15:0] is written before the name, so it's a packed dimension (each element is a 16-bit vector). [10:0] is written after the name, so it's an unpacked dimension (11 separate elements).
Dynamic Arrays
A dynamic array is one whose size isn't known at compile time — it's allocated and resized during runtime instead. It's easily recognized by its empty square brackets [ ].
int m_mem []; // Dynamic array, size unknown but it holds integer values
Learn more about SystemVerilog Dynamic Arrays
Dynamic arrays are a good fit when you know you're storing a list of same-typed elements, but the count depends on something learned at runtime — for example, the number of packets to send for a given test scenario.
Associative Arrays
An associative array stores content against a key rather than a sequential index. It's recognized by a data type sitting inside the square brackets — that type is the key.
int m_data [int]; // Key is of type int, and data is also of type int
int m_name [string]; // Key is of type string, and data is of type int
m_name ["Rachel"] = 30;
m_name ["Orange"] = 2;
m_data [32'h123] = 3333;
Learn more about SystemVerilog Associative Arrays
Associative arrays are the natural fit for sparse data, like tracking scoreboard entries by transaction ID or counting coverage hits by an address that could be anywhere in a huge memory space, without allocating storage for every possible index.
Quick Check
Q: In int m_name [string];, what is string — the data type or the key type?
Show Answer
The key type. The type inside the square brackets always describes the key used to look up an entry; the type before the variable name (int here) is the data stored at each key.
Queues
A queue lets you push data in and pop data out from either end. It's recognized by the $ symbol inside the square brackets.
int m_queue [$]; // Unbound queue, no size
m_queue.push_back(23); // Push into the queue
int data = m_queue.pop_front(); // Pop from the queue
Learn more about SystemVerilog Queues
Queues are the go-to structure for scoreboards, transaction pipelines, and any place you need strict ordering with fast insertion and removal at either end — something a plain dynamic array doesn't give you as conveniently.
Common Mistakes
Mistake #1: Reaching for a static array when the size isn't actually known
Declaring int m_mem [100]; "just to be safe" wastes storage if you rarely need that many entries, and still fails if you occasionally need more than 100. If the count depends on runtime conditions, use a dynamic array or a queue instead of guessing a fixed size.
Mistake #2: Assuming an associative array preserves insertion order
Associative arrays are indexed by key, not by position, so iterating with foreach does not guarantee the order entries were added in. If order matters, use a queue instead.
Mistake #3: Confusing the key type with the data type
In int m_name [string];, it's easy to misread which side is the key. The type before the name (int) is what's stored; the type inside the brackets (string) is what you index with.
Mistake #4: Popping from an empty queue
Calling pop_front() or pop_back() on an empty queue is not a runtime error — it returns the type's default value, leaves the queue unchanged, and may just issue a warning. That's easy to rely on by accident, so it's still worth checking m_queue.size() (or tracking expected pushes/pops) before popping in code that isn't guaranteed to have an entry waiting, since a silently-returned default value can mask a real bug.
Practice Exercise
Challenge: Pick the Right Array Type
You're writing a testbench component that tracks outstanding read requests by their unique transaction ID (a random 32-bit value), so you can look up and remove a request once its response arrives. Which array type fits best, and how would you declare it?
Hint
Transaction IDs are random 32-bit values, not a small sequential range, and you need to look one up directly rather than scan through a list.
Solution
// Associative array: key = transaction ID, data = the outstanding request
my_transaction pending_reqs [bit [31:0]];
// Add an outstanding request
pending_reqs[tx_id] = req;
// Look up and remove it once the response arrives
if (pending_reqs.exists(resp_id)) begin
my_transaction original_req = pending_reqs[resp_id];
pending_reqs.delete(resp_id);
end
An associative array is the right choice here: transaction IDs are sparse (not a small, dense 0..N range), and you need direct key-based lookup rather than positional access — exactly what a static or dynamic array isn't built for.
Summary
In this tutorial, you learned:
- Static arrays have a size known at compile time, and split further into packed and unpacked forms
- Dynamic arrays use empty
[ ]brackets and are sized at runtime - Associative arrays use a data type inside
[ ]to represent the key, ideal for sparse or non-sequential data - Queues use
[$]and support fastpush_back()/pop_front()-style insertion and removal at either end - Choosing the right array type up front avoids wasted storage, missing-order bugs, and runtime errors later on