浅析Go语言bitset的实现原理

 更新时间:2023年08月08日 11:32:55   作者:Go学堂  
bitset包是一个将非负整数映射到布尔值的位的集合,这篇文章主要通过开源包bitset来为大家分析一下位集合的设计和实现,感兴趣的可以学习一下

一、bitset简介

1.1、主要功能

bitset包是一个将非负整数映射到布尔值的位的集合。比如我们有一个64位的二进制序列,要将第N位设置成true,对应的就是将第N位置成1。如下:

该包因为使用的是位操作,所以比使用map[uint]bool来实现非负整数到布尔值的映射会更高效。

该包不仅提供了setting、clearing、flipping和testing的方法。还提供了集合的交集、并集、差集等方法。

1.2、github上的基础属性

项目地址: https://github.com/bits-and-blooms/bitset 

1.3、谁在用

二、设计与实现

在了解了bitset的基本功能之后,我们来分析bitset的设计和实现。

2.1 数据结构

在bitset包中,核心的数据结构是BitSet。其定义如下:

// A BitSet is a set of bits. The zero value of a BitSet is an empty set of length 0.
type BitSet struct {
	length uint
	set    []uint64
}

set字段为什么是一个切片?

首先来看为什么使用uint64的数据类型。bitset不是按位存储的集合吗,怎么set的数据类型是uint64呢?

这里就涉及到计算机的一个基础知识点:

计算机存储和处理的信息都是以二值信号表示的。所谓的二值信号就是0和1,也就是我们常说的二进制。

所以,整数的底层也是二进制位。uint64在go语言中就代表的是用64个二进制位表示的整数值。

在bitset中,我们先假设set字段只有一个uint64的整数。那么,如果我们想将第7位设置成1,那么就如下:

但是,一个uint64的整数最多也就只有64个二进制位。那如果我们想设置第100位为true,那又该怎么表示呢? 这也就是set字段的类型为什么是一个切片的原因了。既然一个uint64最多只能表示64个二进制位,那么我就用多个uint64不就能表示更多的二进制位了吗。

所以,set中第一个uint64表示前64个二进制位,第二个uint64表示65到128的二进制位,以此类推。这样就理论上就可以表示任意位数的二进制位了。

2.2 length字段代表的是什么的长度

length字段表示在初始化一个BitSet对象时,该BitSet对象总共能容纳多少位,根据这个总位数来分配set字段的切片长度。如下:

// New creates a new BitSet with a hint that length bits will be required
func New(length uint) (bset *BitSet) {
	defer func() {
		if r := recover(); r != nil {
			bset = &BitSet{
				0,
				make([]uint64, 0),
			}
		}
	}()
	bset = &BitSet{
		length,
		make([]uint64, wordsNeeded(length)),
	}
	return bset
}

看代码的第12到15行。在第14行中,需要计算的是要表示length个二进制位需要几个uint64的非负整数来表示。这里通过wordsNeeded函数来计算的,如下:

// wordsNeeded calculates the number of words needed for i bits
func wordsNeeded(i uint) int {
	if i > (Cap() - wordSize + 1) {
		return int(Cap() >> log2WordSize)
	}
	return int((i + (wordSize - 1)) >> log2WordSize)
}

这里主要看第6行的int((i + (wordSize - 1)) >> log2WordSize)。这里有几个常量,如下:

  • **log2WordSize常量:**在bitset中的定义是uint(6)。为什么是6呢?因为2的6次方是64,而我们在set字段中又是用uint64来表示一组二进制位的。 同时 看这个计算右移6位,右移6位代表什么?就是代表用左边的数除以64(2的6次方)的商。这里我们要计算length个位数一共能用几个uint64来表示,就是用length除以64即可了。
  • **wordSize常量:在bitset中的定义是uint(64)。**正好表示的是64位,一个uint64类型的位数。这里要看一下为什么还要用i(也就是length)加上一个(wordSize-1)呢?。举个例子,假设i=65,即要表示65个二进制位,那需要用两个uint64的整数来表示才行。但65右移6位是1,所以需要加上wordSize-1再右移6位,结果就是2,即用2个uint64的整数才能存储65位的二进制位。

所以,wordsNeeded函数表示的就是要存储i个二进制位需要用几个uint64的整数。

2.3 如何在整数中实现位操作

为了简便,我们用uint8来说明。uint8代表的是一个8位的非负整数。例如,要把uint8的第2位设置成1。用二进制表示就是:00000100。这个怎么得到呢?我们知道1的二进制表示是00000001,那么让这个1左移2位就能得到结果00000100。即 1<<2

如果再把该uint8的第3位也设置成1,怎么办呢?首先让1左移3位得到00001000。因为原有uint8的第二位也是1,这里就要用uint8原有的值和00001000进行做或操作,就能保持住uint8原有的位的值不变了。如下:

原有的uint8(第二位是1):00000100
          第三位设置成1:00001000
     -----------------------------
或的结果:              00001100

以上就是在整数中进行的位操作。

2.4 如何计算第N位落在哪个分组上

在上面的BitSet的数据结构中,我们知道set字段是一个uint64的切片类型,相当于把每64位分成一组。那么,当设置第N位为1的时候,首先要做的是计算第N位应该落在哪个分组上。这个是怎么计算呢?就是第N位是63(因为位数是从0开始的)的多少倍,比如要设置第66位为1,那么66位是63的1倍(余数省略),所以在切片的第1个分组上(索引是从0开始,实际是切片的第二个分组)。

还是以uint8(8位)一组为例来说。如果要设置第10位,则落在第二个uint8的分组上。如下:

按位操作来计算除法就是右移操作。这里让N右移3位,因为移动3位,代表的2的3次方,即8。也是用10除以8的商是1,即在set切片的第1个索引上,也就是第二个uint8上。

2.5 如何计算第N位落在分组的第几位上

其次,要计算第N位是在第2个分组的第几位上。简单点就是取余操作。用10%8,就是第2位上(因为从0开始,所以是第3位)。 同样,这里还有一种按位移操作的方法:10&7。我们解释下这个与操作。 我们看下8的二进制表示:1000。要想让10除以8,就是将第3位的1抹掉,并保持其他位不变。要想保持原有位保持不变,就和1进行与操作。所以,让二进制的1000变成0111,再和10的二进制进行与操作,就相当于除以8取余数了。如下:

你看,这样就把最高位的1给消除了,结果余数是2的1次方,即2。 最后,因为一个uint8的整数的最高位是第7位(从0位开始),所以第10位应该是第二个uint8的第3位上。最后让1再左移上述结果的2位即可。

如下是bitset的实现:

// log2WordSize is lg(wordSize)
const log2WordSize = uint(6)
func (b *BitSet) Set(i uint) *BitSet {
	if i >= b.length { // if we need more bits, make 'em
		b.extendSet(i)
	}
	// 说明第0位从右边往左边数的
	b.set[i>>log2WordSize] |= 1 << wordsIndex(i)
	return b
}
// the wordSize of a bit set
const wordSize = uint(64)
// wordsIndex calculates the index of words in a `uint64`
func wordsIndex(i uint) uint {
	return i & (wordSize - 1)
}

以上就是针对BitSet最基本的数据结构以及如何设置一个位为1的实现,其他的方法基本都是类似的思想来实现的,有兴趣大家可以继续研读该包的源代码。

总结

bitset基于uint64的整数实现了位的操作。该包的代码实现中涉及到大量的位操作。阅读本包的源代码,可以帮助大家理解位操作的概念以及应用场景。

以上就是浅析Go语言bitset的实现原理的详细内容,更多关于Go bitset的资料请关注脚本之家其它相关文章!

相关文章

  • GO语言基本类型分析

    GO语言基本类型分析

    这篇文章主要介绍了GO语言基本类型,较为详细的分析了整形、浮点型、字符串、指针等类型的具体用法,是深入学习GO语言所必须掌握的重要基础,需要的朋友可以参考下
    2014-12-12
  • 详解Golang时间处理的踩坑及解决

    详解Golang时间处理的踩坑及解决

    在各个语言之中都有时间类型的处理,这篇文章主要和大家分享一下Golang进行时间处理时哪里最容易踩坑以及解决方法,需要的可以参考一下
    2023-01-01
  • 一文带你搞懂golang中内存分配逃逸分析

    一文带你搞懂golang中内存分配逃逸分析

    这篇文章主要带大家一起学习一下golang中内存分配逃逸分析,文中的示例代码讲解详细,对我们深入了解golang有一定的帮助,感兴趣的小伙伴可以了解下
    2023-08-08
  • Go语言中读取命令参数的几种方法总结

    Go语言中读取命令参数的几种方法总结

    部署golang项目时难免要通过命令行来设置一些参数,那么在golang中如何操作命令行参数呢?那么下面这篇文章就来给大家介绍了关于Go语言中读取命令参数的几种方法,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面随着小编来一起看看吧。
    2017-11-11
  • golang中for循环遍历channel时需要注意的问题详解

    golang中for循环遍历channel时需要注意的问题详解

    这篇文章主要给大家介绍了关于golang中for循环遍历channel时需要注意的问题的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2018-04-04
  • Go container包的介绍

    Go container包的介绍

    这篇文章主要介绍了Go container包,go语言container包中有List和Element容器ist和Element都是结构体类型。结构体类型有一个特点,那就是它们的零值都会是拥有其特定结构,但没有任何定制化内容的值,相当于一个空壳,下面一起进文章来了解具体内容吧
    2021-12-12
  • Golang指针隐式间接引用详解

    Golang指针隐式间接引用详解

    在 Go中,指针隐式解引用是指通过指针直接访问指针所指向的值,而不需要显式地使用 * 运算符来解引用指针,这篇文章主要介绍了Golang指针隐式间接引用,需要的朋友可以参考下
    2023-05-05
  • Go JSON编码与解码的实现

    Go JSON编码与解码的实现

    这篇文章主要介绍了Go JSON编码与解码的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • 快速升级Go版本(几分钟就搞定了)

    快速升级Go版本(几分钟就搞定了)

    go现在的更新速度是非常的快啊,用着用着网上的教程就不配套了,下面这篇文章主要给大家介绍了关于快速升级Go版本的相关资料,文中介绍的方法几分钟就搞定了,需要的朋友可以参考下
    2024-05-05
  • 利用Golang生成整数随机数方法示例

    利用Golang生成整数随机数方法示例

    这篇文章主要介绍了利用Golang生成整数随机数的相关资料,文中给出了详细的介绍和完整的示例代码,相信对大家具有一定的参考价值,需要的朋友们下面来一起看看吧。
    2017-04-04

最新评论