#hash #hasher #hash-map #no-std

no-std foldhash

一种快速、非加密、最小化DoS抵抗的哈希算法

2个版本

新版本 0.1.1 2024年8月17日
0.1.0 2024年8月12日

276算法

Download history 295/week @ 2024-08-12

每月下载 295

Zlib 许可证

47KB
490

Foldhash

此仓库包含Foldhash,这是一个用Rust实现的快速、非加密、最小化DoS抵抗的哈希算法,适用于计算使用,如哈希表、布隆过滤器、计数草图等。

何时不应该使用Foldhash

  • 你担心人们研究你长时间运行的程序的行为,以逆向工程其内部随机状态,并使用这些知识创建许多碰撞输入,以进行计算复杂性攻击。有关更多详细信息,请参阅“哈希DoS抵抗”部分。

  • 你期望Foldhash在版本或平台之间具有一致的输出,例如用于持久化文件格式或通信协议。

  • 你依赖Foldhash的特性进行任何类型的网络安全。Foldhash不适用于任何加密目的。

Foldhash有两个变体,一个针对速度优化,非常适合哈希表和布隆过滤器等数据结构,另一个针对统计质量优化,非常适合HyperLogLog和MinHash等算法。

Foldhash可以在禁用其默认“std”功能的#![no_std]环境中使用。

性能

我们评估了Foldhash与三个常用的Rust哈希:aHash v0.8.11、fxhash v0.2.1和SipHash-1-3(当时Rust的默认哈希算法)。我们评估了Foldhash提供的两种变体,foldhash-f和foldhash-q,它们分别对应于crate中的foldhash::fast和foldhash::quality。

首先,我们注意到具有随机状态的哈希器会扩大你的HashMap的大小,这可能会或可能不会对你的性能产生重要影响。

std::mem::size_of::<foldhash::HashMap<u32, u32>>() = 40  // (both variants)
std::mem::size_of::<ahash::HashMap<u32, u32>>() = 64
std::mem::size_of::<fxhash::FxHashMap<u32, u32>>() = 32
std::mem::size_of::<std::collections::HashMap<u32, u32>>() = 48

我们在两台机器上测试了运行时性能,一台配备了2023年苹果M2 CPU,另一台配备了2023年英特尔至强铂金8481C服务器CPU,两者都使用稳定的Rust 1.80.1。由于我们的竞争对手之一(aHash)依赖于基于AES指令以实现最佳性能,因此我们在英特尔机器上包含了一个带有和不带有-C目标-cpu=native的基准测试。

我们在各种数据类型上进行了测试,这些数据类型被认为是现实世界中可能要散列的类型/分布的代表性类型,在哈希表键的上下文中

  • u32 - 随机32位无符号整数,
  • u32pair - 随机32位无符号整数的对,
  • u64 - 随机64位无符号整数,
  • u64pair - 随机64位无符号整数的对,
  • u64lobits - 只有最低16位变化的64位无符号整数,
  • u64hibits - 只有最高16位变化的64位无符号整数,
  • ipv4 - std::net::Ipv4Addr,相当于[u8; 4]
  • ipv6 - std::net::Ipv6Addr,相当于[u8; 16]
  • rgba - 随机的(u8, u8, u8, u8)元组,
  • strenglishword - 包含从最常见的10,000个英语单词中均匀抽取的单词的字符串,
  • struuid - 随机UUID,以字符串形式散列,
  • strurl - 包含从10,000个URL语料库中均匀抽取的URL的字符串,
  • strdate - 随机的YYYY-MM-DD日期字符串,
  • accesslog - (u128, u32, chrono::NaiveDate, bool),旨在模拟典型的大型复合类型,在这种情况下是访问日志的(resource_id, user_id, date, success)
  • kilobyte - 随机字节字符串,长度为一千字节,
  • tenkilobyte - 随机字节字符串,长度为十千字节。

我们在以下四个上下文中测试了上述数据类型的散列性能

  • hashonly - 仅散列值所需的时间,
  • lookupmiss - 在包含随机元素的1,000元素哈希表中查找所需的时间,仅采样我们知道不在哈希表中的键,
  • lookuphit - 类似于 lookupmiss,不同之处在于键是从已知存在于哈希表中的键中采样的。
  • setbuild - 从1,000个随机采样的元素(每个元素重复10次,因此总共10,000次插入,其中大约90%是重复的)构建包含1,000个元素的 HashSet 所需的时间。

所有时间都是以每操作预期的平均时间为单位,因此分别是:一次哈希、一次查找或一次插入。完整结果 可以在此处找到。为了总结,我们在此只展示针对 u64strenglishword 的结果,以及在整个基准测试中的观察到的几何平均数和平均排名。

Xeon 8481c

┌────────────────┬────────────┬────────────┬────────────┬─────────┬─────────┬─────────┐
│              avg_rank       ┆       1.582.662.093.704.97 │
│        geometric_mean       ┆       6.217.017.568.7428.70 │
╞════════════════╪════════════╪════════════╪════════════╪═════════╪═════════╪═════════╡
│          distr ┆      bench ┆ foldhash-f ┆ foldhash-q ┆  fxhash ┆   ahash ┆ siphash │
╞════════════════╪════════════╪════════════╪════════════╪═════════╪═════════╪═════════╡
│            u64 ┆   hashonly ┆       0.791.030.671.239.09 │
│            u64 ┆ lookupmiss ┆       2.012.441.732.7312.03 │
│            u64 ┆  lookuphit ┆       3.043.592.643.8412.65 │
│            u64 ┆   setbuild ┆       6.136.524.886.6617.80|            ..................... |
│ strenglishword ┆   hashonly ┆       2.632.983.243.5711.87 │
│ strenglishword ┆ lookupmiss ┆       4.635.034.515.8615.19 │
│ strenglishword ┆  lookuphit ┆       8.629.258.2810.0621.35 │
│ strenglishword ┆   setbuild ┆      14.7715.5718.8615.7235.36 │
└────────────────┴────────────┴────────────┴────────────┴─────────┴─────────┴─────────┘

Xeon 8481c with RUSTFLAGS="-C target-cpu=native"

┌────────────────┬────────────┬────────────┬────────────┬─────────┬─────────┬─────────┐
│              avg_rank       ┆       1.893.122.252.774.97 │ 
│        geometric_mean       ┆       6.006.827.396.9429.49 │ 
╞════════════════╪════════════╪════════════╪════════════╪═════════╪═════════╪═════════╡
│          distr ┆      bench ┆ foldhash-f ┆ foldhash-q ┆  fxhash ┆   ahash ┆ siphash │
╞════════════════╪════════════╪════════════╪════════════╪═════════╪═════════╪═════════╡
│            u64 ┆   hashonly ┆       0.791.010.671.349.24 │
│            u64 ┆ lookupmiss ┆       1.682.121.621.9612.04 │
│            u64 ┆  lookuphit ┆       2.683.192.283.1613.09 │
│            u64 ┆   setbuild ┆       6.166.424.757.0318.88|            ..................... |
│ strenglishword ┆   hashonly ┆       2.602.973.253.0411.58 │
│ strenglishword ┆ lookupmiss ┆       4.414.964.824.7932.31 │
│ strenglishword ┆  lookuphit ┆       8.689.358.468.6321.48 │
│ strenglishword ┆   setbuild ┆      15.0116.3419.3415.3735.22 │
└────────────────┴────────────┴────────────┴────────────┴─────────┴─────────┴─────────┘

Apple M2

┌────────────────┬────────────┬────────────┬────────────┬─────────┬─────────┬─────────┐
│              avg_rank       ┆       1.622.812.023.584.97 │
│        geometric_mean       ┆       4.414.865.395.7121.94 │
╞════════════════╪════════════╪════════════╪════════════╪═════════╪═════════╪═════════╡
│          distr ┆      bench ┆ foldhash-f ┆ foldhash-q ┆  fxhash ┆   ahash ┆ siphash │
╞════════════════╪════════════╪════════════╪════════════╪═════════╪═════════╪═════════╡
│            u64 ┆   hashonly ┆       0.600.700.410.786.61 │
│            u64 ┆ lookupmiss ┆       1.501.611.231.658.28 │
│            u64 ┆  lookuphit ┆       1.782.101.572.258.53 │
│            u64 ┆   setbuild ┆       4.745.193.615.3815.36|            ..................... |
│ strenglishword ┆   hashonly ┆       1.842.131.852.1311.61 │
│ strenglishword ┆ lookupmiss ┆       2.712.962.472.999.27 │
│ strenglishword ┆  lookuphit ┆       7.548.777.838.7718.65 │
│ strenglishword ┆   setbuild ┆      16.6117.0914.8316.5226.42 │
└────────────────┴────────────┴────────────┴────────────┴─────────┴─────────┴─────────┘

从上述基准测试中我们可以看出,对于哈希表性能而言,foldhash-q 提供的额外质量几乎从未真正值得它相对于 foldhash-f 的微小但不可忽视的计算开销。这就是我们为什么将 foldhash::fast 作为默认选择提供的原因,尽管它具有可测量的偏差(也请参阅“质量”部分)。

fxhash在基准测试中对小输入通常表现不错,然而作为哈希函数,它具有结构上的弱点,我们认为不建议将其用作通用哈希函数。例如,在Apple M2上针对 u64hibitslookuphit 基准测试中,foldhash的每次查找需要1.77纳秒,而fxhash需要67.72纳秒(由于所有内容都发生冲突 - 如果使用更大的哈希表,效果会更差)。我们认为foldhash-f在质量与性能之间达到了哈希表的正确平衡,而fxhash则过于接近太阳。

当使用AES指令支持编译时,aHash在中等长度的字符串上比foldhash快,但在几乎所有其他情况下或当AES指令不可用时会慢。

质量

foldhash-f在64位完整输出方面是一个相当强的哈希。然而,像SMHasher3这样的统计测试可以区分它和理想哈希函数在关注单个输入/输出位之间关系的测试中。这样的一个属性是雪崩效应:当使用foldhash-f时,输入中的单个比特改变不会以50%的概率翻转另一个比特,这与它像合适的随机预言机那样行为时应发生的相反。

如上述基准测试所示,在foldhash-f上花费更多精力来提高哈希质量并不会导致更好的哈希表性能。然而,也存在一些哈希函数的使用场景,其中每个(比特的)哈希都是无偏的,并且是输入中所有比特的随机函数,例如在HyperLogLog或MinHash等算法中。

为此,我们还提供了foldhash-q,它只是foldhash-f的后处理版本,以正确地雪崩所有比特。Foldhash-q通过了 SMHasher3 测试套件 没有任何失败。您还可以绘制翻转输入比特时翻转特定输出比特的极端概率(50%是理想的),这可以很好地展示fxhash和foldhash-f如何失败这个雪崩属性,而foldhash-q和SipHash-1-3如何通过。

FxHash Foldhash-f Foldhash-q SipHash-1-3

背景

foldhash这个名字来源于折叠乘法技术。这种方法将两个64位单词压缩成一个64位单词,同时充分混合比特。它通过64 x 64位 -> 128位的乘法来实现,然后使用XOR操作将128位乘积的两个半部分折叠在一起。

let full = (x as u128) * (y as u128);
let lo = full as u64;
let hi = (full >> 64) as u64;
let folded = lo ^ hi;

我们不知道有关于这种操作形式的正式分析,但根据经验,它工作得非常好。为什么它应该有效的一个非正式的直观理解是,乘法可以看作是一个参数的多个平移副本的和,只包括另一个参数中设置了位的那些平移副本,例如,对于乘以4位字 abcdefgh

abcd * efgh =

  abcd    * e
   abcd   * f
    abcd  * g
     abcd * h
--------------- +

请注意,乘积的中间位是许多输入位的一个函数,而最高位和最低位受较少的输入位的影响。通过将上半部分折叠回下半部分,这些影响相互补偿,使得所有输出位都受到大部分输入位的影响。

我们并非发明了折叠乘法,它以前以基本上相同的方式在 aHashwyhashxxhash3 中使用过。该操作也用于 mum-hash,以及可能的其他地方。我们不知道谁最初发明了它,我们能找到的最早参考文献是 Steven Fuerst 在 2012 年 撰写关于它的博客

抗哈希拒绝服务(HashDoS)

折叠乘法有一个相当明显的缺陷:如果一个半部分是零,输出就是零。这使得创建大量的哈希冲突变得容易(甚至可能是偶然的,因为零是哈希的常见输入)。为了应对这个问题,foldhash 中的每个折叠乘法都有以下形式

folded_multiply(input1 ^ secret1, input2 ^ secret2)

在这里,secret1secret2 要么是 foldhash 事先生成的秘密随机数,要么是受此类秘密影响的部分哈希结果。这(加上哈希函数中的其他精心设计)确保了不可能创建一个输入列表,该列表在 foldhash 的每个实例中都发生冲突,并且还通过确保每个哈希表使用不同的种子和因此不同的访问模式,防止了哈希表上的某些访问模式成平方增长。当我们声称 foldhash 是“最小化 DoS-抵抗的”时,我们指的是这两个属性:它做了最小的工作来击败非常简单的攻击。

然而,为了清楚起见,foldhash 不声称能抵御交互式攻击者的哈希拒绝服务(HashDoS)。对于密码学的学生来说,从直接观察哈希输出中推导出秘密值应该是显而易见的,从间接观察哈希(例如通过时间攻击或哈希表迭代)中推导出秘密值也是可行的。一旦攻击者知道了秘密值,他们就可以再次轻松地创建无限多的哈希冲突。

无运行时依赖