位图(bitmap)是一种优雅而高效的数据结构,它巧妙地利用了计算机最底层的位运算能力。你可以把它想象成一个巨大的开关阵列,每个开关只有打开和关闭两种状态 —— 这就是位图的本质。每一位都可以独立控制,却又可以通过位运算实现群体操作。
在实际应用中,位图的威力令人惊叹。设想你需要在海量数据中查找重复的数字,传统的哈希表或数组都会占用大量内存。而位图却能巧妙地用一个比特位标记一个数字的出现情况,极大地压缩了存储空间。在处理10亿个不重复的整数时,位图仅需要125MB内存,相比其他数据结构动辄需要几个GB,效率提升显著。
位图的运用也体现在我们日常使用的数据库系统中。数据库会用位图索引来加速查询,尤其是对于性别、状态这样的枚举字段,一个位图就能快速定位满足条件的记录。比如在电商系统中,快速筛选出"在售且有库存"的商品,位图索引可以通过简单的位与运算瞬间得出结果。
在大规模系统的权限控制中,位图也显示出其独特魅力。用户的各项权限可以编码到不同的位上,判断权限时只需一条位运算指令,既高效又直观。比如一个CMS系统,可以用一个32位的整数表示用户的全部权限状态,包括读、写、管理等多个维度。
布隆过滤器更是位图思想的精妙应用。它用多个哈希函数在位图上标记数据,能够以极小的内存代价判断一个元素是否可能存在。这在网页爬虫、垃圾邮件过滤等场景下广泛应用。虽然可能有小概率的误判,但在实际应用中往往是可以接受的权衡。
正是由于以上特点,位图在处理海量数据、状态标记、数据压缩、快速统计等场景中表现出色。它用最简单的方式解决了最复杂的问题,这正是计算机科学之美的体现。
BitVec
和 BitMap
类似,只是关注点有些不同。BitVec更像是位操作的抽象数据类型,它强调的是向量化的位运算操作。比如在Rust语言中, bitvec 提供了一系列方便的接口来进行位操作。而Bitmap则更强调其作为"图"的特性,通常用固定大小的位数组来表示集合中元素的存在性。
BitVec 具有以下的优势:
- 空间效率高 - 每个比特位只占用1位(bit)空间,可以表示0或1两种状态
- 快速的位运算 - 支持AND、OR、XOR等位运算操作,性能很高,甚至可以利用 SIMD 加速
- 随机访问快 - 可以O(1)时间定位到任意位置的比特位
- 紧凑存储 - 一个字节(byte)可以存储8个比特位的信息
- 内存占用小 - 对于数据量大但状态简单的场景很节省内存
Go 内部实现的 BitVec
在 Go 运行时的内部, cmd/compile/internal/bitvec 实现了一个位向量数据结构 BitVec
,在 ssa 活跃性分析中使用(bvecSet 封装了 BitVec)。在 runtime/stack.go 中实现了 bitvector
并在内存管理中使用。
我们重点看 BitVec
, 它的方法比较全。
BitVec 的结构体定义如下:
|
|
然后定义了一批位操作的方法:
- func (dst BitVec) And(src1, src2 BitVec) :对两个位向量进行与操作,结果放入到 dst 位向量中
- func (dst BitVec) AndNot(src1, src2 BitVec)
- func (bv BitVec) Clear()
- func (dst BitVec) Copy(src BitVec)
- func (bv BitVec) Count() int
- func (bv1 BitVec) Eq(bv2 BitVec) bool
- func (bv BitVec) Get(i int32) bool
- func (bv BitVec) IsEmpty() bool
- func (bv BitVec) Next(i int32) int32
- func (bv BitVec) Not()
- func (dst BitVec) Or(src1, src2 BitVec)
- func (bv BitVec) Set(i int32)
- func (bv BitVec) String() string
- func (bv BitVec) Unset(i int32)
这里可以看到 Go 内部实现也有一些"不规范"的方法,这些 Receiver 的名字不一致,叫做了 dst、bv、bv 1 三种名称,看起来是有深意的。dst 代表操作最后存储的位向量。不过 bv 1 就有点说不过去了,虽然也能理解,为了和参数中的 bv 2 保持一致。
我们可以挑几个方法看它的实现。
比如 And
方法:
|
|
就是求两个位向量的交集,这里用到了位运算 &
。逐个元素进行位与操作,然后存储到 dst 中。
可以看到如果使用SIMD指令,这里的性能会有很大的提升。
再比如Not
方法:
|
|
这里是对位向量取反,用到了位运算 ^
。然后对最后一个元素进行了特殊处理,清除了多余的位。
这里这一句bv.B[len(bv.B)-1] &= 1<<uint(bv.N%wordBits) - 1
可能难以理解,其实是为了清除最后一个元素中多余的位,这里的 1<<uint(bv.N%wordBits) - 1
就是一个掩码,用来清除多余的位。
再比如Count
方法:
|
|
这里是统计位向量中 1 的个数,用到了 bits.OnesCount32
方法,这个方法是一个快速计算Uint32中bit为1的个数的方法。
这里的实现都是比较简单的,但是在实际应用中,位向量的操作是非常高效的,可以用来解决很多问题。
如果你的项目中有这种需求,比如你要实现一个布隆过滤器/布谷鸟过滤器,或者你要实现一个高效的权限控制系统,那么位向量是一个非常好的选择。