SystemVerilog · Module 3
Associative Arrays
Arbitrary key types, exists(), delete(), iteration, sparse memory, scoreboard lookup.
Module 3 · Page 3.3
When Sequential Indices Make No Sense
You are modeling a 4GB address space. The DUT only touches a few dozen locations during any given test. A static array of 4 billion entries wastes memory. A dynamic array forces you to manage a mapping from address to array index manually. An associative array gives you exactly what you need: use the actual 32-bit address as the key, store only the addresses you actually write to, and let the simulator handle the hash table underneath.
Associative arrays are also the right tool for scoreboards that track transactions by ID, coverage models that track which opcodes have been seen, and any lookup table where the key space is sparse, string-based, or simply not known in advance. The syntax is clean: mem[32'hFFFF_0000] = 8'hAB writes a byte to an address. mem.exists(32'hFFFF_0000) checks if it was ever written. mem.delete(32'hFFFF_0000) removes it.
The behavior that catches engineers off guard: reading a non-existent key with val = mem[key] does not throw an error. It returns the type's default value (0 for int, 'x for logic), and most simulators issue a warning you will never notice in a large log.
The part worth getting exactly right is when an entry appears. A pure read is not supposed to create one — creation belongs to a write. But the forms that look like reads and are actually read-modify-writes do create the entry, silently and with the default as their starting value:
int hits [string];
if (hits["idle"] > 0) ... // read: returns 0, entry should not appear
hits["idle"]++; // read-modify-WRITE: entry created, becomes 1
hits["idle"] += 2; // same — creation, starting from the default 0
count_them(hits["idle"]); // if the formal is `ref`, this can create it tooThat distinction is what makes num() untrustworthy as a "how many did I see" metric in a coverage-style array: every ++ on a never-seen key adds an entry, so the count includes keys the design never produced. exists() is the only side-effect-free question, and it is the right guard whenever a key's presence is uncertain.
How Associative Arrays Work — Key Concepts
Think of an associative array as a hash map. Under the hood, most simulators implement it as a balanced tree or hash table indexed by the key type. The key can be any integral type, a string, or the wildcard [*] which accepts any integral key regardless of width. Elements are stored only for keys that have been written — the "size" of the array grows as you add entries.
Iteration order matters: for integer keys, foreach visits entries in ascending key order. For string keys, iteration is lexicographic. This sorted iteration is something dynamic and static arrays do not offer, and it makes associative arrays useful for ordered reporting and deterministic test reproduction.
Auto-allocation on write
Writing to any key instantly creates the entry. No new[] needed. The array grows on demand, one key at a time.
exists() — safe read guard
Returns 1 if the key has an entry, 0 if not. Always call before reading when the key might be absent. Reading missing keys returns default but may silently create the entry.
num() — entry count
Returns the current number of occupied entries. Unlike $size() for other arrays, associative arrays use the num() method.
Sorted iteration
foreach visits integer keys in ascending order, string keys lexicographically. first(), next(), last(), prev() for manual traversal.
Syntax — Every Method You'll Use
// ── Declaration ──────────────────────────────────────────────────
logic [7:0] mem [int]; // int key, 8-bit value
int score [string]; // string key, int value
logic [31:0] regs [logic[7:0]]; // 8-bit address key, 32-bit value
int wild [*]; // wildcard key — any integral type
// ── Write (auto-creates entry) ────────────────────────────────────
mem[32'hA000] = 8'hFF;
score["write"] = 10;
regs[8'h05] = 32'hCAFE_BABE;
// ── Read ──────────────────────────────────────────────────────────
logic [7:0] val = mem[32'hA000]; // OK: key exists
int s = score["write"]; // OK: key exists
// ── exists() — ALWAYS check before reading unknown keys ──────────
if (mem.exists(32'hA000))
val = mem[32'hA000];
else
$display("Key not found");
// ── num() — count of active entries ──────────────────────────────
$display("Entries: %0d", mem.num()); // how many keys are populated
// ── delete() — remove one key or all keys ────────────────────────
mem.delete(32'hA000); // remove one entry
mem.delete(); // remove ALL entries (clear the array)
// ── foreach — sorted iteration ────────────────────────────────────
foreach (mem[addr])
$display("mem[0x%0h] = 0x%02h", addr, mem[addr]); // ascending key order
// ── first() / next() / last() / prev() ───────────────────────────
int k;
if (mem.first(k))
do $display("0x%0h", k);
while (mem.next(k)); // walks all keys in order| Method | Signature | Returns | Notes |
|---|---|---|---|
exists(key) | int exists(key_t key) | 1 if key present, 0 if not | Must call before reading uncertain keys |
num() | int num() | Count of current entries | Use instead of $size() |
delete(key) | void delete(key_t key) | void | Removes one entry; safe if key absent |
delete() | void delete() | void | Removes all entries — clears the array |
first(key) | int first(ref key_t key) | 1 if array non-empty | Sets key to smallest/first entry |
last(key) | int last(ref key_t key) | 1 if array non-empty | Sets key to largest/last entry |
next(key) | int next(ref key_t key) | 1 if successor exists | Advances key to next entry in order |
prev(key) | int prev(ref key_t key) | 1 if predecessor exists | Retreats key to previous entry |
Visual — Sparse Storage and Iteration Order
Visual — What "sparse" actually means in memory
Sparse Memory Model — Only Occupied Entries Stored
Address space: 32-bit (4 billion possible addresses). Only 5 locations written. An associative array stores exactly 5 entries:
| Key (Address) | Value | exists()? | foreach visit order |
|---|---|---|---|
32'h0000_0100 | 8'hAA | 1 | 1st |
32'h0000_0200 | 8'hBB | 1 | 2nd |
32'hA000_0000 | 8'hCC | 1 | 3rd |
32'hF000_0000 | 8'hDD | 1 | 4th |
32'hFFFF_FFFC | 8'hEE | 1 | 5th |
| All other addresses | Not stored | 0 | Never visited |
Total memory used: proportional to 5 entries, not 4 billion. mem.num() = 5.
String Key Iteration — Lexicographic Order
| Write order | Key | Value | foreach visit order |
|---|---|---|---|
| 3rd write | "write" | 10 | 4th (lexicographic: w > r > n > i) |
| 1st write | "read" | 5 | 3rd |
| 4th write | "nop" | 0 | 2nd |
| 2nd write | "idle" | 3 | 1st (i comes first) |
Write order does not determine iteration order. foreach always visits in sorted key order — this makes reports deterministic regardless of the order transactions arrived.
exists() vs Direct Read — The Behavior Difference
| Operation | Key exists? | Result | Side effect |
|---|---|---|---|
mem.exists(key) | Yes | 1 | None |
mem.exists(key) | No | 0 | None — pure check, no entry created |
val = mem[key] | Yes | Stored value | None |
val = mem[key] | No | Default value (0/X) | May create entry with default value (tool-dependent) |
mem[key] = val | No | void | Creates entry — intended behavior |
Code Examples — Sparse Maps to Protocol Scoreboards
Example 1 — Beginner: Basic Integer and String Keys
module tb_aa_basics;
int addr_map [int]; // int key, int value
string op_name [int]; // int opcode → string name
int counters [string]; // string key → counter
initial begin
// Declarations must precede every statement in a begin-end block
// (IEEE 1800 §9.3.1), so recv_op cannot be declared beside its use below.
logic [7:0] recv_op;
// ── Integer key ───────────────────────────────────────────────
addr_map[32'h1000] = 100;
addr_map[32'h2000] = 200;
addr_map[32'hF000] = 999;
$display("num = %0d", addr_map.num()); // 3
$display("[0x1000] = %0d", addr_map[32'h1000]); // 100
// ── Exists check ──────────────────────────────────────────────
$display("exists(0x1000) = %0b", addr_map.exists(32'h1000)); // 1
$display("exists(0x5000) = %0b", addr_map.exists(32'h5000)); // 0
// ── Delete ────────────────────────────────────────────────────
addr_map.delete(32'h2000);
$display("After delete: num = %0d", addr_map.num()); // 2
// ── String key ────────────────────────────────────────────────
counters["read"] = 0;
counters["write"] = 0;
counters["idle"] = 0;
counters["read"]++;
counters["read"]++;
counters["write"]++;
// ── foreach: lexicographic order ─────────────────────────────
$display("Operation counts (sorted):");
foreach (counters[op])
$display(" %-8s: %0d", op, counters[op]);
// Prints: idle:0, read:2, write:1 (lexicographic)
// ── Opcode → name lookup ─────────────────────────────────────
op_name[8'h10] = "ADD";
op_name[8'h11] = "SUB";
op_name[8'h20] = "LOAD";
recv_op = 8'h11;
if (op_name.exists(recv_op))
$display("opcode 0x%02h = %s", recv_op, op_name[recv_op]); // SUB
$finish;
end
endmoduleExpected output:
num = 3
[0x1000] = 100
exists(0x1000) = 1
exists(0x5000) = 0
After delete: num = 2
Operation counts (sorted):
idle : 0
read : 2
write : 1
opcode 0x11 = SUBExample 2 — Intermediate: Sparse 32-bit Memory Model
module tb_sparse_mem;
// Sparse byte-addressable memory — 4GB address space, only used bytes stored
logic [7:0] mem [logic[31:0]];
// Write a word (4 bytes) to the memory model
task automatic write_word(input logic [31:0] addr, input logic [31:0] data);
mem[addr] = data[7:0];
mem[addr+1] = data[15:8];
mem[addr+2] = data[23:16];
mem[addr+3] = data[31:24];
endtask
// Read a word — returns X if any byte is missing
function automatic logic [31:0] read_word(input logic [31:0] addr);
if (!mem.exists(addr) || !mem.exists(addr+1) ||
!mem.exists(addr+2) || !mem.exists(addr+3))
return 32'hXXXX_XXXX;
return {mem[addr+3], mem[addr+2], mem[addr+1], mem[addr]};
endfunction
initial begin
logic [31:0] k; // declared first — see the note in Example 1
write_word(32'h0000_1000, 32'hAABB_CCDD);
write_word(32'hF000_0000, 32'h1234_5678);
$display("Bytes used: %0d", mem.num()); // 8
$display("[0x1000] = 0x%08h", read_word(32'h0000_1000)); // AABBCCDD
$display("[0x2000] = 0x%08h", read_word(32'h0000_2000)); // xxxxxxxx
// Dump all occupied addresses in order
if (mem.first(k)) do
$display(" mem[0x%08h] = 0x%02h", k, mem[k]);
while (mem.next(k));
$finish;
end
endmoduleExample 3 — Verification: Transaction Scoreboard by ID
module tb_scoreboard;
typedef struct {
logic [31:0] addr;
logic [31:0] data;
bit is_write;
} txn_t;
txn_t sent_txns [int]; // tid → sent transaction
txn_t rcvd_txns [int]; // tid → received response
int mismatch_cnt = 0;
// Called when driver sends a transaction
task automatic record_sent(int tid, txn_t t);
sent_txns[tid] = t;
endtask
// Called when monitor captures a response
task automatic record_rcvd(int tid, txn_t t);
rcvd_txns[tid] = t;
// Check immediately if sent was already recorded
if (sent_txns.exists(tid)) compare(tid);
endtask
// Compare a single transaction pair
task automatic compare(int tid);
if (sent_txns[tid].addr !== rcvd_txns[tid].addr ||
sent_txns[tid].data !== rcvd_txns[tid].data) begin
$error("TID=%0d MISMATCH: sent addr=0x%h data=0x%h got addr=0x%h data=0x%h",
tid,
sent_txns[tid].addr, sent_txns[tid].data,
rcvd_txns[tid].addr, rcvd_txns[tid].data);
mismatch_cnt++;
end else
$display("TID=%0d PASS", tid);
sent_txns.delete(tid); // clean up matched pair
rcvd_txns.delete(tid);
endtask
initial begin
txn_t t;
// Simulate out-of-order: responses arrive in different order from sends
t = '{32'h1000, 32'hAABB, 1}; record_sent(0, t);
t = '{32'h2000, 32'hCCDD, 0}; record_sent(1, t);
// Response for TID=1 arrives before TID=0
t = '{32'h2000, 32'hCCDD, 0}; record_rcvd(1, t);
t = '{32'h1000, 32'hAABB, 1}; record_rcvd(0, t);
$display("Mismatches: %0d", mismatch_cnt);
$finish;
end
endmoduleExpected output:
TID=1 PASS
TID=0 PASS
Mismatches: 0Example 4 — Corner Case: first()/next() Traversal and Empty Array Guard
module tb_aa_traverse;
int scores [string];
string k;
initial begin
// ── Empty array: first() returns 0 ────────────────────────────
if (!scores.first(k))
$display("Array is empty — first() returns 0"); // prints
// ── Populate ──────────────────────────────────────────────────
scores["charlie"] = 85;
scores["alice"] = 92;
scores["bob"] = 78;
// ── first() / next() — full forward walk ──────────────────────
$display("Forward (lexicographic):");
if (scores.first(k)) do
$display(" %s = %0d", k, scores[k]);
while (scores.next(k));
// ── last() / prev() — backward walk ──────────────────────────
$display("Backward:");
if (scores.last(k)) do
$display(" %s = %0d", k, scores[k]);
while (scores.prev(k));
// ── Delete during iteration is UNSAFE — collect keys first ───
string keys_to_delete [$]; // queue to collect
foreach (scores[s])
if (scores[s] < 90) keys_to_delete.push_back(s);
foreach (keys_to_delete[i])
scores.delete(keys_to_delete[i]); // safe: delete after iteration
$display("After removing <90: num = %0d", scores.num()); // 1 (alice)
$finish;
end
endmoduleExpected output:
Array is empty — first() returns 0
Forward (lexicographic):
alice = 92
bob = 78
charlie = 85
Backward:
charlie = 85
bob = 78
alice = 92
After removing <90: num = 1Simulation Behavior — Internals and Edge Cases
Memory and Performance Characteristics
Simulators implement associative arrays as balanced binary trees or hash maps. Lookup, insertion, and deletion are all O(log N) for tree-based implementations or average O(1) for hash-based. For large associative arrays (hundreds of thousands of entries), this is significantly more efficient than searching a dynamic array for a matching element. However, for small arrays (dozens of entries), the overhead of hashing or tree traversal can make a dynamic array faster — profile if performance matters.
What Happens When You Read a Missing Key
The LRM says reading a non-existent associative array entry returns the default value for the element type — 0 for two-state types, X for four-state. Whether the act of reading also creates the entry is tool-dependent. Aldec Riviera and Synopsys VCS handle this differently. The safe contract: always call exists() first. Never rely on "read returns 0 if missing" as a correctness guarantee in portable code.
| Scenario | Behavior | Safe practice |
|---|---|---|
| Read existing key | Returns stored value | Always safe |
| Read missing key | Returns default; may or may not create entry (tool-dependent) | Call exists() first |
| Write to new key | Creates entry, stores value | Always safe — this is the normal write operation |
| Delete existing key | Entry removed, num() decremented | Always safe |
| Delete non-existent key | No-op, no error | Safe — no guard needed |
| foreach during delete | Undefined — iterator may be invalidated | Collect keys first, delete after loop |
Where Associative Arrays Belong in Real Verification
// ── 1. SPARSE MEMORY MODEL — most common use ──────────────────────
logic [7:0] mem [longint unsigned]; // 64-bit address space, byte-granule
// mem[addr] = data; — writes any address instantly
// if (mem.exists(addr)) ... — checks before read
// ── 2. OUT-OF-ORDER SCOREBOARD — transactions arrive in any order ──
typedef struct { logic[31:0] data; int time_sent; } pending_t;
pending_t pending [int]; // TID → pending transaction
// Driver: pending[tid] = txn;
// Monitor: if (pending.exists(tid)) { compare; pending.delete(tid); }
// ── 3. OPCODE/REGISTER NAME TABLE ─────────────────────────────────
string reg_name [logic[11:0]]; // 12-bit reg addr → human name
reg_name[12'h000] = "STATUS";
reg_name[12'h004] = "CONTROL";
reg_name[12'hFFF] = "VERSION";
// In monitor: $display("Write to %s", reg_name.exists(addr) ? reg_name[addr] : "UNKNOWN");
// ── 4. COVERAGE TRACKING — which opcodes were exercised ───────────
int opcode_hits [logic[7:0]];
// In monitor: opcode_hits[op]++; (creates entry if first occurrence)
// End-of-test report:
// foreach (opcode_hits[op])
// $display("op=0x%02h hit %0d times", op, opcode_hits[op]);
// ── 5. CONFIGURATION DATABASE (UVM resource-like) ─────────────────
int cfg_int [string];
string cfg_string [string];
cfg_int["timeout_cycles"] = 1000;
cfg_string["interface_name"] = "axi_if";
// ── 6. PROTOCOL COVERAGE: seen address ranges ─────────────────────
int region_hits [string];
function automatic void classify_addr(logic [31:0] a);
string region = (a inside {[32'h0:32'hFFF]}) ? "BOOT" :
(a inside {[32'h1000:32'hFFFF]}) ? "SRAM" : "PERIPH";
region_hits[region]++;
endfunctionBugs Engineers Hit With Associative Arrays
Bug 1 — Reading a Missing Key: Silent Default Value
int hit_count [logic[7:0]]; // tracks how many times each opcode seen
// BUGGY: reading an unseen opcode returns 0, which looks correct
// But on some tools it also silently CREATES the entry with value 0
// Now the coverage report shows the opcode was "seen" when it wasn't
logic [7:0] op = 8'hAB;
if (hit_count[op] == 0) // reads a missing key — may create it!
$display("opcode 0xAB never seen"); // correct by luck, but entry now exists
// End-of-test: hit_count.num() = 1, but opcode was never actually driven!
// CORRECT: always use exists() to check presence
if (!hit_count.exists(op))
$display("opcode 0xAB never seen"); // purely checks, no side effect
else if (hit_count[op] == 0)
$display("seen but count is 0 — shouldn't happen");Bug 2 — Deleting During Iteration: Undefined Behavior
int data [string];
data["a"]=1; data["b"]=2; data["c"]=3;
// BUGGY: deleting inside foreach — undefined/tool-dependent behavior
foreach (data[k])
if (data[k] < 2) data.delete(k); // may skip entries or crash
// CORRECT: collect keys to delete, then delete after loop
string to_del [$];
foreach (data[k])
if (data[k] < 2) to_del.push_back(k);
foreach (to_del[i])
data.delete(to_del[i]); // safe: delete after iteration completeBug 3 — Increment of a Never-Seen Key: Unintentional Creation
int hit [logic[7:0]];
// This is actually FINE for int keys — default is 0, so 0++ = 1 ✓
// hit[op]++ works correctly for int-valued AA with integer keys
hit[8'hAB]++; // creates entry with value 1 if key was absent
hit[8'hAB]++; // now 2 — correct
// PROBLEM: logic-type values start as X, not 0
logic [7:0] count [int];
count[5]++; // X + 1 = X — the counter is stuck at X forever!
$display("count[5] = %0d", count[5]); // X
// CORRECT: use int or bit for counters, not logic
int safe_count [int];
safe_count[5]++; // int default is 0, so 0+1 = 1 ✓Bug 4 — Pending Entries Left in Scoreboard at End-of-Test
Scoreboard ends with pending entries — and the leftover pattern names the cause
SCOREBOARD-DRAINA test reports zero mismatches and passes. The drain check then reports sent.num() = 17 — seventeen transactions were issued and never matched. Nothing failed a comparison, because the comparisons never happened.
Expected. Every record_sent(tid) is eventually followed by a record_rcvd(tid) with the same key, the pair is compared, and both entries are deleted. num() returns to zero.
Actual. Seventeen keys remain. Crucially, an out-of-order scoreboard degrades locally — one unmatched ID leaves exactly one stranded entry and every other transaction still compares correctly. That is the opposite of the in-order queue scoreboard, where a single drop desynchronises everything behind it. Here the damage does not spread, which is precisely why the leftover set is diagnostic.
Do not reason from the count. Dump the keys — the associative array is the only scoreboard structure that can tell you exactly which transactions went missing:
function void check_phase(uvm_phase phase);
foreach (sent_txns[tid])
`uvm_error("SB", $sformatf("unmatched SENT tid=%0d addr=0x%0h", tid, sent_txns[tid].addr))
foreach (rcvd_txns[tid])
`uvm_error("SB", $sformatf("unmatched RCVD tid=%0d addr=0x%0h", tid, rcvd_txns[tid].addr))
endfunctionThen read the shape of what is left, because each pattern has a different cause:
| Leftover pattern | What it means |
|---|---|
| A contiguous block of the highest IDs | Drain problem — responses were still in flight when the test ended |
| IDs scattered through the run | The DUT genuinely dropped those transactions |
Every ID unmatched, and rcvd also non-empty | Key disagreement — responses arrived under different IDs and filled rcvd instead of matching |
sent empty but rcvd non-empty | The DUT produced responses nobody asked for — replay, or a stuck valid |
In the case above the leftovers were every ID from 40 upward, contiguous — the third row, then the first. The DUT remapped IDs above a threshold, so those responses populated rcvd_txns under keys that never matched, and the test's end-of-test delay was also too short to expose it as a hang.
The deeper cause is a silent scoreboard. Comparison only runs inside if (sent_txns.exists(tid)), so a response whose key never matches takes the else path — which did not exist. Nothing failed because nothing was checked.
Make the unmatched path loud, and make the drain check mandatory:
task automatic record_rcvd(int tid, txn_t t);
if (!sent_txns.exists(tid))
`uvm_error("SB", $sformatf("response tid=%0d was never sent - ID remap?", tid))
rcvd_txns[tid] = t;
if (sent_txns.exists(tid)) compare(tid);
endtaskA response for an ID that was never sent is, by itself, conclusive evidence of a key-space disagreement — and it fires at the moment it happens rather than at end of test.
Three habits. Every scoreboard gets a drain check that dumps keys, not just counts — num() != 0 is a failure and the key list is the diagnosis. Every unmatched arrival is an error at the time it arrives, never a silent else. And choose the structure from the ordering contract: associative-by-ID when the DUT may reorder or drop, queue only when in-order no-drop delivery is guaranteed. The associative form costs a key and buys you the ability to name exactly which transactions vanished.
Interview Questions
Any integral type (int, bit, logic [N:0], longint), a string, a class handle, or the wildcard [*], which accepts any integral key. The key type also fixes the iteration order: integral keys are visited in ascending numeric order and string keys lexicographically, regardless of the order they were written.
That sorted iteration is the property worth remembering, because it is what makes an end-of-test dump deterministic — two runs that saw the same transactions in different orders produce byte-identical reports. Neither static nor dynamic arrays give you that. The wildcard [*] is the one to use sparingly: it buys generality and costs compile-time checking on the key.
Best Practices & Coding Guidelines
Always exists() before read
Reading a missing key returns default value and may create an entry. Use exists(key) for uncertain reads. Direct read is fine only when you know the key was previously written.
Use int/bit for counters
logic-valued AA entries default to X. Incrementing X gives X — the counter is permanently broken. Use int for any counter-style associative array value.
Never delete during foreach
Modifying the key set while iterating is undefined. Collect keys to delete into a temporary queue or dynamic array, then delete them after the loop completes.
Drain check at end-of-test
Any scoreboard associative array that holds pending transactions should be checked at end-of-test. Non-zero num() means the DUT missed transactions — this is a test failure, not just a warning.
| Use case | Best array type | Reason |
|---|---|---|
| Full address space memory model | Associative [int] or [longint] | Sparse — only stores written addresses |
| Out-of-order transaction scoreboard | Associative [int] (TID key) | O(log N) lookup by ID, any-order matching |
| Named configuration table | Associative [string] | Human-readable keys, lexicographic iteration |
| Small fixed lookup table | Static array or case statement | Simpler, faster for small known sets |
| Coverage: which opcodes seen | Associative [opcode_type] | Auto-creates on first occurrence; num() = unique count |
Where This Is Specified
Associative arrays are defined in IEEE Std 1800 (SystemVerilog), clause 7 — Aggregate data types — the type and its legal key types in §7.8, and the built-in methods (num, size, delete, exists, first, last, next, prev) in §7.9. The IEEE Standards Association listing is the primary source.
Two things reward reading the clause directly. First, the standard defines the iteration order as ascending for integral keys and lexicographic for strings — so the deterministic reporting this page leans on is a language guarantee, not a convenient simulator habit. Second, the standard is explicit that modifying an associative array while iterating it yields undefined behaviour, which is why the collect-then-delete idiom is a rule rather than a style preference.
The declaration-placement rule behind the corrections in Examples 1 and 2 is §9.3.1: every declaration in a begin–end block must precede the first statement.
Related lessons. The other three array types are fixed-size arrays, dynamic arrays, and queues — with array methods for the shared operations and randomising arrays for the constraint side these arrays deliberately sit outside. The out-of-order scoreboard this page builds is the structural counterpart to the in-order queue scoreboard in queues, and the transaction identity it keys on is UVM sequence items.
Summary
Associative arrays are the right tool whenever the key space is sparse, irregular, or string-based. The auto-allocation on write, sorted iteration, and O(log N) lookup make them ideal for memory models, out-of-order scoreboards, and coverage databases. The three behavioral rules that matter in practice: always guard uncertain reads with exists(), use int for counter-style values rather than logic, and never delete keys during a foreach loop.
- Auto-allocation on write — no
new[]needed. Any key you write to is created instantly. exists(key)is the safe read guard. Reading a missing key returns default but may create the entry — tool-dependent behavior you cannot rely on.num()returns the entry count — not$size(). A non-zero num() at end-of-test in a scoreboard means dropped transactions.- Iteration is sorted. Integer keys ascending, string keys lexicographic — independent of write order. Deterministic reporting without extra sorting.
- Verification-only. Cannot be synthesized. Keep them in testbench packages.
Part of SystemVerilog Fundamentals·Arrays·Lesson 16 of 53
View program