在 SystemVerilog 中,关联数组(Associative Array)是一种非常有用的数据结构,它允许使用任意类型的数据作为索引(称为“键”),并将数据存储在对应的稀疏可变大小的集合中。与定宽数组或动态数组不同,关联数组只为那些实际赋过值的索引分配存储空间,非常适合建模稀疏存储查找表字典结构。


🧠 核心含义

  • 键-值对存储:每个条目由一个唯一的“键”和一个对应的“值”组成。

  • 稀疏性:并非所有可能的索引(如 0..2^32-1)都占用内存,只有被写入的索引才会创建条目。

  • 动态增长:添加新条目时,关联数组自动扩大;删除条目时,相应内存被释放。

  • 索引类型灵活:键可以是 整型字符串类句柄,甚至是通配符用户自定义类型

你可以把它想象成 Verilog 中的 reg [7:0] mem [0:1023] 的超级升级版——不再局限于连续整数索引,也不要求提前声明大小。


📚 基本语法与声明

// 声明:data_type 是值类型,index_type 是键的类型
data_type array_name [index_type];

// 示例
int               dict[string];        // 用字符串索引,值为整数
string            rev_map[int];        // 用整数索引,值为字符串
my_class          obj_pool[my_class];  // 用类句柄作为索引(键为对象引用)
bit [7:0]         byte_by_addr[longint]; // 用64位整数索引
real              values[shortint];    // 用短整数索引

关联数组不需要声明大小,访问之前未赋值的索引会返回默认值(对于 int 是 0,对于 string 是 "" 等),但不会自动创建条目(除非你在读取时隐式创建——通常不推荐)。


🛠️ 常用内建方法

方法 说明
array.num() 返回当前数组中条目的个数
array.delete() 删除整个数组或删除指定索引的条目(如 array.delete(idx)
array.exists(idx) 检查给定索引是否存在条目
array.first(itr) 获得第一个索引(用于迭代),通过引用返回
array.next(itr) 获得下一个索引,常用于遍历
array.last(itr) 获得最后一个索引
array.prev(itr) 获得前一个索引

💡 详细举例

例1:用字符串键存储学生成绩

program associative_array_demo;
    // 声明关联数组:键为字符串(学生姓名),值为整数(分数)
    int score[string];

    initial begin
        // 赋值
        score["Alice"] = 95;
        score["Bob"]   = 87;
        score["Carol"] = 92;

        $display("Number of students: %0d", score.num());   // 3

        // 访问存在的键
        $display("Bob's score: %0d", score["Bob"]);         // 87

        // 检查不存在的键
        if (score.exists("Dave"))
            $display("Dave's score: %0d", score["Dave"]);
        else
            $display("Dave not found");

        // 遍历所有条目(注意:遍历顺序不保证与插入顺序一致)
        begin
            string name;
            int    sc;
            if (score.first(name)) begin
                do begin
                    sc = score[name];
                    $display("%s -> %d", name, sc);
                end while (score.next(name));
            end
        end

        // 删除一个条目
        score.delete("Bob");
        $display("After delete, num = %0d", score.num());    // 2
    end
endprogram

例2:用整数键实现稀疏内存模型

模拟一个有 4GB 地址空间的存储器,但只实际存储被写入的地址:

module sparse_memory;
    // 键为 64 位地址,值为字节数据
    typedef bit [7:0] byte_t;
    byte_t mem[longint];

    initial begin
        // 只写入很少几个地址,关联数组只为这些地址分配空间
        mem[64'h0001_0000] = 8'hAA;
        mem[64'hFFFF_FFFF] = 8'h55;
        mem[64'h1000_0000_0000_0000] = 8'hFF;

        $display("Number of stored bytes: %0d", mem.num());   // 3

        // 读取未初始化的地址 -> 返回默认值 0,但不会新增条目
        $display("mem[0] = %x (still %0d entries)", mem[0], mem.num()); // 0, 3
    end
endmodule

例3:用类句柄作为索引(追踪每个对象的特定属性)

class packet;
    int id;
    function new(int id); this.id = id; endfunction
endclass

module class_key_demo;
    packet pkt1, pkt2;
    // 键是 packet 句柄,值是到达时间
    int arrival_time[packet];

    initial begin
        pkt1 = new(1);
        pkt2 = new(2);
        
        arrival_time[pkt1] = 1000;
        arrival_time[pkt2] = 1020;

        $display("pkt1 arrival: %0d", arrival_time[pkt1]); // 1000
        $display("pkt2 arrival: %0d", arrival_time[pkt2]); // 1020
    end
endmodule

例4:使用通配符索引(*

[*] 表示索引可以是任何积分值(int、bit等),但不推荐在要求代码清晰且类型安全的场景使用:

int mem[*];  // 索引类型未指定,可接受任何积分类型

⚠️ 重要注意事项

  1. 读取不存在的索引:返回默认值,但不会自动创建条目。如果之后希望删除整个数组或准确计算 num(),这一点很重要。

    int a[string];
    int x = a["hello"];   // x 得到 0,但数组仍然是空的(num() == 0)
  2. 遍历顺序:关联数组的遍历顺序是不确定的。如果需要有序访问,请使用 sort 方法或 queue

  3. 性能:对于少量条目(< 1000),关联数组非常高效。超大条目时哈希冲突可能影响性能。

  4. 可比性:只有可比较的类型才能作为键(如整数、字符串、类句柄)。不能使用实数、unpacked struct 等不可直接比较的类型作为键。


🔁 关联数组 vs. 其他数组类型

特性 定宽数组 动态数组 关联数组
声明时大小 需要 不需要(运行时定) 不需要
索引类型 整数 整数 任意类型(整数/字符串/句柄等)
存储稀疏性 全部连续分配 全部连续分配 仅存储赋值的条目
动态改变大小 可以(new[] 自动
适用场景 固定大小存储 连续增长的存储 查找表、字典、稀疏映射

✅ 总结

  • 关联数组 = 键值对的集合,键可以是任何数据类型。

  • 用于建模查找表稀疏存储配置映射对象缓存等。

  • 方法:existsnumdeletefirst/next 用于遍历。

  • 内存高效:只为使用的条目分配存储。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐