1.概述

转载:https://mp.weixin.qq.com/s/HVb0g6w37gvn307DrnVtTg

为解决布隆过滤器不能删除元素的问题,布谷鸟过滤器横空出世。

论文《Cuckoo Filter:Better Than Bloom》作者将布谷鸟过滤器和布隆过滤器进行了深入的对比。

相比布谷鸟过滤器而言,布隆过滤器有以下不足:

查询性能弱
空间利用效率低
不支持反向操作(删除)
不支持计数。

查询性能弱 是因为布隆过滤器需要使用多个 hash 函数探测位图中多个不同的位点,这些位点在内存上跨度很大,会导致 CPU 缓存行命中率低。

空间效率低 是因为在相同的误判率下,布谷鸟过滤器的空间利用率要明显高于布隆,空间上大概能节省 40% 多。

不过布隆过滤器并没有要求位图的长度必须是 2 的指数,而布谷鸟过滤器必须有这个要求。从这一点出发,似乎布隆过滤器的空间伸缩性更强一些。

不支持反向删除操作 这个问题着实是击中了布隆过滤器的软肋。

在一个动态的系统里面元素总是不断的来也是不断的走,布隆过滤器就好比是印迹,来过了就会有痕迹,就算走了也无法清理干净。

比如你的系统里本来只留下 1kw 个元素,但是整体上来过了上亿的流水元素。

那布隆过滤器很无奈,它会将这些流失的元素的印迹也会永远存放在那里。

随着时间的流失,这个过滤器会越来越拥挤,直到有一天你发现它的误判率太高了,不得不进行重建。

布谷鸟过滤器在论文里声称自己解决了这个问题,它可以有效支持反向删除操作。而且将它作为一个重要的卖点,诱惑你们放弃布隆过滤器改用布谷鸟过滤器。

但是经过我一段时间的调查研究发现,布谷鸟过滤器并没有它声称的那么美好。它支持的反向删除操作非常鸡肋,以至于你根本没办法使用这个功能。

在向读者具体说明这个问题之前,还是先给读者仔细讲解一下布谷鸟过滤器的原理。

2.布谷鸟哈希

布谷鸟过滤器源于布谷鸟哈希算法,布谷鸟哈希算法源于生活 —— 那个热爱「鸠占鹊巢」的布谷鸟。

布谷鸟喜欢滥交(自由),从来不自己筑巢。它将自己的蛋产在别人的巢里,让别人来帮忙孵化。

待小布谷鸟破壳而出之后,因为布谷鸟的体型相对较大,它又将养母的其它孩子(还是蛋)从巢里挤走 —— 从高空摔下夭折了。

最简单的布谷鸟哈希结构是一维数组结构,会有两个 hash 算法将新来的元素映射到数组的两个位置。

如果两个位置中有一个位置为空,那么就可以将元素直接放进去。但是如果这两个位置都满了,它就不得不「鸠占鹊巢」,随机踢走一个,然后自己霸占了这个位置。

p1 = hash1(x) % l
p2 = hash2(x) % l

不同于布谷鸟的是,布谷鸟哈希算法会帮这些受害者(被挤走的蛋)寻找其它的窝。

因为每一个元素都可以放在两个位置,只要任意一个有空位置,就可以塞进去。

所以这个伤心的被挤走的蛋会看看自己的另一个位置有没有空,如果空了,自己挪过去也就皆大欢喜了。

但是如果这个位置也被别人占了呢?

好,那么它会再来一次「鸠占鹊巢」,将受害者的角色转嫁给别人。然后这个新的受害者还会重复这个过程直到所有的蛋都找到了自己的巢为止。

正如鲁迅的那句名言「占自己的巢,让别人滚蛋去吧!

但是会遇到一个问题,那就是如果数组太拥挤了,连续踢来踢去几百次还没有停下来,这时候会严重影响插入效率。

这时候布谷鸟哈希会设置一个阈值,当连续占巢行为超出了某个阈值,就认为这个数组已经几乎满了。

这时候就需要对它进行扩容,重新放置所有元素。

还会有另一个问题,那就是可能会存在挤兑循环。

比如两个不同的元素,hash 之后的两个位置正好相同,这时候它们一人一个位置没有问题。

但是这时候来了第三个元素,它 hash 之后的位置也和它们一样,很明显,这时候会出现挤兑的循环。

不过让三个不同的元素经过两次 hash 后位置还一样,这样的概率并不是很高,除非你的 hash 算法太挫了。

布谷鸟哈希算法对待这种挤兑循环的态度就是认为数组太拥挤了,需要扩容(实际上并不是这样)

2.1 优化

上面的布谷鸟哈希算法的平均空间利用率并不高,大概只有 50%。到了这个百分比,就会很快出现连续挤兑次数超出阈值。

这样的哈希算法价值并不明显,所以需要对它进行改良。

改良的方案之一 是增加 hash 函数,让每个元素不止有两个巢,而是三个巢、四个巢。这样可以大大降低碰撞的概率,将空间利用率提高到 95%左右。

另一个改良方案 是在数组的每个位置上挂上多个座位。这样即使两个元素被 hash 在了同一个位置,也不必立即「鸠占鹊巢」。

因为这里有多个座位,你可以随意坐一个。除非这多个座位都被占了,才需要进行挤兑。很明显这也会显著降低挤兑次数。

这种方案的空间利用率只有 85%左右,但是查询效率会很高 ,同一个位置上的多个座位在内存空间上是连续的,可以有效利用 CPU 高速缓存。

所以更加高效的方案 是将上面的两个改良方案融合起来,比如使用 4 个 hash 函数,每个位置上放 2 个座位。

这样既可以得到时间效率,又可以得到空间效率。这样的组合甚至可以将空间利用率提到高 99%,这是非常了不起的空间效率。

3.布谷鸟过滤器

布谷鸟过滤器和布谷鸟哈希结构一样,它也是一维数组,但是不同于布谷鸟哈希的是,布谷鸟哈希会存储整个元素,而布谷鸟过滤器中只会存储元素的指纹信息(几个bit,类似于布隆过滤器)。

这里过滤器牺牲了数据的精确性换取了空间效率。正是因为存储的是元素的指纹信息,所以会存在误判率,这点和布隆过滤器如出一辙。

首先布谷鸟过滤器还是只会选用两个 hash 函数,但是每个位置可以放置多个座位。

这两个 hash 函数选择的比较特殊,因为过滤器中只能存储指纹信息。当这个位置上的指纹被挤兑之后,它需要计算出另一个对偶位置。

而计算这个对偶位置是需要元素本身的,我们来回忆一下前面的哈希位置计算公式。

fp = fingerprint(x)
p1 = hash1(x) % l
p2 = hash2(x) % l

我们知道了 p1 和 x 的指纹,是没办法直接计算出 p2 的。

3.1 特殊的 hash 函数

布谷鸟过滤器巧妙的地方就在于设计了一个独特的 hash 函数,使得可以根据 p1 和 元素指纹 直接计算出 p2,而不需要完整的 x 元素。

fp = fingerprint(x)
p1 = hash(x)
p2 = p1 ^ hash(fp)  // 异或

从上面的公式中可以看出,当我们知道 fp 和 p1,就可以直接算出 p2。同样如果我们知道 p2 和 fp,也可以直接算出 p1 —— 对偶性。

p1 = p2 ^ hash(fp)

所以我们根本不需要知道当前的位置是 p1 还是 p2,只需要将当前的位置和 hash(fp) 进行异或计算就可以得到对偶位置。

而且只需要确保 hash(fp) != 0 就可以确保 p1 != p2,如此就不会出现自己踢自己导致死循环的问题。

也许你会问为什么这里的 hash 函数不需要对数组的长度取模呢?

实际上是需要的,但是布谷鸟过滤器强制数组的长度必须是 2 的指数,所以对数组的长度取模等价于取 hash 值的最后 n 位。

在进行异或运算时,忽略掉低 n 位 之外的其它位就行。将计算出来的位置 p 保留低 n 位就是最终的对偶位置。

// l = power(2, 8)
p_ = p & 0xff

3.2 数据结构

简单起见,我们假定指纹占用一个字节,每个位置有 4 个 座位。

type bucket [4]byte  // 一个桶,4个座位
type cuckoo_filter struct {buckets [size]bucket // 一维数组nums int  // 容纳的元素的个数kick_max  // 最大挤兑次数
}

3.3 插入算法

插入需要考虑到最坏的情况,那就是挤兑循环。所以需要设置一个最大的挤兑上限

def insert(x):fp = fingerprint(x)p1 = hash(x)p2 = p1 ^ hash(fp)// 尝试加入第一个位置if !buckets[p1].full():buckets[p1].add(fp)nums++return true// 尝试加入第二个位置if !buckets[p2].full():buckets[p2].add(fp)nums++return true// 随机挤兑一个位置p = rand(p1, p2)c = 0while c < kick_max:// 挤兑old_fp = buckets[p].replace_with(fp)fp = old_fp// 计算对偶位置p = p ^ hash(fp)// 尝试加入对偶位置if !buckets[p].full():buckets[p].add(fp)nums++return truec++return false

3.4 查找算法

查找非常简单,在两个 hash 位置的桶里找一找有没有自己的指纹就 ok 了。

def contains(x):fp = fingerprint(x)p1 = hash(x)p2 = p1 ^ hash(fp)return buckets[p1].contains(fp) || buckets[p2].contains(fp)

3.5 删除算法

删除算法和查找算法差不多,也很简单,在两个桶里把自己的指纹抹去就 ok 了。

def delete(x):fp = fingerprint(x)p1 = hash(x)p2 = p1 ^ hash(fp)ok = buckets[p1].delete(fp) || buckets[p2].delete(fp)if ok:nums--return ok

3.6 一个明显的弱点

so far so good!布谷鸟过滤器看起来很完美啊!

删除功能和获取元素个数的功能都具备,比布隆过滤器强大多了,而且似乎逻辑也非常简单,上面寥寥数行代码就完事了。

如果插入操作返回了 false,那就意味着需要扩容了,这也非常显而易见。

but ! 考虑一下,如果布谷鸟过滤器对同一个元素进行多次连续的插入会怎样?

根据上面的逻辑,毫无疑问,这个元素的指纹会霸占两个位置上的所有座位 —— 8个座位。这 8 个座位上的值都是一样的,都是这个元素的指纹。

如果继续插入,则会立即出现挤兑循环。从 p1 槽挤向 p2 槽,又从 p2 槽挤向 p1 槽。

也许你会想到,能不能在插入之前做一次检查,询问一下过滤器中是否已经存在这个元素了?

这样确实可以解决问题,插入同样的元素也不会出现挤兑循环了。但是删除的时候会出现高概率的误删。

因为不同的元素被 hash 到同一个位置的可能性还是很大的,而且指纹只有一个字节,256 种可能,同一个位置出现相同的指纹可能性也很大。

如果两个元素的 hash 位置相同,指纹相同,那么这个插入检查会认为它们是相等的。

插入 x,检查时会认为包含 y。因为这个检查机制会导致只会存储一份指纹(x 的指纹)。那么删除 y 也等价于删除 x。这就会导致较高的误判率。

论文没有欺骗我们,它也提到了这个问题。(读者不必理解后半句)

图片
这句话明确告诉我们,如果想要让布谷鸟过滤器支持删除操作,那么就必须不能允许插入操作多次插入同一个元素,确保每一个元素不会被插入多次(kb+1)

这里的 k 是指 hash 函数的个数 2,b 是指单个位置上的座位数,这里我们是 4

在现实世界的应用中,确保一个元素不被插入指定的次数那几乎是不可能做到的。如果你觉得可以做到,请思考一下要如何做!

你是不是还得维护一个外部的字典来记录每个元素的插入次数呢?这个外部字典的存储空间怎么办?

因为不能完美的支持删除操作,所以也就无法较为准确地估计内部的元素数量。

3.7 证明

下面我们使用开源的布谷鸟过滤器库来证明一下上面的推论

go get github.com/seiflotfy/cuckoofilter

这个布谷鸟过滤器对每个元素存储的指纹信息为一个字节,同一个位置会有 4 个座位。我们尝试向里面插入 15 次同一个元素。

package mainimport ("fmt""github.com/seiflotfy/cuckoofilter"
)func main() {cf := cuckoo.NewFilter(100000)for i := 0; i < 15; i++ {var ok = cf.Insert([]byte("geeky ogre"))fmt.Println(ok)}
}-------
true
true
true
true
true
true
true
true
false
false
false
false
false
false
false

我们发现插入它最多只能插入 8 次同一个元素。后面每一次返回 false 都会经过上百次的挤兑循环直到触碰了最大挤兑次数。

如果两个位置的 8 个座位 都存储了同一个元素,那么空间浪费也是很严重的,空间效率直接被砍得只剩下 1/8,这样的空间效率根本无法与布隆过滤器抗衡了。

如果不支持删除操作,那么布谷鸟过滤器单纯从空间效率上来说还是有一定的可比性的。这确实比布隆过滤器做的要好一点,但是布谷鸟过滤器这必须的 2 的指数的空间需求又再次让空间效率打了个折扣。

补充:来自CuckooFilter4j 的特别说明

图片

4.相关项目

布谷鸟过滤器论文:Cuckoo Filter: Practically Better Than Bloom

Redis 布谷鸟过滤器模块:

https://github.com/kristoff-it/redis-cuckoofilter

最有影响力的布谷鸟过滤器 C 库:

https://github.com/efficient/cuckoofilter

Java 版布谷鸟过滤器:

https://github.com/efficient/cuckoofilter

【不体系】布谷鸟过滤器相关推荐

  1. 过滤请求绝技,布隆过滤器与布谷鸟过滤器

    欢迎关注方志朋的博客,回复"666"获面试宝典 大家都知道,在计算机中,IO一直是一个瓶颈,很多框架以及技术甚至硬件都是为了降低IO操作而生,今天聊一聊过滤器,先说一个场景: 我们 ...

  2. Redis 之布隆过滤器与布谷鸟过滤器

    欢迎关注方志朋的博客,回复"666"获面试宝典 大家都知道,在计算机中,IO一直是一个瓶颈,很多框架以及技术甚至硬件都是为了降低IO操作而生,今天聊一聊过滤器,先说一个场景: 我们 ...

  3. Redis 过滤请求绝技 — 布隆过滤器与布谷鸟过滤器

    来源:www.cnblogs.com/Courage129/p/14337466.html 大家都知道,在计算机中,IO一直是一个瓶颈,很多框架以及技术甚至硬件都是为了降低IO操作而生,今天聊一聊过滤 ...

  4. 过滤请求绝技 — 布隆过滤器与布谷鸟过滤器

    点击上方"朱小厮的博客",选择"设为星标" 后台回复"书",获取 后台回复"k8s",可领取k8s资料 大家都知道,在计 ...

  5. 为什么我加了过滤器然后就登不进去了_布隆过滤器过时了,未来属于布谷鸟过滤器?...

    为了解决布隆过滤器不能删除元素的问题,布谷鸟过滤器横空出世.论文<Cuckoo Filter:Better Than Bloom>作者将布谷鸟过滤器和布隆过滤器进行了深入的对比.相比布谷鸟 ...

  6. 布隆过滤器与布谷鸟过滤器

    大家都知道,在计算机中,IO一直是一个瓶颈,很多框架以及技术甚至硬件都是为了降低IO操作而生,今天聊一聊过滤器,先说一个场景: 我们业务后端涉及数据库,当请求消息查询某些信息时,可能先检查缓存中是否有 ...

  7. java简单实现布谷鸟过滤器的

    布谷鸟过滤器,一种增强版的布隆过滤器,不同于布隆过滤器的是,存放一段hash的地方会多个位置,用于增加空间率用率,布谷鸟过滤器会有两个hash,异或算法,两个hash能找到相互的位置,用于其中一个被布 ...

  8. Redis中的布隆过滤器与布谷鸟过滤器,你了解多少?

    点击关注公众号,实用技术文章及时了解 大家都知道,在计算机中,IO一直是一个瓶颈,很多框架以及技术甚至硬件都是为了降低IO操作而生,今天聊一聊过滤器,先说一个场景: 我们业务后端涉及数据库,当请求消息 ...

  9. Redis布隆过滤器与布谷鸟过滤器

    -     目录    - 大家都知道,在计算机中,IO一直是一个瓶颈,很多框架以及技术甚至硬件都是为了降低IO操作而生,今天聊一聊过滤器,先说一个场景: 我们业务后端涉及数据库,当请求消息查询某些信 ...

最新文章

  1. 如何开通实时计算 Flink 版?
  2. 从Oracle9i中发送电子邮件
  3. debian 升级linux内核,Debian8升级内核到4.5
  4. 3.6数对 (Python)
  5. Apache Cassandra 开源数据库软件修复高危RCE漏洞
  6. 【深入浅出精华版视频】-刘意day13思维导图整理
  7. html5圆形图片自动旋转,纯CSS3圆形图片鼠标滑过旋转翻盖动画特效
  8. 炉石传说的代码是Java吗,hearthstone: 炉石传说,JAVA模拟器(HearthStone Simulator for Java)...
  9. Grub Rescue恢复
  10. c 实现走迷宫流程图_C语言实现一个走迷宫小游戏(深度优先算法)
  11. 使用ffmpeg转码MP4至m3u8格式并切片,以及自动检测切片m3u8脚本编写
  12. 机器学习笔记 - SimSwap 指南:高保真换脸的高效框架
  13. Python爬虫入门记录1.0:获取网站某板块首页面资讯文章标题
  14. 120年奥运历史数据分析
  15. cad的dwg如何转换成pdf?
  16. 李清照词全集,共49首,有百家评说,附 txt 文本下载,就在本帖的附件中。
  17. Unicode编码范围
  18. java开发自学怎么样_为什么说自学java开发不可取?
  19. ImageIO.write 支持写入的图片格式 Java
  20. 2023美春赛Z题:奥运会的未来

热门文章

  1. 腾讯计划对斗鱼进行私有化?斗鱼盘前涨超10%
  2. iPhone 14系列升级巨大:两款Pro版或将配置4800万像素镜头
  3. 爱奇艺首届“黑客马拉松“落幕 极客变身“大娱乐家”
  4. 得物App回应“倒钩价格被炒至69999元”:已做下架处理
  5. 3个月贵了1.6万!特斯拉Model 3今年已涨价6次
  6. 继淘宝特价版之后 闲鱼已向微信提交小程序申请
  7. 造车大军再添一员!消息称滴滴正式启动造车项目
  8. iPhone 12无线充电模块曝光:AirPower有戏了!
  9. 中国信通院:二季度83款5G手机申请入网 款型数占比已过半
  10. 老外大赞iPhone 12系列:全面屏版iPhone 4 外形史上最佳