SystemVerilog Arrays: Packed, Dynamic and Associative
Arrays are how a verification environment holds groups of related values: a bus word, a slice of memory, a scoreboard of expected results, or a table keyed by address. Plain Verilog gave you only fixed-size arrays. SystemVerilog adds several distinct array kinds, and a good deal of day-to-day testbench debugging comes down to picking the right one and knowing its built-in operations. This guide builds up the family from first principles, with code you can run yourself.
Packed versus unpacked: the core distinction
Every SystemVerilog array is either packed or unpacked, and the difference is decided purely by where the dimension sits relative to the variable name. A dimension written to the left of the name is packed; a dimension written to the right is unpacked.
A packed dimension describes a single contiguous vector of bits. Because the bits are adjacent, you can do arithmetic on the whole thing, take bit-selects and part-selects, and copy it as one value. An unpacked dimension describes a collection of separate elements laid out like a C array or a hardware memory; you address one element at a time, and the elements need not be stored contiguously.
| Aspect | Packed (left of name) | Unpacked (right of name) |
|---|---|---|
| Declaration | bit [31:0] w; | byte b [4]; |
| Stored as | One contiguous bit vector | Independent elements |
| Bit / part selects | Yes (w[15:8]) | No, element access only |
| Arithmetic on the whole | Yes | No |
| Typical use | Registers, buses, flags | Memories, buffers, scoreboards |
A worked example: packed, unpacked and multidimensional
You can also combine the two. bit [7:0] matrix [3][4] is an unpacked 3-by-4 grid whose every element is an 8-bit packed vector. The example below declares one packed word, one unpacked array of bytes, and a two-dimensional unpacked array, then shows the operations each one allows. Note the foreach (matrix[r, c]) form, which walks both unpacked dimensions at once.
module tb;
bit [31:0] packed_word;
byte unpacked_bytes [4];
bit [7:0] matrix [3][4];
initial begin
packed_word = 32'hDEAD_BEEF;
$display("packed_word = %h", packed_word);
$display("byte[1] (15:8) = %h", packed_word[15:8]);
unpacked_bytes = '{8'h11, 8'h22, 8'h33, 8'h44};
$display("unpacked_bytes[2]= %h", unpacked_bytes[2]);
foreach (matrix[r, c])
matrix[r][c] = r*10 + c;
$display("matrix[2][3] = %0d", matrix[2][3]);
$finish;
end
endmodulepacked_word = deadbeef byte[1] (15:8) = be unpacked_bytes[2]= 33 matrix[2][3] = 23 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp7ysfj_go/packed_unpacked.sv:14: $finish called at 0 (1s)
The part-select packed_word[15:8] pulls out one byte of the vector, something you cannot do on an unpacked array. The '{...} braces are an array literal used to assign every unpacked element in one statement.
Dynamic arrays: sizing at run time
A dynamic array has an unpacked dimension whose size is not known until simulation runs. You declare it with empty square brackets and allocate storage later with new[n]. This is ideal when the element count depends on a packet length, a random value, or data read from a file.
Two details matter. new[6](old) resizes the array to six elements while copying the existing contents across, so you grow an array without losing data. delete() frees all elements and returns the size to zero.
module tb;
int dyn [];
initial begin
dyn = new[4];
foreach (dyn[i]) dyn[i] = i*i;
$write("after new[4] size=%0d :", dyn.size());
foreach (dyn[i]) $write(" %0d", dyn[i]);
$write("n");
dyn = new[6](dyn);
dyn[4] = 100; dyn[5] = 200;
$write("after new[6] size=%0d :", dyn.size());
foreach (dyn[i]) $write(" %0d", dyn[i]);
$write("n");
dyn.delete();
$display("after delete size=%0d", dyn.size());
$finish;
end
endmoduleafter new[4] size=4 : 0 1 4 9 after new[6] size=6 : 0 1 4 9 100 200 after delete size=0 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpxvunlqdz/dynamic_array.sv:16: $finish called at 0 (1s)
Right after new[4] every element is the default value for its type (0 for int), which is why the loop fills them explicitly. The size() method reports the current length.
Associative arrays: sparse, keyed storage
When the index space is huge or not contiguous, an associative array is the right tool. Instead of a numeric position, it maps a key of almost any type to a value, storing only the entries you actually write. A model of a 4 GB address space, for instance, should never allocate four billion elements; an associative array indexed by address stores just the addresses that were touched.
Here the key is a string. Useful methods are num() for the entry count, exists(key) to test membership without creating an entry, and delete(key) to remove one. A foreach visits the keys in sorted order.
module tb;
int age [string];
initial begin
age["arjun"] = 21;
age["bhavya"] = 23;
age["chetan"] = 20;
$display("entries = %0d", age.num());
$display("bhavya = %0d", age["bhavya"]);
$display("has 'dev'? = %0d", age.exists("dev"));
foreach (age[name])
$display(" %s -> %0d", name, age[name]);
age.delete("chetan");
$display("after delete = %0d entries", age.num());
$finish;
end
endmoduleentries = 3 bhavya = 23 has 'dev'? = 0 arjun -> 21 bhavya -> 23 chetan -> 20 after delete = 2 entries
Because exists("dev") returns 0 and does not insert anything, it is the safe way to probe an associative array before reading, avoiding the accidental creation of empty entries.
Built-in array methods
SystemVerilog gives unpacked arrays a set of built-in methods that would otherwise be hand-written loops. They fall into three groups:
- Reduction methods collapse the array to a single value:
sum(),product(),and(),or(),xor(). - Locator methods take a
withexpression and return a queue of results:find,find_index,find_first,find_last. - Ordering methods rearrange elements in place or return a filtered queue:
sort,rsort,reverse,unique.
Inside a with clause the keyword item refers to the element under test. Locator results come back as a queue (declared with [$]), because the number of matches is not known in advance.
module tb;
int data [] = '{5, 2, 8, 2, 9, 1};
int res [$];
int idx [$];
initial begin
$display("sum = %0d", data.sum());
res = data.find with (item > 4);
$write("find > 4 :"); foreach (res[i]) $write(" %0d", res[i]); $write("n");
idx = data.find_index with (item == 2);
$write("index of 2 :"); foreach (idx[i]) $write(" %0d", idx[i]); $write("n");
data.sort();
$write("sorted :"); foreach (data[i]) $write(" %0d", data[i]); $write("n");
data.rsort();
$write("rsorted :"); foreach (data[i]) $write(" %0d", data[i]); $write("n");
res = data.unique();
$write("unique :"); foreach (res[i]) $write(" %0d", res[i]); $write("n");
$finish;
end
endmodulesum = 27 find > 4 : 5 8 9 index of 2 : 1 3 sorted : 1 2 2 5 8 9 rsorted : 9 8 5 2 2 1 unique : 9 8 5 2 1
Observe that find_index returns positions (1 and 3), not the values, while find returns the matching values themselves. After rsort leaves the array in descending order, unique keeps the first occurrence of each value, so the duplicate 2 is dropped.
Which array should you use?
| Array kind | Size | Index | Reach for it when |
|---|---|---|---|
| Packed | Fixed, compile time | Bit position | You need a vector you can slice and do arithmetic on |
| Fixed unpacked | Fixed, compile time | Integer | The element count is known and never changes |
| Dynamic | Set at run time | Integer | The length depends on run-time data |
| Associative | Grows per key | Any type | The index space is large or sparse |
Master these four and the matching method set, and most testbench data structures become a few lines instead of a page of loops.
Quick check
Where do packed and unpacked dimensions go relative to the variable name?
A packed dimension is written to the left of the name (for example bit [31:0] w); an unpacked dimension is written to the right (for example byte b [4]).
How do you enlarge a dynamic array without losing its current contents?
Use arr = new[n](arr). The argument in parentheses copies the old elements into the newly sized array.
What does exists("dev") return for an associative array that has no such key, and why is it useful?
It returns 0. It is useful because it tests for a key without creating an entry, unlike a plain read which can insert an empty element.
What data type do locator methods such as find and find_index return?
A queue (declared with [$]), because the number of matching elements is not known until the method runs.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses