布谷鸟过滤器:实际上优于布隆过滤器

Feb 10, 2021

布谷鸟过滤器:实际上优于布隆过滤器

本文译自原论文:https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf

摘要

在许多网络系统中,布隆过滤器用于高速数据集的隶属测试。它允许一小部分假阳性结果,但具有很好的空间效率。但是它不允许从集合中删除项,而此前试图扩展“标准”布隆过滤器以支持删除的尝试都会增大空间或降低性能。

我们提出了一种新的数据结构,称为布谷鸟过滤器,它可以代替布隆过滤器进行集合近似隶属度测试。 布谷鸟过滤器支持动态添加和删除项,同时实现比布隆过滤器更高的性能。对于存储了许多条目且设置较低假阳性率的应用程序,布谷鸟过滤器比空间优化过的布隆过滤器具有更低的空间开销。我们的实验结果还表明,布谷鸟过滤器在时间和空间上的性能都优于此前那些扩展了布隆过滤器以支持删除的数据结构。

1. 导言

许多数据库、缓存、路由器和存储系统使用近似集合隶属测试来决定给定的项是否在一个集合(通常是很大的)中,并伴随有一些小的假阳性概率。这个测试最广泛使用的数据结构是布隆过滤器[3],由于其内存效率而被广泛研究。布隆过滤器已经被用来:减少概率路由表中所需的空间[25];加速最长前缀匹配的IP地址[9];改进网络状态管理和监控[24, 4];在数据包中编码多播转发信息[15];以及其他许多应用[6]。

标准布隆过滤器的一个限制是,如果不重建整个过滤器(或者可能引入通常不期望出现的假阴性),就无法删除现有的项。有些方法扩展了标准的布隆过滤器以支持删除,但具有很大的空间或性能开销。计数布隆过滤器[12]已被建议用于多种应用[24, 25, 9],但它们通常使用3-4倍的空间来保持与空间优化的布隆过滤器相同的假阳性率。 其他变种包括d-left计数布隆过滤器[5],仍然是1.5倍的大小,商过滤器[2]显著降低了查找性能,从而提供了与布隆过滤器可相比的空间开销。

本文表明,与标准布隆过滤器相比,支持删除的近似集合隶属测试不需要在空间或性能上施加更高的开销。 我们提出了布谷鸟过滤器,一种实用的数据结构,提供了四个主要优点。

  1. 它支持动态添加和删除项;

  2. 它提供了比传统的布隆过滤器更高的查找性能,即使当其接近满载(例如,95%的空间已被使用);

  3. 它比诸如商过滤器等替代品更容易实现;

  4. 在许多实际应用中,如果目标假阳性率ε小于3%,则它使用的空间小于布隆过滤器。

布谷鸟过滤器是布谷鸟哈希表的一个紧凑变体[21],对于插入的每个项,只存储其指纹——使用散列函数从项生成的位字符串,而不是键值对。过滤器中充满了指纹(例如,95%的条目都是指纹),这使得空间效率很高。 项x的隶属查询只需搜索x的指纹的哈希表,如果找到相同的指纹,则返回true。

在构造布谷鸟过滤器时,其指纹大小由目标假阳性率ϵ决定。较小的ϵ值需要较长的指纹才能拒绝更多的假阳性查询。 有趣的是,虽然我们表明对于许多实际工作,布谷鸟过滤器实际上比布隆过滤器更好,但它们是会逐渐变糟糕的:布谷鸟过滤器中使用的最小指纹大小随着表中条目的数量呈对数增长(正如我们在章节4中所解释的那样)。 因此对于较大的表,每个插入项的空间开销更高,但这种额外空间的使用会降低假阳性率。对于拥有几十亿或更少插入项的实际问题,当ϵ<3%时,布谷鸟过滤器在支持删除的同时使用的空间小于不可删除的、空间优化的布隆过滤器。布谷鸟过滤器与常规哈希表有很大的不同,因为只有指纹存储在过滤器中,每个项的原始密钥和值位不再可检索。由于没有存储完整的键,布谷鸟过滤器甚至无法执行标准的布谷鸟散列来插入新项,这涉及到根据其哈希值移动现有键。这种差异意味着适用于布谷鸟散列的标准技术、分析和优化不一定适用于布谷鸟过滤器。

表1

fifilter type space cost cache misses per lookup deletion support
Bloom 1 k no
blocked Bloom 1x 1 no
counting Bloom 3x ∼ 4x k yes
d-left counting Bloom 1.5x ∼ 2x d yes
quotient 1x ∼ 1.2x ≥ 1 yes
cuckoo ≤1x 2 yes

表1:布隆过滤器和变体的特性。假设标准和计数布隆过滤器使用k个哈希函数,而d-left计数布隆过滤器有d个分区。

本文的技术贡献包括

  • 应用标准布谷鸟散列的一种变体-部分键布谷鸟散列来构建支持动态添加和删除项的布谷鸟过滤器(章节3).。
  • 探索为什么部分键布谷鸟散列确保高表占用大多数现实世界的应用程序(章节4)。
  • 优化布谷鸟过滤器,以在空间效率方面优于布隆过滤器(章节5)。

2. 背景和相关工作

2.1 布隆过滤器和变体

我们比较了标准的布隆过滤器和包括支持删除或更好的查找性能在内的部分变体,如表1中所总结的。 这些数据结构在章节7中进行了实证评估.。布谷鸟过滤器比这些数据结构拥有更高的空间效率和性能。

标准布隆过滤器[3]提供项集合的紧凑表示,其支持两个操作:插入和查找。一个布隆过滤器允许一个可调的假阳性率ϵ,以便查询返回“绝对不存在”(没有错误)或“可能存在”(有错误的概率)。 ϵ越低,过滤器所需的空间越大。

一个布隆过滤器由k个散列函数和一个位数组组成,所有位最初设置为“0”。 若要插入项,它将此项用k个哈希函数散列到位数组中的k个位置,然后将所有k位设置为“1”。 查找处理类似,读取数组中的k个对应位:如果所有位都被设置了,查询返回true;否则返回false。

布隆过滤器不支持删除。

布隆过滤器可以非常节省空间,但不是最优的[20]。 对于假阳性率ϵ,空间优化的布隆过滤器使用 k = log2 (1/ϵ) 个散列函数。 这样的布隆过滤器可以使用1.44 log2 (1/ϵ) 个bit存储每个项,它只取决于ϵ,而不是项大小或项总数。 信息论最小值需要每项log2 (1/ϵ)位,因此空间优化的布隆过滤器施加高于信息论下限44%的空间开销。

信息论上的最优基本上可以通过使用指纹(长度1/ϵ位)和一个完美的哈希表[6]来实现。 为了有效地处理删除,我们用设计良好的布谷鸟哈希表来替换一个完美的哈希函数。

计数布隆过滤器[12]扩展布隆过滤器以允许删除。 计数布隆过滤器使用计数器数组代替位数组。插入增加k个计数器的值,而不是简单地设置k位,查找时检查每个所需计数器是否为非零。 删除操作则减少这些k个计数器的值。为了防止算术溢出(即增加具有最大可能值的计数器),数组中的每个计数器必须足够大,才能保留布隆过滤器的属性。 在实践中,计数器由四个或更多位组成,因此计数布隆过滤器比标准布隆过滤器需要4倍多空间。 (可以通过引入二次哈希表结构来管理溢出计数器来构造计数布隆过滤器,以减少使用空间,但牺牲是额外的复杂性。)

块布隆过滤器[22]不支持删除,但在查找时提供更好的位置定位。 块布隆过滤器由一个小的布隆过滤器阵列组成,每个过滤器安装在一个CPU缓存行中。 每个项只存储在这些由散列分区确定的小布隆过滤器中的一个。 因此,每个查询加载布隆过滤器最多只会导致一次缓存缺失,这大大提高了性能。 一个缺点是,由于小布隆过滤器阵列的负载不平衡,假阳性率变得更高。

d-left计数布隆过滤器[5]与我们在这里使用的方法相似。 使用d-left散列[19]的哈希表储存物品的指纹。 这些过滤器通过删除它们的指纹来删除项。与计数布隆过滤器相比,它们降低了50%的空间成本,与空间优化的不可删除的布隆过滤器相比,通常需要1.5-2倍的空间。 如我们所表明的,布谷鸟过滤器比d-left计数布隆过滤器拥有更好的空间效率,并且具有其他优点,包括简单性。

商过滤器[2]也是存储指纹以支持删除的紧凑哈希表。 商过滤器使用类似于线性探测的技术来定位指纹,从而提供更好的位置定位。 然而,它们需要额外的元数据来编码每个条目,这需要比同等的标准布隆过滤器多10∼25%的空间。 此外,它的所有操作都必须在获取目标项之前解码一个项表的序列,哈希表填充得越多,这些序列变得越长。 因此,当哈希表的占用率超过75%时,其性能显著下降。

其他变体:无论是在空间上还是在性能上,很多其他变体已经被提出来改进布隆过滤器。排名索引哈希[14]构建线性链式哈希表来存储压缩指纹。 虽然与d-left计数布隆过滤器相似,而且空间效率更高,但更新内部索引以降低链的成本消耗是非常耗时的,因此在动态设置中不那么吸引人。 Putze等人提出了布隆过滤器的两种变体[22]。 一个是前面讨论过的块布隆过滤器;另一个称为Golomb压缩序列的过滤器将所有项的指纹存储在一个排序列表中。 它的空间接近最优,但数据结构是静态的,需要非恒定的查找时间来解码被编码的序列。 因此,本文没有拿他与其他过滤器比较。 Pagh等人基于Cleary[8]提出了一种渐近空间最优数据结构[20]。 然而,这种数据结构比其替代方案要复杂得多,似乎不适合高性能的实现。 相反,布谷鸟过滤器易于实现。

2.2 布谷鸟哈希表

图1

布谷鸟过滤器:实际上优于布隆过滤器_第1张图片

图1:布谷鸟散列的图解

布谷鸟哈希基础:一个基本的布谷鸟哈希表[21]由一个桶数组组成,每个插入项都有由散列函数h1(x)和h2(x)确定的两个候选桶。 查找过程会检查两个桶是否任意一个桶包含此项。 图1(a)显示将新项 x 插入到8个桶的哈希表中的示例,其中 x 可以放置在桶2或6中。 如果x的两个桶中的任何一个是空的,则算法将x插入到该空桶中,插入完成。如果两个桶都没有空间,如例所示,项选择一个候选桶 (例如桶 6), 踢出去现有的项 (在本例中为“a”) 并将此被踢出项重新插入到它的备用位置。 在我们的例子中,重新放置“a”触发了另一个重置,将现有的项“c”从桶4踢到桶1。这个过程可能会重复,直到找到一个空桶,如图1(b)所示或直到达到最大位移次数(例如在我们的实现中为500次)。如果没有找到空桶则认为此哈希表太满,无法插入。 虽然布谷鸟散列可能执行一系列重置,但其均摊插入时间为O(1)。

布谷鸟散列确保了高空间占用率,因为它改进了在插入新项时对先前项放置决策。 布谷鸟散列的大多数实际实现是通过使用包含多个项的桶来扩展上面的基本描述,如[10]中所建议的那样.。在假设所有散列函数都是完全随机的情况下,使用k个哈希函数和大小为b的桶时的最大可能负载已经被分析过了[13]。通过配置适当的布谷鸟哈希表参数(在章节5中探索),其95%的表空间可以被填充。

使用布谷鸟散列进行集合隶属测试:最近,标准布谷鸟哈希表已被用于在一些应用程序中提供集合隶属信息。 为了支持事务性内存,Sanchez等人建议将每个事务的读/写内存地址集存储在布谷鸟哈希表中,并在填满时将此表转换为布隆过滤器[23]。 他们的设计使用了标准的布谷鸟哈希表,因此比布谷鸟过滤器需要更多的空间。 我们以前在建立高速和高内存利用率的键值存储[17, 11]和基于软件的以太网交换机[26]时都应用布谷鸟哈希表作为内部数据结构。 这项工作的动机是通过一种称为部分键布谷鸟散列的优化来提高哈希表的性能。 然而正如我们在本文中所显示的,这种技术也使得一种新的方法能够构建一个布隆过滤器替代品,这是以前没有研究过的。 因此本文还应用了部分键布谷鸟散列,但更重要的是,它提供了一个深入的分析,使用这种技术专门服务于集合隶属测试(而不是键值查询),并进一步比较了布谷鸟过滤器与集合隶属数据结构替代品的性能。

布谷鸟过滤器的挑战:为了使布谷鸟过滤器具有很高的空间效率,我们使用多方式关联的布谷鸟哈希表来提供高速查找和高表使用率(例如,填充95%的哈希表插槽);为了进一步减少哈希表的大小,每个项首先被散列到一个恒定大小的指纹中,然后插入到这个哈希表中。应用这种数据结构的挑战是重新设计插入过程,并仔细配置哈希表,以尽量减少每个项的空间使用:

  • 首先,只在哈希表中存储指纹而不是使用标准的布谷鸟散列方法插入项。因为在布谷鸟散列中,插入算法必须能够将现有指纹重新定位到它们的替代位置。 一个空间效率低但简单的解决方案是将每个插入的项全部存储到它的条目中(可能是表外部);提供原始项(“键”),计算其备用位置很容易。相反,布谷鸟过滤器使用部分键布谷鸟散列来寻找一个项的替代位置,只基于它的指纹(章节3).

  • 其次,布谷鸟过滤器将每个项与哈希表中的多个可能位置关联。 这种项存储位置的灵活性提高了表的使用率,但是对于每次查找探测可能位置时, 保持相同的假阳性率需要更多的空间来存储更长的指纹。 在章节5中我们展示了我们对优化表占用与其大小之间的平衡相关分析,以最小化每个项的平均空间成本。

3. 布谷鸟过滤器算法

在本文中,用于我们的布谷鸟过滤器的布谷鸟哈希表的基本单位称为条目。 每个条目存储一个指纹。哈希表由一个桶数组组成,其中一个桶可以有多个条目。

本节描述布谷鸟过滤器如何执行插入、查找和删除操作。章节3.1 展示了部分键布谷鸟散列,一个标准布谷鸟散列的变体,使布谷鸟过滤器可以动态插入新项。 这项技术是在以前的工作中首次引入的[11],但当时的背景是改进存储全键的常规布谷鸟哈希表的查找和插入性能。与此相反,本文重点对仅使用指纹的部分键布谷鸟散列时的空间效率进行了优化和分析,使布谷鸟过滤器与布隆过滤器相比有竞争力,甚至比布隆过滤器更紧凑。

3.1 插入

如前所述,在标准布谷鸟散列中,将新项插入到现有哈希表中需要一些方法来访问原始现有项,以便确定在需要时将它们迁移到哪里,以便为新项腾出空间(章节2.2)。 然而,布谷鸟过滤器只存储指纹,因此没有办法恢复和重新散列原始键以找到它们的替代位置。为了突破这个限制,我们利用一种称为部分键布谷鸟散列的技术来根据其指纹导出一个项的备用位置。 对于项x,我们的散列方案计算两个候选桶的索引如下

式1

h1(x)=hash(x),h2(x)=h1(x)⊕hash(x′sfingerprint)公式(1)中的异或操作确保了一个重要特性:

h1(x)也可以通过h2(x)和同一公式中的指纹来计算。 换句话说,在桶i中迁走一个键(不管i是h1(x)或h2(x)),我们直接用当前桶的索引i和存储在桶中的指纹计算它的备用桶j:

式2

j=i⊕hash(fingerprint)因此,插入只使用表中的信息,而不必检索原始项x。

Algorithm 1: Insert(x)
f = fingerprint(x);
i1 = hash(x);
i2 = i1 ⊕ hash(f);
if bucket[i1] or bucket[i2] has an empty entry then
	add f to that bucket;
	return Done;
// must relocate existing items;
i = randomly pick i1 or i2;
for n = 0; n < MaxNumKicks; n++ do
	randomly select an entry e from bucket[i];
	swap f and the fingerprint stored in entry e;
	i = i ⊕ hash(f);
	if bucket[i] has an empty entry then
		add f to bucket[i];
		return Done;
// Hashtable is considered full;
return Failure;

Copy

此外,指纹在与当前桶的索引进行异或之前会进行散列,以帮助在表中均匀地分配项。 如果备用位置用“i⊕指纹”计算时不将指纹进行散列,且指纹的大小与表的大小相比很小,那么从邻近的桶中被踢出的项会落在表中的相邻近的位置。 例如,使用8位指纹,从桶中踢出的项i将被放置到离桶i最多256个桶的桶中,因为异或操作将改变桶索引的八个低阶位,而高阶位不会改变。散列指纹可以确保这些项可以重新定位到哈希表的完全不同部分的桶中,从而减少哈希冲突并提高表的利用率。

利用部分键布谷鸟散列,布谷鸟过滤器通过算法1中所示的过程动态地添加新项,因为这些指纹可以明显短于 h1 或 h2 的尺寸,这有两个后果。 一,通过公式 (1)计算的(h1,h2)不同可能选择的总数,可以比标准的布谷鸟散列中使用完美的散列来导出h1和h2小得多。 这个可能会造成更多的碰撞,尤其是以前对布谷鸟散列的分析(如[10, 13])不足以涵盖。 对部分键布谷鸟散列的完整分析仍然保持开放(远不止本文);在章节4中我们提供了一个这个问题的详细讨论,并考虑如何为实际工作实现高利用率。

第二,插入两个有相同指纹的不同项x和y是没问题的;在桶中多次出现相同的指纹是可能的。然而,布谷鸟过滤器不适合插入相同项超过2b次(b是桶大小)的应用程序,因为此重复项的两个桶将变得过载。 这种情况有几种解决办法。 首先,如果表不需要支持删除,那么这个问题就不会出现,因为每个指纹只有一份必须存储。其次,您可以在一定的空间成本下,将计数器与桶关联起来,并适当地增加/减少它们。 最后,如果原始密钥存储在某个地方(可能是在较慢的外部存储中),如果表已经包含(假阳性)桶和指纹的匹配条目,则以减慢插入为代价,可以查阅该记录以完全防止重复插入。类似的要求适用于传统的和d-left计数布隆过滤器。

Algorithm 2: Lookup(x)
f = fingerprint(x);
i1 = hash(x);
i2 = i1 ⊕ hash(f);
if bucket[i1] or bucket[i2] has f then
	return True;
return False; 

Copy

3.2 查找

布谷鸟过滤器的查找过程很简单,如算法2所示。给定一个项x,算法首先根据公式 (1)计算x的指纹和两个候选桶。然后读取这两个桶:如果两个桶中的任何现有指纹匹配,则布谷鸟过滤器返回true,否则过滤器返回false。请注意,只要不发生桶溢出,就可以确保没有假阴性。

Algorithm 3: Delete(x)
f = fingerprint(x);
i1 = hash(x);
i2 = i1 ⊕ hash(f);
if bucket[i1] or bucket[i2] has f then
	remove a copy of f from this bucket;
	return True;
return False;

Copy

3.3 删除

标准布隆过滤器不能删除,因此删除单个项需要重建整个过滤器,而计数布隆过滤器需要更多的空间。布谷鸟过滤器就像计数布隆过滤器,可以通过从哈希表删除相应的指纹删除插入的项。其他具有类似删除过程的过滤器比布谷鸟过滤器更复杂。例如,d-left计数布隆过滤器必须使用额外的计数器,以防止指纹碰撞1的“假删除”问题,商过滤器必须移动指纹序列去填补删除之后的“空”条目并维持其“桶结构”2。

算法3 中给出的布谷鸟过滤器的删除过程更简单。 它检查给定项的两个候选桶;如果任何桶中的指纹匹配,则从该桶中删除匹配指纹的一份副本。

删除不需要在删除项后清除条目。 它还避免了两个项共享一个候选桶并具有相同指纹时的“错误删除”问题。 例如,如果项x和y都驻留在桶i1中并在指纹f上碰撞,部分键布谷鸟散列确保它们也可以驻留在桶i2中,因为i2=i1⊕hash(f)。 删除x时,进程是否删除插入x或y时添加的f的副本并不重要。 x删除后, y仍被视为集合成员,因为在任何一个桶i1和i2中都留下了相应的指纹。这样做的一个重要结果是,删除后过滤器的假阳性行为保持不变。(在上面的示例中,表中的y会导致查找x时出现假阳性,根据定义:它们共享相同的桶和指纹。)这是近似集合隶属数据结构的预期假阳性行为,其概率仍以ϵ为界。

请注意,要安全地删除项x,必须事先插入它。 否则,删除非插入项可能会无意中删除碰巧共享相同指纹的真实、不同的项。 这一要求也适用于所有其他支持删除的过滤器。

4. 渐近行为

在这里,我们表明,在布谷鸟过滤器中使用部分键布谷鸟散列存储指纹,会让指纹大小的下限随着过滤器大小而缓慢增长。这与其他方法(例如指纹和静态布隆过滤器的完美散列,前面讨论过)形成了对比,其中指纹大小仅取决于所需的假阳性概率。 虽然这似乎是负面的,但在实践中这个影响似乎并不重要。 从经验上讲,当每个桶持有6位或更大的四个指纹时,包含多达40亿个项的过滤器可以有效地填充其哈希表95%的负载。

在本部分和下一节中,用于我们分析的符号是:

符号 含义
ϵ 目标假阳性率
f 指纹长度(位)
α 负载因子 (0 ≤ α ≤ 1)
b 每个桶的条目数
m 桶的数量
n 项数
C 每个项的平均位

最小指纹大小:我们提出的部分键布谷鸟散列法基于给定项的当前位置和指纹导出其备用存储桶(章节3)。因此每个项的候选存储桶不是独立的。例如,假设一个项可以放置在桶i1或i2中。根据式 (1),当使用f位指纹时,i2的可能值的数目最多为 2f 。使用1字节指纹,给定i1,i2最多只有2f=256个不同的可能值。对于m个桶的表,当2f

直观地说,如果指纹足够长,部分键布谷鸟散列仍然可以很好地近似于标准布谷鸟散列;然而,如果哈希表非常大,并且存储相对较短的指纹,则由于哈希碰撞而导致插入失败的机会将增加,这可能会降低表的占用率。 这种情况可能出现在将布谷鸟过滤器用于项数很大,但只设定中等低的假阳性率情况。 在下面,我们确定性地解析了插入失败概率的下界。

让我们首先推导给定一个含有q个项的集合在同一个桶中碰撞的概率。假设第一个项x有它的第一个桶 i1和一个指纹 tx。如果其他q-1个项和这个项x有相同的两个桶,它们必须3:

(1) 具有相同的指纹tx,发生概率为1/2f;

(2)第一个存储桶为i1或i1⊕h(tx),发生概率为2/m。

因此,这些q个项共享相同两个存储桶的概率为(2/m·1/2f)q−1。

现在考虑一个构造过程,该过程将n个随机项插入到 m=cn 个桶的空表中(对于常数c和常数桶大小 b)。每当有q=2b+1个项映射到相同的两个桶时,插入就失败了。这个概率为失败提供了一个下限(而且我们相信,这个构造过程的失败概率占主导地位,虽然我们没有证明这一点,也不需要为了获得一个下界而去证明)。由于n个项中总共有(n2b+1)个不同的2b+1个项的可能集合,因此在构造过程中发生碰撞的2b+1个项的组的预期数量为

式3

(n2b+1)(22f·m)2b=(n2b+1)(22f·cn)=Ω(n4bf)

我们得出结论4bf必须是Ω(n)以避免非微小的失败概率,否则这种期望是Ω(1)的。因此,指纹的大小必须是f=Ω(logn/b)位。

这一结果似乎有点不走运,因为指纹所需的位数量是Ω(logn);请记住,布隆过滤器每个项使用一个常数(大约ϵln(1/ϵ)位。 因此我们可能会担心这种方法的可伸缩性。然而正如我们接下来所展示的,布谷鸟过滤器的实际应用是通过下界分母中的b因子来存储的:只要我们使用合理大小的桶,指纹大小就可以保持较小。

图2

布谷鸟过滤器:实际上优于布隆过滤器_第2张图片

图2:通过使用f位指纹实现的部分键布谷鸟散列,在不同大小的表中(m=215, 220, 225 和230 桶)的负载因子α。
通过使用两个完全独立的哈希函数,短指纹足以接近最佳负载因子 α=0表示空,1表示完全满。每个点是10次独立运行中看到的最小负载因子。

经验评估:图2 显示了部分键布谷鸟散列实现的负载因子,我们改变了表m中的指纹大小f、桶大小b和桶数量。在实验中,我们将指纹大小f从1位变化到20位。随机64位键被插入到空过滤器中,直到一次插入重新定位现有指纹超过500次,然后找到一个空条目(我们的“满”条件),然后我们停止并测量当前的负载因子α。 我们对m=215, 220, 225和230个桶的过滤器进行了十次实验,并测量了他们在十个试验中的最小负荷。 由于我们测试机的内存约束,我们没有使用更大的表。

如图2所示,在不同的配置下,有b=4的过滤器可以填充到95%的使用率,而b=8时使用足够长的指纹可以填充到98%。 在此之后,增加指纹大小在提高负载因子方面几乎没有回报(当然,它降低了假阳性率)。 正如该理论所建议的,当过滤器变大时,实现接近最优利用率所需的最小f增加。 此外,比较图2(a)还有图2(b),当桶大小b变大时,高利用率的最小f减小,这也是理论的建议。 总的来说,短指纹似乎足以满足现实大小的项集。 即使b=4且m=230,因此哈希表可以包含多达40亿个项,一旦指纹超过六个比特,α接近实验中使用两个完全独立的散列函数得到的“最佳负载因子”。

洞察:在公式 (3)中导出的指纹大小下界,以及图2中所示的经验结果给出了对于布谷鸟过滤器的重要见解。理论上布谷鸟过滤器的空间成本比布隆过滤器“更差”—Ω(logn)相比于常数—在这种设置中,常数项是非常重要的。 对于布隆过滤器,实现ϵ=1%需要每个项大约10位,而不管是否存储1000、100万或10亿个项。 相反,布谷鸟过滤器需要更长的指纹来保持其哈希表相同的高空间效率,但相应地实现了较低的假阳性率。如理论所预测的,如果桶大小b足够大,每个指纹的Ω(logn)位则增长较缓慢。 我们发现,出于实际目的,它可以被视为一个合理大小的常量来实现。图2表明布谷鸟过滤器针对数十亿个项,6位指纹足以确保哈希表的利用率非常高。

作为一个探索,部分键布谷鸟散列是非常有效的,正如我们在章节7中进一步展示的。然而,关于这一技术的几个理论问题仍有待今后的研究,包括证明插入一个新项的成本的界限和研究该项需要多少独立的哈希函数。

5. 空间优化

在章节3中介绍的布谷鸟过滤器操作插入、查找和删除的基本算法独立于哈希表配置(如,每个桶有多少个条目)。然而选择合适的布谷鸟过滤器参数会显著影响空间效率。本节重点通过参数选择和附加机制优化布谷鸟过滤器的空间效率。

空间效率是通过一个完整的过滤器中表示每个项的平均位数来衡量的,由表大小除以过滤器可以有效存储的项总数得出。 回想一下,虽然哈希表的每个条目存储一个指纹,但并不是所有的条目都被占用:对于布谷鸟过滤器,表中必须有一些松弛,否则插入项时会出现故障。 因此每个项实际地存储成本比指纹本身更高:如果每个指纹为f位,哈希表具有α的负载因子,则每个项的均摊空间成本C为

式4

ααC=table sizeavg of items=f·(avg of entries)α·(avg of entries)=fα  bits

正如我们将展示的,f和α都与桶的大小b有关。下面的部分研究如何(近似)最小化式 (4)。通过选择最佳桶大小b,给出目标假阳性率ϵ。

5.1 最佳桶尺寸

保持布谷鸟过滤器的总大小不变,但改变桶的大小会导致两个后果:

(1) 较大的桶可以提高表的利用率(即b越大α越大) ,使用k=2个哈希函数时,当桶大小b=1(即直接映射哈希表)时,负载因子α为50%,但使用桶大小b=2、4或8时则分别会增加到84%、95%和98%。

(2) 较大的桶需要较长的指纹才能保持相同的假阳性率(即b越大f越大)。 使用较大的桶时,每次查找都会检查更多的条目,从而有更大的概率产生指纹冲突。在查找不存在项的最坏情况下,查询必须探测两个桶,其中每个桶可以有b个项。 (虽然并非所有的桶都能装满,我们在这里分析其最坏的情况;这给了我们对于一个95%满的表合理准确的估计。) 在每个条目中,一次查询能与存储的指纹匹配并返回假阳性成功匹配的概率最大为1/2f 。 做了2b次这样的匹配后,命中假指纹的概率上限为

式5

1−(1−1/2f)2b≈2b/2f

这与桶的大小b成正比。 为了保留目标假阳性率ϵ,过滤器确保ϵ2b/2f≤ϵ ,因此需要的最小的指纹大小大约是:

式6

ϵϵf≥log2(2b/ϵ)=log2(1/ϵ)+log2(2b)  bits

空间成本的上限 如我们所展示的,f和α都取决于桶大小b。 由公式(4)得平均空间成本C受下列约束:

式7

ϵαC≤[log2(1/ϵ)+log2(2b)]/α

其中α随b增加。 例如当b=4时,1/α ≈1.05,公式 (7)结果表明,布谷鸟过滤器渐近优于(通过一个常数因子)每项需要ϵ1.44log2(1/ϵ) 个位的布隆过滤器。

表2

  bits per item load factor α avg. # memory references / lookup(positive query|negative query)
space-optimized Bloom fifilter 1.44 log~2~ (1/ϵ) - log~2~(1/ϵ)|2
cuckoo fifilter (log2 (1/ϵ) + 3)/α 95.5% 2|2
cuckoo fifilter w/ semi-sort (log2 (1/ϵ) + 2)/α 95.5% 2|2

表2:布隆过滤器和三个布谷鸟过滤器的空间和查找成本

图3

布谷鸟过滤器:实际上优于布隆过滤器_第3张图片

图3:每个项的均摊空间成本与测量的假阳性率,不同桶大小b=2,4,8。
每个点为10次运行的平均值

最优桶大小b 为了比较使用不同桶大小b的空间效率,我们进行了实验,首先用不同指纹大小的部分键布谷鸟散列构造布谷鸟哈希表,并测量了每个项的均摊空间成本及其实现的假阳性率。 作为如图3所示空间最优桶大小取决于目标假阳性率ϵ:当ϵ>0.002时。每桶有两个条目比每桶使用四个条目产生的结果略好;当ϵ减小到0.00001<ϵ≤0.002时,每个桶四个条目可以最小化空间。

总之,我们选择(2,4)-布谷鸟过滤器(即每个项有两个候选桶,每个桶最多有四个指纹)作为默认配置,因为它达到了大多数实际应用[6]可能需要的最佳或接近最佳的空间效率的假阳性率。 在下面,我们提出了一种技术,通过编码每个桶,进一步节省了b=4的布谷鸟过滤器的空间。

5.2 半排序桶以节省空间

本小节描述了一种针对于每个桶有b=4个条目的布谷鸟过滤器的技术,可以每项节省一位的存储空间。这种优化是基于这样一个事实,即桶内指纹的顺序不会影响查询结果。 基于这一观察,我们可以压缩每个桶,首先对其指纹进行排序,然后对排序后的指纹序列进行编码。 该方案类似于[5]中使用的“半排序桶”优化。

下面的示例说明了压缩如何节省空间。 假设每个桶包含b=4个指纹,每个指纹为f=4位(更多的情况将在稍后讨论)。 一个未压缩的桶占4x4=16位。 然而,如果我们对存储在这个桶中的所有四个4位指纹进行排序(空项被视为存储值为"0"的指纹),那么总共只有3876个可能的结果(包含替换位置的唯一组合数量)。如果我们预先计算并将这些3876个可能的桶值存储在一个额外的表中,并将原始桶替换为一个到这个预先计算的表中的索引,那么每个原始桶可以用12位索引(212=4096>3876)表示,而不是 16 比特, 对每个指纹可以节省1比特4。

请注意,这种基于置换的编码(即索引所有可能的结果)需要额外对表编码/解码并且每个查找都变为间接查找。 因此,为了获得高的查找性能,必须使编码/解码表足够小以适合使用缓存。 因此我们的“半排序”优化只应用于具有四个条目的表。此外,当指纹大于四位时,每个指纹只有四个有效比特被编码,其余的会直接且独立的存储。

6. 与布隆过滤器的比较

图4

布谷鸟过滤器:实际上优于布隆过滤器_第4张图片

图4:假阳性率与每个元素的空间成本。对于低假阳性率(<3%),布谷鸟过滤器比空间优化的布隆过滤器每个元素需要更少的比特。
计算布谷鸟过滤器空间开销使用的负载因子是基于经验的。

我们使用表2中所示的度量以及几个额外的因素来比较布隆过滤器和布谷鸟过滤器。

空间效率:表2比较了空间优化的布隆过滤器和有与没有半排序的布谷鸟过滤器。图4进一步显示了这些方案存储一个项所需的位,当ϵ从0.001%变化至10%时。 信息论下限中每个项需要ϵlog2(1/ϵ)位,一个最佳的布隆过滤器使用ϵ1.44log2(1/ϵ)位,有44%的开销。 因此当ϵ<3%时有半排序的布谷鸟过滤器比布隆过滤器空间效率更高。

内存访问次数:对于具有k个哈希函数的布隆过滤器,一次阳性查询必须从位数组中读取k位。对于空间优化的布隆过滤器则需要ϵk=log2(1/ϵ),随着ϵ越来越小,阳性查询必须探测更多的位,并且在读取每个位时可能会导致更多的缓存行缺失。 例如,当ϵ=25%时,k=2;但ϵ=1%时,k=7,这是实际中更常见的情况。对空间优化的布隆过滤器的阴性查询在返回之前平均读取两位,因为有一半的位是被设置过的。 任何对布谷鸟过滤器的查询,无论是阳性还是阴性,总是读取固定数量的桶,这样(最多)只会有两个缓存行缺失。

值关联:布谷鸟过滤器可以扩展为同时为每个匹配指纹返回一个关联值(存储在过滤器外部)。 布谷鸟过滤器的这一特性提供了一种近似表查找机制,它为每个现有项平均返回1+ϵ个值(由于假阳性命中,它可以匹配多个指纹),并为每个不存在的项返回平均ϵ个值。标准布隆过滤器不提供此功能。 虽然像Bloomier过滤器这样的变体可以扩展布隆过滤器来实现这类功能,但这些结构比布谷鸟过滤器更复杂,需要更多的空间[7].

最大容量:布谷鸟过滤器有一个负载阈值。 在达到最大可行负载因子后,插入不再稳定,并且越来越有可能失败,因此哈希表必须扩容才能存储更多的项。 相反,您可以继续将新项插入到布隆过滤器中,不过是以增加假阳性率为代价。 为了保持相同的目标假阳性率,布隆过滤器也必须扩容。

重复限制:如果布谷鸟过滤器支持删除,则必须存储同一项的多个副本。 插入同一项kb+1次将导致插入失败。 这类似于计数布隆过滤器,其中重复插入会导致计数器溢出。 然而将相同的项多次插入布隆过滤器或不可删除的布谷鸟过滤器是没有效果的。

7. 评估

我们实现的标准布谷鸟过滤器5有大约500行C++代码,以及另外500行用于支持在章节5.2中提到的“半排序”优化。在下面,我们将标准布谷鸟过滤器表示为“CF”,半排序布谷鸟过滤器表示为“ss-CF”。除了布谷鸟过滤器,我们实现了其他四个过滤器用于比较:

  • 标准布隆过滤器(BF)[3]:我们评估了标准的布隆过滤器作为基准。 在我们所有的实验中,会根据过滤器的大小和项的总数来配置哈希函数k的数量以达到最低的假阳性率。此外还应用了性能优化来通过减少散列来加速查找和插入[16]。 每次插入或查找只需要两个散列函数h1(x)和h2(x),然后使用这两个散列来模拟额外的k-2个哈希基于以下格式:

    式8

    gi(x)=h1(x)+i·h2(x)

  • 块布隆过滤器(blk-BF)[22]:每个过滤器由一个块数组组成,每个块是一个小的布隆过滤器。每个块的大小是64字节,以匹配我们测试机中的CPU缓存行。对于每个小型布隆过滤器,我们还应用了与标准布隆过滤器相同的模拟多个哈希函数的优化。

  • 商过滤器(QF)[2]:我们评估了我们自己实现商过滤器6。 此过滤器存储每个项的三个额外位作为元数据,以帮助定位项。 由于商过滤器的性能随着负载的增加而下降,我们在[2]的评估中设置了最大负载因子为90%.

  • d-left计数布隆过滤器(dl-CBF)[5]:过滤器配置为具有d=4个哈希表。所有的哈希表有相同数量的桶;每个桶有四个条目。

我们强调标准和块布隆过滤器不支持删除,因此它们可以作为基线进行比较。

实验设置:所有要插入的项都是从随机数生成器中预先生成的64位整数。我们没有消除重复项,因为重复的概率很小。

在每个查询上,所有过滤器首先使用CityHash[1]生成项的64位散列。 然后,每个过滤器根据需要在这个散列中划分这64位。 例如,布隆过滤器将高32位和低32位分别视为前两个独立散列,然后使用这两个32位值计算其他k-2个散列。计算64位哈希的时间(在我们的测试中,每个项大约20ns)也包含在我们的测试中。 所有实验都使用一台带有两个IntelXeon处理器([email protected] 12MBL3缓存)和32GB DRAM的机器。

度量:为了充分理解不同的过滤器如何在功能、空间和性能方面实现权衡,我们通过以下度量来比较上述过滤器:

  • 空间效率:由过滤器大小(以位为单位)除以完整过滤器包含的项数来测量。
  • 实现的假阳性率:通过查询具有不存在项的过滤器和计统计阳性返回值数量的分数来测量。
  • 构造率:用一个全过滤器包含的项数除以从空构建这个全过滤器的时间来衡量。
  • 查找、插入和删除吞吐量:由过滤器每秒可以执行的平均操作数来衡量。 值可以取决于过滤器的工作负载和占用率。

表3

metrics CF ss-CF BF blk-BF QF dl-CBF
# of items (million) 127.78 128.04 123.89 123.89 120.80 103.82
bits per item 12.60 12.58 13.00 13.00 13.33 15.51
false positive rate 0.19% 0.09% 0.19% 0.43% 0.18% 0.76%
constr. speed (million keys/sec) 5.00 3.13 3.91 7.64 1.91 4.78

表3:空间效率和构建速度。所有过滤器均为192MB。黑体字是行中最好的。

7.1 达到目标假阳性率

我们首先评估空间效率和假阳性率。 在每次运行中,所有过滤器都被配置为具有相同的大小(192MB)。 布隆过滤器被配置为使用九个散列函数,这将在每项占用13位是最小化假阳性率。对于布谷鸟过滤器,它们的哈希表有m=225个桶,每个由四个12位条目组成。d-left计数布隆过滤器具有相同数量的哈希表条目,但是分位d=4个分区。商过滤器也有227个条目,其中每个条目存储3位元数据和9位的其余部分。

每个过滤器最初是空的,持续放置项直到过滤器要么看到插入失败(对于CF和dl-CBF),要么达到目标容量限制(对于BF、BLK-BF和QF)。 不同过滤器的构建速率和假阳性率见表3。

在所有过滤器中,ss-CF达到最低的假阳性率。使用大约相同数量的空间(12.60位/项),启用半排序可以节省一个位,将其编码到每个项的指纹中,从而将假阳性率从0.19%降低到0.09%。另一方面,在访问每个桶时,半排序需要编码和解码,从而使构造速率从5.00降低到3.13百万项每秒。

BF和blk-BF都使用13.00位每项,k=9个哈希函数的配置,但块过滤器的假阳性率比BF高2倍,比最佳CF高4倍。 这种差异是因为blk-BF通过散列将每个项分配给单个块,并且不平衡的映射将创建“热”块,该块要服务更多的项,而“冷”块则很少使用。 不幸的是,即使使用强哈希函数,这种不平衡的赋值也会发生在块之间[18]这增加了总体假阳性率。另一方面,通过在单个缓存行中操作任何查询,blk-BF实现了最高的构造速率。

QF每个项花费的比特比BFs和CFs更多,实现了第二佳的假阳性率。由于每个桶的编码和解码成本,其建造效率最低。

最后,dl-CBF在当整个表约78%满时出现插入失败并停止构建,因此存储的项更少。它实现的假阳性率比其他过滤器差得多,因为每个查找必须检查16个条目,因此哈希冲突的可能性更高。

7.2 查找性能

图5

布谷鸟过滤器:实际上优于布隆过滤器_第5张图片

图5:当过滤器达到其容量时的查找性能。 每点为10次运行的平均值。

不同的工作负载 我们接下来测试在过滤器被填充后的查找性能。本节将比较不同工作负载下的查找吞吐量和延迟。工作负载的是由阳性查询(即表中存在项)和阴性查询(即表中不存在项)的分数来表现的,这会影响查找速度。我们将输入工作负载中阳性查询的分数p从0%(所有查询都是阴性的)变化到100%(所有查询都是阳性的)。

查找吞吐量的测试结果如图5所示。每个过滤器占用192MB,远远大于我们测试机中的L3缓存(20MB)。当所有查询都为阴性时,blk-BF表现良好,因为每次查找都可以在获取第一个“0”位后立即返回。然而当更多的查询是阳性时,它的性能就会下降,因为它必须读取额外的位作为查找的一部分。当p增加时,BF的吞吐量变化相似,但大约要慢4MOPS。 这是因为BF在完成一次查找中可能会出现多个缓存缺失,而块的版本总是可以在一个缓存行中操作,并且每次查找最多有一次缓存缺失。

相反,CF总是取两个桶7,从而在查询为100%阳性和100%阴性时达到相同的高性能。 当p=50%时,性能略有下降,因为CPU的分支预测此时最不准确(匹配或不匹配的概率正好是1/2)。 在半排序的情况下,当阳性查询的分数增加时,CF的吞吐量展现出相似的趋势,但是由于读每个桶时额外的解码开销其吞吐要更低一点。作为性能惩罚的回报,半排序版本与标准布谷鸟过滤器相比,假阳性率降低了两倍。但是,当阳性查询超过25%时,ss-CF仍然优于BF。

QF在所有过滤器中表现最差。 当QF为90%满时,查找必须搜索一长串表条目并为了目标项对每一个进行解码。

dl-CBF的性能优于ss-CF,但比BF慢30%。在服务所有都为阴性查询和所有都为阳性查询时,它也能保持大约相同的性能,因为其在每次查找中只搜索恒定数量的条目。

图6

布谷鸟过滤器:实际上优于布隆过滤器_第6张图片

图6:在不同的占用率下查找吞吐量(MOPS)。 每点为10次运行的平均值。

不同的利用率 在本实验中,我们测量当这些过滤器在不同的利用率时的查找吞吐量。我们将每个过滤器的负载因子α从0(空)变化到最大占用率。图6展示了当所有查询都为阴性(即,对于不存在的项)和所有查询都为阳性(即现有项)时的平均瞬时查找吞吐量。

在阴性查询和阳性查询中,CF和ss-CF的吞吐量在不同的负载因子级别上都是最稳定的。 这是因为要读取和比较的条目总数保持不变,即使插入了更多的项。

相反,当负载更多时,QF的吞吐量大大降低。 随着负载因子的增加,该筛选器搜索目标项的条目链越来越长。

当服务阴性查询和阳性查询时,BF和blk-BF的行为都表现出不同。 对于阳性查询,无论插入了多少项,它们必须始终检查总计k个位,从而提供恒定的查找吞吐量;而对于阴性查询,当过滤器加载较少时,设置的位较少,当看到“0”时,查找可以更早返回。

dl-CBF的行为与BF不同。 当所有查找都为负值时,它确保了与CF一样的恒定吞吐量,因为无论这个过滤器包含多少项,都必须搜索来自四个桶的总共16个条目。 对于阳性查询,如果插入的项较少,在检查所有四个桶之前,查找可能会更早返回;但是,在dl-CBF大约20%满之后,这种差异变得可以忽略不计。

7.3 插入性能

图7

布谷鸟过滤器:实际上优于布隆过滤器_第7张图片

图7:在不同的占用率下的插入吞吐量。 插入随机键,直到每个数据结构达到其设计容量。 每点为10次运行的平均值。

根据一个完整过滤器包含的项总数和插入这些项的总时间来测量的总体构建速度如表3所示. 我们还研究了整个构建过程中的瞬时插入吞吐量。也就是说,我们测量不同过滤器在不同负载因子级别时的插入吞吐量,如图7所示。与图6中所示的查找吞吐量相反,当它们被填充的越多时,两种类型的CF都降低了插入吞吐量(尽管它们的总体构建速度仍然很高),而BF和blk-BF都保证了几乎恒定的插入吞吐量。在成功插入新项之前,CF可能要递归地移动现有键序列,当负载因子增加时,此过程变得更耗时。相反,两个布隆过滤器总是只需要设置k个位,而不考虑负载因子。

QF也降低了插入吞吐量,因为它必须在插入新项之前移动一个项序列,并且当表更满时,这个序列会增加的更长。

dl-CBF保持恒定的吞吐量。 对于每个插入,它必须在最多四个桶中找到一个空条目;如果找不到这样的条目,则插入停止,而不像布谷鸟散列中那样重新定位现有项。这也是为什么它的最大负载因子不超过80%。

7.4 删除性能

图8

布谷鸟过滤器:实际上优于布隆过滤器_第8张图片

图8:在不同的占用率下删除至空的吞吐量(MOPS)。 每点为10次运行的平均值。

图8比较了支持删除的过滤器之间的删除性能。 实验从最初的满过滤器中删除键,直到它为空。CF达到了最高的吞吐量。CF和ss-CF在整个过程中都提供了稳定的性能。在所有过滤器中,dl-CBF的性能第二好。当接近满时,QF是最慢的,但当接近空时,QF比ss-CF更快。

评估摘要:CF确保了不同工作负载和不同占用水平下的高稳定查找性能。它的插入吞吐量随着过滤器的填充而下降,但总体构建速率仍然是比其他过滤器更快,除了blk-BF。使用半-排序使布谷鸟过滤器比空间优化的BFs更具有空间效率。它虽然让查找、插入和删除慢了一点, 但是仍然比传统的BFs更快。

8. 结论

布谷鸟过滤器是一种新的近似集隶属查询数据结构,可以用于以前使用布隆过滤器解决的许多网络问题。布谷鸟过滤器通过三种方式改进了布隆过滤器:

(1)支持动态删除项;(2)更好的查找性能;(3)对于需要低假阳性率的应用程序(ϵ<3%)有更好的空间利用率

布谷鸟过滤器基于布谷鸟散列来存储项集的指纹。一个进一步的关键贡献,我们已经应用了部分键布谷鸟散列,通过只允许基于存储的指纹重新定位,这使得布谷鸟过滤器大大提高了效率。 我们的配置探索表明,布谷鸟过滤器使用大小为4的桶,将在广泛的应用中表现良好,同时布谷鸟过滤器参数可以很容易地根据应用程序进行调整。

我们认为进一步扩展和优化布谷鸟过滤器是可能的,并将进一步推进它们的使用,上述的数据结构是一个快速和高效的构建块,已经很好地适应了网络和分布式系统的实际需求。

9. 感谢

这项工作得到了英特尔通过英特尔云计算科学技术中心(ISTC-CC)和国家科学基金会提供的资金的支持,这些捐赠是CCF-0964474、CNS-1040801、CCF-1320231、CNS-1228598和IIS-0964473。 我们感谢CoNEXT评审员的反馈和建议;RasmusPagh讨论分析的布谷鸟过滤器的空间使用,这产出了许多关于渐近行为的见解;HyeontaekLim的代码优化;IulianMoraru,AndrewPavlo,PeterSteenkiste的写作建议。

你可能感兴趣的:(布谷鸟过滤器:实际上优于布隆过滤器)