陈天 · Rust 编程第一课
陈天
Tubi TV 研发副总裁
23195 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 65 讲
基础篇 (21讲)
陈天 · Rust 编程第一课
15
15
1.0x
00:00/00:00
登录|注册

17|数据结构:软件系统核心部件哈希表,内存如何布局?

二次探查
让自定义的数据结构做 Hash key
删除一个值
哈希表重新分配与增长
ctrl 表
RawTable
RandomState
开放寻址法
链地址法
HashMap 的内存布局
HashMap 的基本使用方法
HashMap 的数据结构
SIMD 查表
哈希表的冲突解决机制
BTreeMap
HashSet
Rust 的哈希表
哈希表和列表的相似性
哈希表的重要性
使用文中同样的方式,结合 rust-gdb / rust-lldb 探索 BTreeMap。你能画出来在插入以 26 个字母为 key,1~26 为 value 后的 BTreeMap 的内存布局么?
思考一下,如果一个 session 表的 key 是 (Source IP、Source Port、Dst IP、Dst Port、Proto) 这样的长度 15 个字节的五元组,value 是 200 字节的 Session 结构,要容纳 1200000 个 Session,整个哈希表要占多大的堆内存?内存的利用率如何?
修改下面代码的错误,使其编译通过
HashSet / BTreeMap / BTreeSet
哈希表
如何使用 rust-gdb / rust-lldb?
为什么 Rust 的 HashMap 要缺省采用加密安全的哈希算法?
思考题
数据结构:软件系统核心部件哈希表,内存如何布局?
参考资料

该思维导图由 AI 生成,仅供参考

你好,我是陈天。
上一讲我们深入学习了切片,对比了数组、列表、字符串和它们的切片以及切片引用的关系。今天就继续讲 Rust 里另一个非常重要的集合容器:HashMap,也就是哈希表。
如果谈论软件开发中最重要、出镜率最高的数据结构,那哈希表一定位列其中。很多编程语言甚至将哈希表作为一种内置的数据结构,做进了语言的核心。比如 PHP 的关联数组(associate array)、Python 的字典(dict)、JavaScript 的对象(object)和 Map。
Google 的工程师 Matt Kulukundis 在 cppCon 2017 做的一个演讲,说:全世界 Google 的服务器上 1% 的 CPU 时间用来做哈希表的计算,超过 4% 的内存用来存储哈希表。足以证明哈希表的重要性。
我们知道,哈希表和列表类似,都用于处理需要随机访问的数据结构。如果数据结构的输入和输出能一一对应,那么可以使用列表,如果无法一一对应,那么就需要使用哈希表。

Rust 的哈希表

那 Rust 为我们提供了什么样的哈希表呢?它长什么样?性能如何?我们从官方文档学起。
如果你打开 HashMap 的文档,会看到这样一句话:
A hash map implemented with quadratic probing and SIMD lookup.
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了 Rust 语言中的哈希表实现,包括哈希表的重要性和应用、冲突解决机制、数据结构和内存布局、以及基本使用方法。通过示例代码展示了 Rust 哈希表的动态增长和缩减过程。文章还介绍了在 Rust 中探索 HashMap 数据结构时,通过使用 std::mem::transmute 方法来打印数据结构,以及通过 rust-gdb / rust-lldb 来查看哈希表的内存布局。通过这些内容,读者可以深入了解 Rust 中哈希表的设计原理和实际应用,对于想深入了解 Rust 哈希表的开发者具有很高的参考价值。文章还介绍了哈希表重新分配与增长、删除值的过程,以及如何让自定义的数据结构成为 HashMap 的 key。此外,还简要介绍了与 HashMap 相关的其它几个数据结构,包括 HashSet、BTreeMap 和 BTreeSet。通过本文的总结,读者可以快速了解 Rust 中哈希表的实现原理、操作方法以及相关数据结构的特点,为他们在实际开发中应用这些知识提供了指导和参考。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《陈天 · Rust 编程第一课》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(18)

  • 最新
  • 精选
  • Geek_5244fa
    说一下对 hashbrown 的理解。 一般的哈希表是对数组大小取模(hash % len)来定位位置的,但是 hashbrown 把 hash 分两部分使用: 1. 低几位(& bucket_size)定位在数组的位置 2. 高 7 位存到对应位置的 ctrl 块里,类似指纹的作用 一般哈希表获取时,取模定位到位置后,要完整对比 key 才能知道是找到(key相同)还是要探查(key 不同)。 而 hashbrown 可以利用 ctrl 里存起来的高 7 位快速发现冲突的情况(低几位相同但高7 位不同),直接进入下一步探查。

    作者回复: 赞!

    2021-10-05
    3
    14
  • GeekCiao
    aHash 已经可以做到DOS防护了:https://github.com/tkaitchuck/aHash/wiki/How-aHash-is-resists-DOS-attacks

    作者回复: 👍

    2021-10-01
    9
  • qinsi
    SIMD lookup也只是在x86/64上用sse2而已,据说avx/neon效果不佳: https://github.com/rust-lang/hashbrown/blob/cedc3267e4/src/raw/mod.rs#L16 看了下其他平台也都还没稳定: https://doc.rust-lang.org/core/arch/index.html#modules

    作者回复: 这是个和 CPU 指令集紧密相关的操作。x86/64 CPU 目前还是服务器的主流(虽然未来不一定)

    2021-10-02
    4
  • 交流会
    老师您好,有个疑问不太懂请教一下: 您在文中说 ---- ...在我的 OS X 下,一开始哈希表为空,ctrl 地址看上去是一个 TEXT/RODATA 段的地址,... 想请问一下,通过看数据地址怎么能区分是在 段上还是堆栈上的? 谢谢~

    作者回复: 你可以在不同的系统上打印 main 函数的地址和 main 下一个 u32,一个 vec 的堆地址,就可以大概看到不同地址的区别了。比如: ```rust fn main() { let a = 1; let arr = vec![1,2,3]; println!("main: {:p}, a: {:p}, s: {:p}", main as *const (), &a, &arr[0]); } ``` 在 playground 下的打印(https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8003d4ccfbf6d63280b124811a219f4a): main: 0x55bcfa1f5100, a: 0x7ffcd8b6adec, s: 0x55bcfb6ca9d0 如果你想知道更详细的各个段的地址,可以使用 readelf 或者在 gdb 里面查看。

    2021-12-05
    2
  • 徐洲更
    HashMap使用的一个问题:文章例子插入的元素是char和i32, 长度固定都是32字节,HashMap在插入数据时,从图上看,是直接将值记录在堆内存中数组对应的位置上。对于一个复杂的数据类型,比如说Key是String,Value是Vec<String>, 那么记录的是不是就是 大小为usize 的指针呢?

    作者回复: 不是 usize 大小的指针。你可以自己想想 String 和 Vec<T> 的结构都是什么样子的。所以这里 key 是 24 个字节,value 也是 24 个字节,它们再通过结构里的指针指向额外的堆地址里的内容。

    2021-10-20
    2
    2
  • Ixa
    把 PartialOrd 和 PartialEq 实现即可解决报错。使用词典的cmp来进行对比 impl Ord for Name { fn cmp(&self, other: &Self) -> Ordering { (self.flags, &self.name).cmp(&(other.flags, &other.name)) } } impl PartialOrd for Name { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } impl PartialEq for Name { fn eq(&self, other: &Self) -> bool { (self.flags, &self.name) == (other.flags, &other.name) } }

    作者回复: 对!

    2021-10-08
    1
  • Marvichov
    第三题: ``` # 最初 root insert a - k x/24 0x100504080 0x100504080: 0x004044a0 0x00000001 0x00000061 0x00000062 0x100504090: 0x00000063 0x00000064 0x00000065 0x00000066 0x1005040a0: 0x00000067 0x00000068 0x00000069 0x0000006a 0x1005040b0: 0x0000006b 0x00000001 0x00000002 0x00000003 0x1005040c0: 0x00000004 0x00000005 0x00000006 0x00000007 0x1005040d0: 0x00000008 0x00000009 0x0000000a 0x0000000b # insert 第11个 出现新的root x 0x1004044a0 0x1004044a0: 00 00 00 00 00 00 00 00 67 00 00 00 00 00 00 00 ........g....... 0x1004044b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ## 继续 insert 到 z x 0x1004044a0 0x1004044a0: 00 00 00 00 00 00 00 00 67 00 00 00 6e 00 00 00 ........g...n... 0x1004044b0: 5 00 00 00 ff ff ff ff ff ff ff ff ff ff ff ff u...������������ ``` `LeafNode`的capacity是11, 装满了过后就变成6个, 留5个slot以备depth加深 ``` root <g n u> leaf <a-f> <h-m> <o-t> <v-z> ``` 疑问: 1. `LeafNode` 第一个field不是parent嘛? 为啥leaf 1的第一位是 `0x004044a0` 仅仅是32位? 我的机器是64位, 而且之后出现的parent的address是 `0x1004044a0`, 感觉这两个很接近? 不知道这是什么trick 2. 怎么从root node read leaf node的memory呢? root node的parent是null, 没找到怎么access leaf node. 我目前只能从最初的root node猜 (以为之后这个root变成了leaf) 3. 这个field能大概讲一讲big picture嘛? 目前读源码还是有难度... ``` struct InternalNode { data: LeafNode, edges: [MaybeUninit>; 2 * B],} ````

    作者回复: 你可以参考:https://gankra.github.io/blah/rust-btree-case/ 来学习。虽然是比较旧的文章,但 Btreemap 的思想和大方向是一致的。 3. B=6,edges 是个数组,里面有 12 个元素,元素类型是可能未初始化(MaybeUninit)的BoxedNode<K, V>。这个代码开头处有伪代码表述想要达成的目的: // This is an attempt at an implementation following the ideal // // ``` // struct BTreeMap<K, V> { // height: usize, // root: Option<Box<Node<K, V, height>>> // } // // struct Node<K, V, height: usize> { // keys: [K; 2 * B - 1], // vals: [V; 2 * B - 1], // edges: [if height > 0 { Box<Node<K, V, height - 1>> } else { () }; 2 * B], // parent: Option<(NonNull<Node<K, V, height + 1>>, u16)>, // len: u16, // }

    2021-10-05
    1
  • Marvichov
    对文中的例子有些疑问: 我的理解是, ctrl section, byte的postion, 对应着bucket的position `a` 在第一个bucket, 为啥ctrl section的第一个byte不是`0x72` 呢? 和示意图 (`ctrl 表` 那一节) 对不上. ``` (gdb) c Continuing. # 插入第四个元素后,哈希表扩容,堆地址起始位置变为 0x5555555a7b50 - 8*8(0x40) added 4: bucket_mask 0x7, ctrl 0x5555555a7b50, growth_left: 3, items: 4 Breakpoint 1, hashmap2::explain (name=..., map=...) at src/hashmap2.rs:32 32 unsafe { std::mem::transmute(arr) } (gdb) x /20x 0x5555555a7b10 0x5555555a7b10: 0x00000061 0x00000001 0x00000000 0x00000000 0x5555555a7b20: 0x00000064 0x00000004 0x00000063 0x00000003 0x5555555a7b30: 0x00000000 0x00000000 0x00000062 0x00000002 0x5555555a7b40: 0x00000000 0x00000000 0x00000000 0x00000000 0x5555555a7b50: 0xff72ffff 0x0aff6502 0xffffffff 0xffffffff ``` ctrl的start addr是0x5555555a7b50, 第一个byte不应该对应bucket 1吗? 然而`0a` 对应的是第5个bucket?

    作者回复: 注意看图,这句话之后的图,你看看图中前后对比。 > 在移动的过程中,会涉及哈希的重分配。从下图可以看到,‘a’ / ‘c’ 的相对位置和它们的 ctrl byte 没有变化,但重新做 hash 后,‘b’ 的 ctrl byte 和位置都发生了变化。 现在 ctrl bits 的位置是:5 6 7 8 1 2 3 4。所以 0a 对应第一项。

    2021-10-04
    3
    1
  • 阿海
    这一章看的比之前吃力很多,尤其是中间观察内存布局的内容。后面有机会再来仔细琢磨下。 尝试回答下问题吧: 1. Name需要实现 Debug, PartialEq, Eq, PartialOrd, Ord 这几个trait ``` #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] struct Name { pub name: String, pub flags: u32, } ``` 2. 粗略算了下,(15 + 200) * 1200000 / (1024 * 1024)大约需要246M内存。内存利用率不知道要怎么算了。 3. 等有时间再琢磨下

    作者回复: 1. 正确! 2. 需要内存 (200+15+1) * 2M = 432M,实际占用 (200+15) * 114M = 246M,利用率 57%。注意 HashMap bucket 数量是指数增长的。

    2021-10-03
    7
    1
  • Geek_5244fa
    > 如果数据结构的输入和输出能一一对应,那么可以使用列表,如果无法一一对应,那么就需要使用哈希表。 这里说的一一对应,结合图来看,似乎说的是位置的映射。 HashMap 有两个映射关系,一个是 key 到位置的映射,一个到 value 的映射。不同 key 可能映射到相同的位置(冲突),也可以映射到相同的值。

    作者回复: 对!

    2021-10-05
收起评论
显示
设置
留言
18
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部