所属分类:php教程
下面由go语言教程栏目给大家介绍我实现了一个更全面的 Golang 版本的布谷鸟过滤器,希望对需要的朋友有所帮助!
“判断一个值是否在一个巨大的集合当中”(下文中统称为集合隶属测试),是一种常见的数据处理问题。在以往的经验中,如果允许一定的假阳性率,那么布隆过滤器是首选,而如今我们有了更好的选择:布谷鸟过滤器。
最近的业务需要用到过滤器,搜索了一下发现我们的场景下布谷鸟过滤器性价比更高,要好于布隆过滤器。
为了确定最终的技术选型,我去读了一下原论文,后来确定要用布谷鸟过滤器时发现几乎没有 golang 的全面实现,当前在 GitHub 上的几个高 stars 实现都存在一些缺陷,并没有最大化空间利用率,因此自己参照原论文以及论文的原 C++实现,移植并优化了一版 Golang 的库,细节内容都在下文中。
代码地址在这,欢迎 star 、使用、贡献、debug: github.com/linvon/cuckoo-filter
布谷鸟过滤器在网络上已经有很多的介绍文章了,这里不再做过多的介绍,只提一下要点,用于引出下面的内容
如果想要知道更多的细节,可以参考 原论文,或者查看我的 中文翻译版本
是一种基于布谷鸟哈系算法实现的过滤器,本质上是存储了存储项哈希值的布谷鸟哈希表。
如果你了解布隆过滤器,你应该知道布隆过滤器原理是采用多种哈希方法,将存储项的不同哈希映射到位数组上,查询时通过对这些位进行检查来判断是否存在。
而布谷鸟过滤器是对存储项做哈希,将其哈希值取一定位数存储在数组中,查询时通过判断相等位数的哈希是否在数组中来判断是否存在。
同样都是存储哈希值,本质上都是存储多位哈希,为什么布谷鸟过滤器更优?
一是由于布谷鸟哈希表更加紧凑,因此可以更加节省空间。
二是因为在查询时,布隆过滤器要采用多种哈希函数进行多次哈希,而布谷鸟过滤器只需一次哈希,因此查询效率很高
三是布谷鸟过滤器支持删除,而布隆过滤器不支持删除
优点有了,缺点呢?相比于布隆过滤器
优缺点都列出来了,我们再来总结一下。对于这种集合隶属测试问题,大部分情景都是读多写少的,而重复插入并没有什么意义,布谷鸟过滤器的删除虽然不完美但总好过没有,同时还有更优的查询和存储效率,应该说在绝大多数情况下其都是一个性价比更高的选择。
先说一下布谷鸟过滤器中的概念,过滤器是由很多桶组成的,每个桶存储插入项经过哈希计算后的值,该值只会存储固定位数。
过滤器中有 n 个桶,桶的数量是根据要存储的项的数量计算得来的。通过哈希算法我们可以计算出一个项应该存储在哪个桶中,此外每增加一个哈希算法,就可以对一个项产生一个候选桶,当重复插入时,会把当前存储的项踢到候选桶中去。理论上哈希算法越多,空间利用率越高,但实际测试使用 k=2 个哈希函数就可以达到 98% 的利用率了。
每一个桶会存储多个指纹,这受制于桶的大小,不同的指纹可能映射到同一个桶中。桶越大,空间利用率越高,但同时每次查询扫描同一桶中指纹数越多,因此产生假阳性的概率越高,此时就需要提高存储的指纹位数,用以降低冲突率,维持假阳性率。
在论文中提到了实现布谷鸟过滤器所需的几个参数,主要是
详细阅读论文,在第五章中作者依靠试验数据告诉了我们如何选择最合适的构建参数,我们可以得到如下结论
根据上述的理论依据,得出的相关实验数据有:
这样一来我们便可以确定如何选择参数来构建我们的布谷鸟过滤器了:
首先哈希函数我们使用两个就足够了,这可以达到足够的空间利用率。根据我们需要的假阳性率,我们可以确定使用多大的桶大小,当然 b 的选择并不绝对,即使 r>0.002,你也可以使用 b=4 来启用半排序桶。之后我们可以根据 b 来计算为了达到目标假阳性率,我们需要的 f 的大小,这样所有的过滤器参数就确定了。
将上面的结论与布隆过滤器的每项 $1.44log_2(1/r)$ 对比,可以发现启用半排序时,当 r<0.03 左右,布谷鸟过滤器空间更小,若不启用半排序,则退化到 r<0.003 左右。
哈希算法的优化
虽然我们指定了需要两个哈希算法,但实际实现上我们使用一个哈希算法就足够了,因为在论文中提到了一种备选桶计算方法,第二个哈希值可以由第一个哈希值与该位置存储的指纹异或计算得来。如果你在担心我们还需要分别计算指纹的哈希和位置的哈希,我们可以只用一种算法制作 64 位的哈希,高 32 位用于计算位置,低 32 位用于计算指纹。
为什么半排序桶只能用于 b=4 的情况?
半排序的本质是对每个指纹取其四位,该四位可以表示为一个十六进制,对于 b 个指纹的四位存储就可以表示为 b 个 16 进制数,将其所有可能按顺序排列后,可以通过索引其位置来找到对应的排列,从而获取实际的存储值。
我们可以通过以下函数计算所有的情况种类数
func getNum(base, k, b, f int, cnt *int) { for i := base; i < 1<<f; i++ { if k+1 < b { getNum(i, k+1, b, f, cnt) } else { *cnt++ } }}func getNextPow2(n uint64) uint { n-- n |= n >> 1 n |= n >> 2 n |= n >> 4 n |= n >> 8 n |= n >> 16 n |= n >> 32 n++ return uint(n)}func getNumOfKindAndBit(b, f int) { cnt := 0 getNum(0, 0, b, f, &cnt) fmt.Printf("Num of kinds: %v, Num of needed bits: %v\n", cnt, math.Log2(float64(getNextPow2(uint64(cnt)))))}
在 b=4 时,总共有 3786 种排列,小于 4096,即用 12 位即可存储所有的排列索引,而如果直接存储所有指纹,则需要 4X4=16 位,这样节省了 4 位,即对每一个指纹节省了一位。
可以发现,在 b 为 2 时是否启用半排序需要存储的位数相同,没有意义。如果 b 太大则需要存储的索引也会急剧扩张,会在查询性能上有很大的损耗,因此 b=4 是一个性价比最高的选择。
此外编码存储四位指纹的选择是因为其刚好可以用一个十六进制表示,利于存储
使用半排序时的参数选择
使用半排序时,应保证 $ceil(b(f-1)/8)<ceil(bf/8)$,否则是否使用半排序占用的空间是一样大的
过滤器大小选择
过滤器的桶总大小一定是 2 的指数倍,因此在设定过滤器大小时,尽量满足 $size/α ~=(<) 2^n$,size 即为想要一个过滤器存储的数据量,必要时应选择小一点的过滤器,使用多个过滤器达到目标效果
这部分主要是 Golang 库相关
在翻阅了 Github 上 cuckoofilter 的 golang 实现后,发现已有的实现都存在一些缺点:
因为自己的场景需要更优的空间和自定的假阳性率,因此移植了原论文的 C++ 实现,并做了一些优化,主要包括
支持调节参数
支持半排序桶
压缩空间到紧凑的位数组,按位存储指纹
支持二进制序列化
php入门到就业线上直播课:立即学习
全程直播 + 实战授课 + 边学 + 边练 + 边辅导
以上就是如何实现一个更全面的Golang版本的布谷鸟过滤器的详细内容,更多请关注zzsucai.com其它相关文章!