每日一 R「12」数据结构(三)哈希表
在上节课程中,我们学习了 Rust 中的切片,今天我们将一块学习下哈希表。哈希表在任何一门开发语言中都是非常重要的容器数据结构,甚至很多语言都将哈希表作为一种内置的数据结构,例如 Java。
01-Rust 中的哈希表 HashMap
A hash map implemented with quadratic probing and SIMD lookup.译:实现了二次探查和 SIMD 查找的哈希表。
官方文档中对哈希表的描述如上。从中我们可以看到两个关键元素:二次探查和 SIMD 查找。首先,学过数据结构的同学应该了解,哈希表主要用于将大量的可能输入映射(或散列)到有限容量的哈希表中。因为输入远远大于容量,所以就可能存在两个不同的输入,根据散列算法映射到哈希表中的统一位置(哈希碰撞或散列碰撞)。为了解决碰撞,可采用不同的算法,例如链地址法,即将碰撞的元素串联到链表中,Java 中的 HashMap 采用的就是这种方法。例如开放寻址法,即将碰撞的元素存储在当前位置向后线性查找、或非线性查找到的空闲位置。从上面的描述中,我们知道 Rust 中 HashMap 采用的是开放寻址法处理冲突,而且使用得是二次探查(非线性)。所谓二次探查就是指,遇到碰撞时,从当前位置向后查找,每次增加 2 的 n 次方,直到找到空闲位置。至于另外一个关键点 SIMD,在学习完数据结构后,我们再回过头来学习。
01.1-哈希表结构
从 HashMap 的源码中可以看到,标准库容器 HashMap 实际上底层实现是 hashbrown::HashMap。泛型 K/V 表示 key/value 键值对类型,S 是哈希算法的状态,默认为 RandomState(SipHash 作为缺省的哈希算法,是一个加密安全的哈希函数),占两个 u64。
HashMap 的底层是个 RawTable<T, A>,其中 T 是 (K, V),A 是内存分配器。RawTable 中有一个域 marker,它是 PhantomData 类型,不占空间,仅用来高速静态分析器存储的结构是 (K, V) 类型。
RawTable 的关键域是 RawTableInner<A>,它包含了如下几个元素:
bucket_mask,值为桶数量减一
ctrl,指向哈希表末端的指针(后面会详细介绍其设计思想)
growth_left,下次扩容前,还能插入的数量
items,哈希表中元素数量
alloc,为使用的内存分配器
01.2-哈希表的基本用法
01.3-哈希表的内存布局
Rust 哈希表中最重要的或者说最精髓的地方在于 ctrl 表的设计。当使用 HashMap::new() 方法创建哈希表后,在栈内存中会存在下图右上角所示的胖指针。初始时哈希表中没有任何元素,ctrl 指向默认的空表地址。当逐渐向表中插入数据后,会在堆上分配空间,ctrl 指向 ctrl table 位置,哈希表的表头地址可以通过 ctrl 地址减去 bucket_size * (key + value) 得到。
ctrl 表是一个由若干个 128 bit 或 16 字节的 group 组成的。Group 中的每个字节称为 ctrl byte,对应一个 bucket。Ctrl byte 如果是 0xff 则表示为空。
Rust 在加载 ctrl table 时每次加载一个 group,即 16 字节。寻址时通过 hashCode 与 group 中的 ctrl byte 进行 mask 比较来确定位置。这就是前面提到的 Rust 中哈希表的第二个特性:SIMD 查表。SIMD 借助现代 CPU 单指令多数据集的能力,通过一个指令可以将多个相关数据(16 个 ctrl bytes)加载到内存,大大提高了查表的速度。
01.4-再哈希与自增长
首先,与 Java 中的哈希表类似,Rust 中的哈希表会按需扩容,扩容方式是按 2 的幂扩容。
其次,扩容时会涉及到哈希表中元素的再哈希(即重新计算元素所述的 bucket)。
最后,再哈希时,如果 key 实现了 copy trait,会对值进行拷贝;如果 key 是像 String 这种类型,堆上的 String 并不会移动,拷贝的是 (addr|len|cap) 胖指针。
01.5-删除值
删除值并不需要真正的清除键值对占用的内存空间,只需要将 bucket 对应的 ctrl byte 置为 0xff 即可。因此,Rust 中删除效率也非常高。
但是,这种设计也存在一个问题。当键值对拥有额外内存时,例如 String,删除并不会立即回收 String 占用的内存空间。而是等到下次再使用这个 bucket 时,再回收之前的对象。有可能会造成内存泄漏。
如果大量添加元素、之后再移除大量元素,可以通过 shrink_to_fit / shrink_to 来释放掉不需要的内存,以提高内存使用率。
02-其他相关结构
02.1-HashSet / BTreeMap / BTreeSet
除了常规的哈希表,Rust 中还提供了 HashSet / BTreeMap / BTreeSet 等与哈希表密切相关的数据结构。
HashSet 是 HashMap 的简化版本,底层实现是 HashMap<K, ()>。
BTreeMap 是在 HashMap 的基础上,增加了有序性,它会按照 key 的升序来存储、便利内部元素。
BTreeSet 是 BTreeMap 的简化版本,可用来表示有序集合。
BTreeMap / BTreeSet 实现有序的关键是 PartialOld / Old trait。
02.1-自定义结构作为 HashMap 的 key
如果要用自定义结构作为哈希表的 key,该结构需要实现 Hash / PartialEq / Eq trait。
Hash trait,是数据结构能够计算出哈希值
PartialEq / Eq,数据结构之间相等和不相等关系
本节课程链接《17|数据结构:软件系统核心部件哈希表,内存如何布局?》
历史文章推荐
版权声明: 本文为 InfoQ 作者【Samson】的原创文章。
原文链接:【http://xie.infoq.cn/article/802da2afceecbb40a4ae6753b】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论