目录
一些进阶解释" >一些进阶解释
首页 后端开发 Golang 如何实现一个更全面的Golang版本的布谷鸟过滤器

如何实现一个更全面的Golang版本的布谷鸟过滤器

Mar 11, 2021 am 11:23 AM
golang

下面由go语言教程栏目给大家介绍我实现了一个更全面的 Golang 版本的布谷鸟过滤器,希望对需要的朋友有所帮助!

“判断一个值是否在一个巨大的集合当中”(下文中统称为集合隶属测试),是一种常见的数据处理问题。在以往的经验中,如果允许一定的假阳性率,那么布隆过滤器是首选,而如今我们有了更好的选择:布谷鸟过滤器。
最近的业务需要用到过滤器,搜索了一下发现我们的场景下布谷鸟过滤器性价比更高,要好于布隆过滤器。
为了确定最终的技术选型,我去读了一下原论文,后来确定要用布谷鸟过滤器时发现几乎没有 golang 的全面实现,当前在 GitHub 上的几个高 stars 实现都存在一些缺陷,并没有最大化空间利用率,因此自己参照原论文以及论文的原 C++实现,移植并优化了一版 Golang 的库,细节内容都在下文中。
代码地址在这,欢迎 star 、使用、贡献、debug: github.com/linvon/cuckoo-filter

布谷鸟过滤器

布谷鸟过滤器在网络上已经有很多的介绍文章了,这里不再做过多的介绍,只提一下要点,用于引出下面的内容

如果想要知道更多的细节,可以参考 原论文,或者查看我的 中文翻译版本

什么是布谷鸟过滤器?

是一种基于布谷鸟哈系算法实现的过滤器,本质上是存储了存储项哈希值的布谷鸟哈希表。

如果你了解布隆过滤器,你应该知道布隆过滤器原理是采用多种哈希方法,将存储项的不同哈希映射到位数组上,查询时通过对这些位进行检查来判断是否存在。

而布谷鸟过滤器是对存储项做哈希,将其哈希值取一定位数存储在数组中,查询时通过判断相等位数的哈希是否在数组中来判断是否存在。

为什么选择布谷鸟过滤器?

同样都是存储哈希值,本质上都是存储多位哈希,为什么布谷鸟过滤器更优?

  • 一是由于布谷鸟哈希表更加紧凑,因此可以更加节省空间。

  • 二是因为在查询时,布隆过滤器要采用多种哈希函数进行多次哈希,而布谷鸟过滤器只需一次哈希,因此查询效率很高

  • 三是布谷鸟过滤器支持删除,而布隆过滤器不支持删除

优点有了,缺点呢?相比于布隆过滤器

  • 布谷鸟过滤器采用一种备用候选桶的方案,候选桶与首选桶可以通过位置和存储值互相异或得出,这种对应关系要求桶的大小必须是 2 的指数倍
  • 布隆过滤器插入时计算好哈希直接写入位即可,而布谷鸟过滤器在计算后可能会出现当前位置上已经存储了指纹,这时候就需要将已存储的项踢到候选桶,随着桶越来越满,产生冲突的可能性越来越大,插入耗时越来越高,因此其插入性能相比布隆过滤器很差
  • 插入重复元素:布隆过滤器在插入重复元素时并没有影响,只是对已存在的位再置一遍。而布谷鸟过滤器对已存在的值会做踢出操作,因此重复元素的插入存在上限
  • 布谷鸟过滤器的删除并不完美:有上述重复插入的限制,删除时也会出现相关的问题:删除仅在相同哈希值被插入一次时是完美的,如果元素没有插入便进行删除,可能会出现误删除,这和假阳性率的原因相同;如果元素插入了多次,那么每次删除仅会删除一个值,你需要知道元素插入了多少次才能删除干净,或者循环运行删除直到删除失败为止

优缺点都列出来了,我们再来总结一下。对于这种集合隶属测试问题,大部分情景都是读多写少的,而重复插入并没有什么意义,布谷鸟过滤器的删除虽然不完美但总好过没有,同时还有更优的查询和存储效率,应该说在绝大多数情况下其都是一个性价比更高的选择。

实战指南

细节实现

先说一下布谷鸟过滤器中的概念,过滤器是由很多桶组成的,每个桶存储插入项经过哈希计算后的值,该值只会存储固定位数。

过滤器中有 n 个桶,桶的数量是根据要存储的项的数量计算得来的。通过哈希算法我们可以计算出一个项应该存储在哪个桶中,此外每增加一个哈希算法,就可以对一个项产生一个候选桶,当重复插入时,会把当前存储的项踢到候选桶中去。理论上哈希算法越多,空间利用率越高,但实际测试使用 k=2 个哈希函数就可以达到 98% 的利用率了。

每一个桶会存储多个指纹,这受制于桶的大小,不同的指纹可能映射到同一个桶中。桶越大,空间利用率越高,但同时每次查询扫描同一桶中指纹数越多,因此产生假阳性的概率越高,此时就需要提高存储的指纹位数,用以降低冲突率,维持假阳性率。

在论文中提到了实现布谷鸟过滤器所需的几个参数,主要是

  • 哈希函数个数(k):哈希个数,取 2 就足够
  • 桶大小(b):每个桶存储多少个指纹
  • 指纹大小(f):每个指纹存储键的哈希值的多少位

详细阅读论文,在第五章中作者依靠试验数据告诉了我们如何选择最合适的构建参数,我们可以得到如下结论

  • 过滤器无法 100% 填满,存在最大负载因子 α,那么均摊到每个项上的存储占用空间就是 f/α
  • 当保持过滤器总大小不变时,桶越大负载因子越高,即空间利用率越高,但每个桶存储的指纹越多,查询时可能发生冲突的概率也越高,为了维持假阳性率不变,桶越大,就需要越大的指纹

根据上述的理论依据,得出的相关实验数据有:

  • 使用 k=2 个哈希函数时,当桶大小 b=1(即直接映射哈希表)时,负载因子 α 为 50%,但使用桶大小 b=2、4 或 8 时则分别会增加到 84%、95% 和 98%
  • 为了保证假阳性率 r,需要保证 $2b/2^f\leq r$ ,那么指纹 f 大小约为 $f ≥ log_2(2b/r)=log_2(1/r) + log_2(2b)$ ,那每个项的均摊成本即为 $C ≤ [log_2(1/r) + log_2(2b)]/α$
  • 实验数据表明,当 r>0.002 时。每桶有两个条目比每桶使用四个条目产生的结果略好;当 r 减小到 0.00001
  • 如果使用半排序桶,可以对每一个存储项减少 1bit 的存储空间,但其仅作用于 b=4 的过滤器

这样一来我们便可以确定如何选择参数来构建我们的布谷鸟过滤器了

首先哈希函数我们使用两个就足够了,这可以达到足够的空间利用率。根据我们需要的假阳性率,我们可以确定使用多大的桶大小,当然 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<> 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)f/8)$,否则是否使用半排序占用的空间是一样大的

过滤器大小选择

过滤器的桶总大小一定是 2 的指数倍,因此在设定过滤器大小时,尽量满足 $size/α ~=(<) 2^n$,size 即为想要一个过滤器存储的数据量,必要时应选择小一点的过滤器,使用多个过滤器达到目标效果

Golang 实现

这部分主要是 Golang 库相关

在翻阅了 Github 上 cuckoofilter 的 golang 实现后,发现已有的实现都存在一些缺点:

  • 绝大部分的库都是固定 b 和 f 的,即假阳性率也是固定的,适应性不好
  • 所有的库 f 都是以字节为单位的,只能以 8 的倍数来调整,不方便调整假阳性率
  • 所有的库都没有实现半排序桶,相比于布隆过滤器的优势大打折扣

因为自己的场景需要更优的空间和自定的假阳性率,因此移植了原论文的 C++ 实现,并做了一些优化,主要包括

  • 支持调节参数

  • 支持半排序桶

  • 压缩空间到紧凑的位数组,按位存储指纹

  • 支持二进制序列化

以上是如何实现一个更全面的Golang版本的布谷鸟过滤器的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

如何使用 Golang 安全地读取和写入文件? 如何使用 Golang 安全地读取和写入文件? Jun 06, 2024 pm 05:14 PM

在Go中安全地读取和写入文件至关重要。指南包括:检查文件权限使用defer关闭文件验证文件路径使用上下文超时遵循这些准则可确保数据的安全性和应用程序的健壮性。

如何为 Golang 数据库连接配置连接池? 如何为 Golang 数据库连接配置连接池? Jun 06, 2024 am 11:21 AM

如何为Go数据库连接配置连接池?使用database/sql包中的DB类型创建数据库连接;设置MaxOpenConns以控制最大并发连接数;设置MaxIdleConns以设定最大空闲连接数;设置ConnMaxLifetime以控制连接的最大生命周期。

如何在 Golang 中将 JSON 数据保存到数据库中? 如何在 Golang 中将 JSON 数据保存到数据库中? Jun 06, 2024 am 11:24 AM

可以通过使用gjson库或json.Unmarshal函数将JSON数据保存到MySQL数据库中。gjson库提供了方便的方法来解析JSON字段,而json.Unmarshal函数需要一个目标类型指针来解组JSON数据。这两种方法都需要准备SQL语句和执行插入操作来将数据持久化到数据库中。

Golang框架与Go框架:内部架构与外部特性对比 Golang框架与Go框架:内部架构与外部特性对比 Jun 06, 2024 pm 12:37 PM

GoLang框架与Go框架的区别体现在内部架构和外部特性上。GoLang框架基于Go标准库,扩展其功能,而Go框架由独立库组成,实现特定目的。GoLang框架更灵活,Go框架更容易上手。GoLang框架在性能上稍有优势,Go框架的可扩展性更高。案例:gin-gonic(Go框架)用于构建RESTAPI,而Echo(GoLang框架)用于构建Web应用程序。

从前端转型后端开发,学习Java还是Golang更有前景? 从前端转型后端开发,学习Java还是Golang更有前景? Apr 02, 2025 am 09:12 AM

后端学习路径:从前端转型到后端的探索之旅作为一名从前端开发转型的后端初学者,你已经有了nodejs的基础,...

如何找出 Golang 正则表达式匹配的第一个子字符串? 如何找出 Golang 正则表达式匹配的第一个子字符串? Jun 06, 2024 am 10:51 AM

FindStringSubmatch函数可找出正则表达式匹配的第一个子字符串:该函数返回包含匹配子字符串的切片,第一个元素为整个匹配字符串,后续元素为各个子字符串。代码示例:regexp.FindStringSubmatch(text,pattern)返回匹配子字符串的切片。实战案例:可用于匹配电子邮件地址中的域名,例如:email:="user@example.com",pattern:=@([^\s]+)$获取域名match[1]。

golang框架开发实战教程:常见疑问解答 golang框架开发实战教程:常见疑问解答 Jun 06, 2024 am 11:02 AM

Go框架开发常见问题解答:框架选择:取决于应用需求和开发者偏好,如Gin(API)、Echo(可扩展)、Beego(ORM)、Iris(性能)。安装和使用:使用gomod命令安装,导入框架并使用。数据库交互:使用ORM库,如gorm,建立数据库连接和操作。身份验证和授权:使用会话管理和身份验证中间件,如gin-contrib/sessions。实战案例:使用Gin框架构建一个简单的博客API,提供POST、GET等功能。

如何用 Golang 使用预定义时区? 如何用 Golang 使用预定义时区? Jun 06, 2024 pm 01:02 PM

Go语言中使用预定义时区包括以下步骤:导入"time"包。通过LoadLocation函数加载特定时区。在创建Time对象、解析时间字符串等操作中使用已加载的时区,进行日期和时间转换。使用不同时区的日期进行比较,以说明预定义时区功能的应用。

See all articles