bigywater.tech
Rust · 59

HashMap 的哈希与扩容

HashMap 的遍历顺序为什么每次不同?容量是怎么长的?删光了内存还在吗?这篇用多次运行、分配器计数和自定义 Hasher 把默认哈希的随机种子、扩容序列、容量回收和哈希退化都真实跑了一遍。

4/4
同一程序 4 次运行第一个 map 的遍历顺序有几种
10 次
插入 1000 个 u64 过程中 capacity 变化次数(3 → 1792)
×60.7
恒为 0 的哈希在 n 从 1000 到 8000 时耗时的增长倍数

随机种子

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    map0 遍历顺序: bhadfgce
    map1 遍历顺序: dgbhecaf
    map2 遍历顺序: agecfhbd
  同一个程序再运行 4 次,每次第一个 map 的顺序: ['aefgdhbc', 'hbfgcade', 'fbegdcah', 'ghbacfed']

固定种子与哈希值

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    固定种子 map0 顺序: eadgfbhc
    固定种子 map1 顺序: eadgfbhc
    同一个 RandomState 对 "hello" 哈希两次: 6115208009943795217 6115208009943795217
    另一个 RandomState: 6018114963070630449
    DefaultHasher::new() 两次: 16156531084128653017 16156531084128653017

扩容序列

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    插入到第    0 个元素时 capacity =    0  累计 alloc 0 dealloc 0
    插入到第    1 个元素时 capacity =    3  累计 alloc 1 dealloc 0
    插入到第    4 个元素时 capacity =    7  累计 alloc 2 dealloc 1
    插入到第    8 个元素时 capacity =   14  累计 alloc 3 dealloc 2
    插入到第   15 个元素时 capacity =   28  累计 alloc 4 dealloc 3
    插入到第   29 个元素时 capacity =   56  累计 alloc 5 dealloc 4
    插入到第   57 个元素时 capacity =  112  累计 alloc 6 dealloc 5
    插入到第  113 个元素时 capacity =  224  累计 alloc 7 dealloc 6
    插入到第  225 个元素时 capacity =  448  累计 alloc 8 dealloc 7
    插入到第  449 个元素时 capacity =  896  累计 alloc 9 dealloc 8
    插入到第  897 个元素时 capacity = 1792  累计 alloc 10 dealloc 9
    size_of::<HashMap<u64,u64>>() = 48

Vec 的 2 倍扩容见Vec 扩容机制;HashMap 的 capacity 序列(3、7、14、28、……)是实测值,没有去读内部的桶数。

with_capacity 与删除

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    with_capacity(100): cap 112 (alloc 1 次)
    插入 100 个后 cap 112(新增 alloc 0 次,没有再扩容)
    第 101 个后 cap 112(新增 alloc 0 次)
    另一个 map 插 100 个再删 90 个:len 10 cap() 54(删除期间 alloc 0 dealloc 0,内存没有释放)
    shrink_to_fit 后 cap() 14(alloc 1 dealloc 1:换了块更小的内存)
    clear() 之后 len 0 cap() 14(dealloc 0 次)

键的要求与哈希退化

实测 · 本机执行输出
  源码:
      1 | use std::collections::HashMap;
      2 | fn main() { let mut m: HashMap<f64, i32> = HashMap::new(); m.insert(1.0, 1); }
  编译:失败
    error[E0599]: the method `insert` exists for struct `HashMap<f64, i32>`, but its trait bounds were not satisfied
     --> <实验目录>/src/u59e.rs:2:62
      |
    2 | fn main() { let mut m: HashMap<f64, i32> = HashMap::new(); m.insert(1.0, 1); }
      |                                                              ^^^^^^
      |
      = note: the following trait bounds were not satisfied:
              `f64: Eq`
              `f64: Hash`
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0599`.
  编译:成功
  运行(退出码 0):
    n= 1000: 默认哈希     0.045 ms   恒等于 0 的哈希     0.493 ms
    n= 2000: 默认哈希     0.061 ms   恒等于 0 的哈希     1.890 ms
    n= 4000: 默认哈希     0.103 ms   恒等于 0 的哈希     7.521 ms
    n= 8000: 默认哈希     0.215 ms   恒等于 0 的哈希    29.903 ms

BTreeMap 不依赖哈希,见BTreeMap 与 HashMap 对比。

分步演示

HashMap
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。扩容与回收用 #[global_allocator] 计数,耗时用 release
    • 没有验证:SipHash 的具体算法与是否抗 HashDoS(只证明了种子随机)、hashbrown 内部的控制字节与 SIMD 探测、替换哈希器(FxHash、ahash)的性能(需要下载 crate)、表大小到容量的映射在其它版本是否一样
    京ICP备2026067502号-1京公网安备11010502063487号