http://blog.studygolang.com/2014/09/bitmap_multi_language/
工作中碰到这样一个问题:
有一个文本文件,有上亿行数据,每行数据是 unsigned int。现在需要将其中可能重复的数只保留一个,同时和另外一个或多个这样的文件进行排重(即和它们做差集)。要求尽可能快的筛选出来。
开始实现比较简单粗暴,将数据直接通过 LOAD DATA INFILE 导入 MySQL 表中,然后多表之间做 LEFT JOIN。数据不是特别大,比如几千万,且就要排重的文件不多时,比如一个,速度还可以接受。然而,当数据上亿,且有多个文件需要排重时,性能急剧下降,必须进行优化。而这,正是 Bitmap 的应用场景。
1、Bitmap 概念
Bitmap 是一个十分有用的数据结构。所谓的 Bit-map 就是用一个 bit 位来标记某个元素对应的 Value,而 Key 即是该元素。由于采用了 Bit 为单位来存储数据,因此在内存占用方面,可以大大节省。(《编程珠玑》第一章引入的问题,提到了 Bitmap)
2、Bitmap 的实现原理
以一个简单的数组排序来说明 Bitmap 的实现原理:array[4,6,3,1,7]
Bitmap 采用的是以空间换时间的思想,数组中最大元素值为7,所以在内存中开辟8位的存储空间,存储空间大小的确定方法是(元素最大值进位到8的倍数/8),之所以除以8,是因为开辟空间的时候以byte为单位,1byte=8bit。
开辟8位的空间后,每位初始化为0,如下表:
0号位
1号位
2号位
3号位
4号位
5号位
6号位
7号位
0
0
0
0
0
0
0
0
开始遍历 array 数组,array[0]=4 时,则将 4号位 置1,变为下表:
0号位
1号位
2号位
3号位
4号位
5号位
6号位
7号位
0
0
0
0
1
0
0
0
array[1]=6 时,则将 6号位 置1,变为下表:
0号位
1号位
2号位
3号位
4号位
5号位
6号位
7号位
0
0
0
0
1
0
1
0
直至遍历完 array 数组,空间各位如下表:
0号位
1号位
2号位
3号位
4号位
5号位
6号位
7号位
0
1
0
1
1
0
1
1
最后,从头开始遍历空间中各位,为1的输出其 位号,得:1,3,4,6,7,其效率为O(n)=8
3、Bitmap 编码实现
一般的,静态语言比较容易实现 Bitmap。在下面的实现中,Bitmap 数据结构可以直接定义为 byte 数组,然而,出于使用的方面原因,这里 Bitmap 的实现额外保存了一些其他信息,因此 Go 和 C 中,使用 struct 定义 Bitmap。
从上面的排序例子知道,实现的关键是 置位 和 清位。
3.1 Go 语言实现
Bitmap 数据结构定义如下:
1
type Bitmap struct {
2
// 保存实际的 bit 数据
3
data []byte
4
// 指示该 Bitmap 的 bit 容量
5
bitsize uint64
6
// 该 Bitmap 被设置为 1 的最大位置(方便遍历)
7
maxpos uint64
8
}
置位 和 清位 方法:
1
// SetBit 将 offset 位置的 bit 置为 value (0/1)
2
func (this *Bitmap) SetBit(offset uint64, value uint8) bool {
3
index, pos := offset/8, offset%8
4
5
if this.bitsize < offset {
6
return false
7
}
8
9
if value == 0 {
10
// &^ 清位
11
this.data[index] &^= 0x01 << pos
12
} else {
13
this.data[index] |= 0x01 << pos
14
15
// 记录曾经设置为 1 的最大位置
16
if this.maxpos < offset {
17
this.maxpos = offset
18
}
19
}
20
21
return true
22
}
完整实现代码:Github Bitmap Golang
3.2 C 语言实现
Bitmap 数据结构定义如下:
1
typedef struct {
2
uint64_t bitsize;
3
uint64_t maxpos;
4
5
/* 不定长,必须是结构的最后一个成员 */
6
uint8_t data[];
7
} Bitmap;
置位 和 清位 方法:
1
bool set_bit(Bitmap* bitmap, uint64_t offset, uint8_t value)
2
{
3
uint64_t index, pos;
4
5
index = offset / 8;
6
pos = offset % 8;
7
8
if (bitmap->bitsize < offset) {
9
return false;
10
}
11
12
if (value) {
13
bitmap->data[index] |= 1 << pos;
14
15
if (bitmap->maxpos < offset) {
16
bitmap->maxpos = offset;
17
}
18
} else {
19
bitmap->data[index] &= BITMAP_MASK ^ (1 << pos);
20
}
21
22
return true;
23
}
相比和 Go 语言版的不同点是,C 没有直接的 清位 操作符
完整实现代码:Github Bitmap C
3.3 Java 语言实现
Bitmap 采用类实现,定义如下成员变量
1
private byte[] data;
2
3
private long bitsize;
4
private long maxpos;
置位 和 清位 方法:
1
public boolean setBit(long offset, int value) {
2
if (this.bitsize < offset) {
3
return false;
4
}
5
6
int index = (int) offset / 8;
7
int pos = (int) offset % 8;
8
9
if (value == 1) {
10
this.data[index] |= 1 << pos;
11
if (this.maxpos < offset) {
12
this.maxpos = offset;
13
}
14
} else {
15
this.data[index] &= Bitmap.BITMAP_MASK ^ (1 << pos);
16
}
17
18
return true;
19
}
跟 C 语言一样, Java 也没有直接提供 清位 操作符。
完整实现代码:Github Bitmap Java
3.4 三种语言实现的不同点
通过对比三种语言的实现,可以看出一些不同点:
1)数据类型的支持:Go 和 C 可以实现 uint8/uint64,而 Java 不区分是否有符号(Java 8 提供了无符号数);
2)清位操作:Go 提供了清位操作符;而 C 和 Java 需要自己实现,关键点是 0×01 和 MASK 做异或操作;
4、Bitmap 具体实现的关键点
从上面三种语言的具体实现可以看出,Bitmap 实现的关键有如下几点:
1)使用 byte 数组保存数据,这样可以极大的节省内存空间;
2)某个元素(也就是某个位置)要置为0或1,通过对 8 (1byte=8bit) 做除法和取余来实现;
3)具体的置位或清位,使用位操作实现:没有直接提供操作符的,可以通过多种操作符组合实现;
5、Bitmap 更多应用
一般地涉及到无重复、int 类型的问题,可以考虑 Bitmap 是否能够实现。
1)文章开头提到的排重实现(代码见 github 的taskdiff)
2)《编程珠玑》第一章的问题,可以参考《编程珠玑–位图法排序》
3)已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数
8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。 (可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==12.5MBytes,这样,就用了小小的12M左右的内存表示了所有的8位数的 电话)
4)2.5亿个整数中找出不重复的整数的个数(内存空间不足以容纳这2.5亿个整数)
将bit-map扩展一下,用2bit表示一个数即可,0表示未出现,1表示出现一次,2表示出现2次及以上,在遍历这些数的时候,如果对应位置的 值是0,则将其置为1;如果是1,将其置为2;如果是2,则保持不变。或者我们不用2bit来进行表示,我们用两个bit-map即可模拟实现这个 2bit-map,都是一样的道理。
6、题外话
用三种语言实现的Bitmap进行排重处理,C 和 Java 版本速度差不多,1.5 亿数据60秒内处理完;而 Go 版本需要 1分50秒 左右(主要 bufio 包性能不太理想);内存占用方面,C 最少,Go 次之,Java 最多。