1. 算法背景
假如有100个不重复的数存放在文件里,怎么确定某个数是否在这100个数中?
一般可以这样做,将这100个数读取到内存并存放在char a[100]的数组里,只需遍历这100个数即可。
那么假如有10亿个不重复的数呢,最大的数是2^32-1,这个时候显然内存存放不了那么多数,那么只能先将一部分数据读到内存,做完判断后再去读一部分数据,直到读完10亿个数为止,这个时候不但需要遍历很久,而且还要大量的磁盘I/O操作,在时间上是不可承受的。
为了加快查找,可以申请一个4G的超大数组char a[2^32],把每个数都映射成数组的下标,如果某个数x存在,就将a[x-1]置1,不存在就置0,这是时候只需一次取地址操作即可。但是内存显然更大,我们观察到上面的方法用一个字节来标记一个数是否存在,其实只需1bit就可以标记,这样就可以把内存减小8倍。
网上所说的Bitmap算法正是这么一个算法,把一个数映射成一个bit来标记。下图说明了简单的示例,有2个字节,标记1~15,其中{1,2,5,7,11}存在,则将相应的bit位置1,否则置0。
代码实现起来很简单,假设sz是数据范围的上限,用pV来表示一个bit数组。
unsigned char *pV = 0;
pV = sqlite3MallocZero( (sz+7)/8 + 1 ); //申请sz/8字节大小的的内存
那么SETBIT(pV,1000)就是把pV中的第1000比特置1,CLEARBIT(pV,1000)就是把pV中的第1000比特置0,TESTBIT(pV,1000)就是判断1000是否在pV中存在,宏定义实现如下
#define SETBIT(V,I) V[I>>3] |= (1<<(I&7))
#define CLEARBIT(V,I) V[I>>3] &= ~(1<<(I&7))
#define TESTBIT(V,I) (V[I>>3]&(1<<(I&7)))!=0
2. 算法改进
虽然用bit来表示一个数可以减小内存空间,但也需要500M的内存,内存占用还是太大,我们发现即使数据量有10亿,也有大量的bit位是空着,而且一般情况下数据量也没这么大,这就导致了大量内存空间的浪费。
针对上述问题,SQLite对Bitmap算法做了改进,也就是需要多少数,就申请多少bit的空间,算法的实现是非常精妙的,下面我们就来欣赏一下。
SQLite主要用Bitmap算法来存放文件数据页编号,最大可以到2^32-1。首先定义一个Bitvec的结构体,该结构体如下。
typedef struct Bitvec Bitvec;
struct Bitvec {
//Bitmap中存放的最大的数为2^32-1
u32 iSize; /* Maximum bit index. Max iSize is 4,294,967,296. */
//当前Bitvec对象中hash元素的个数
u32 nSet; /* Number of bits that are set - only valid for aHash
** element. Max is BITVEC_NINT. For BITVEC_SZ of 512,
** this would be 125. */
//把iSize分割后的子对象的iSize大小
u32 iDivisor; /* Number of bits handled by each apSub[] entry. */
/* Should >=0 for apSub element. */
/* Max iDivisor is max(u32) / BITVEC_NPTR + 1. */
/* For a BITVEC_SZ of 512, this would be 34,359,739. */
//以下是一个联合体,根据iSize、nSet和iDivisor的大小来决定使用哪一种结构
union {
BITVEC_TELEM aBitmap[BITVEC_NELEM]; /* Bitmap representation */
u32 aHash[BITVEC_NINT]; /* Hash table representation */
Bitvec *apSub[BITVEC_NPTR]; /* Recursive representation */
} u;
};
相关宏定义如下:
/* Bitvec 结构体的大小*/
#define BITVEC_SZ 512
/* 一个字节类型,占8bit*/
#define BITVEC_TELEM u8
#define BITVEC_NELEM 500 // 即aBitmap的大小占512-4*3=500字节
#define BITVEC_NINT 125 //即aHash的长度,占125*4=500字节
#define BITVEC_NPTR 125 //即apSub的长度,占125*4=500字节
#define BITVEC_NBIT 4000//即一个Bitvec对象所表示的最大bit数量500*8
为了清晰起见,把联合体中的宏定义替换掉得到
union {
u8 aBitmap[500]; /* Bitmap representation */
u32 aHash[125]; /* Hash table representation */
Bitvec *apSub[125]; /* Recursive representation */
} u;
那么自然要问aBitmap、aHash和apSub都有什么作用,根据Bitvec对象所处的状态来决定用哪个数据结构来表示。假设p是一个Bitvec对象,其关系如下图
当p没有被分割时,即p-> iDivisor=0时,如果p-> iSize<=4000当前对象的元素个数小于4000,则用aBitmap的每个bit位来存放数据,当p->iSize>4000时,由于aBitmap已经不够存放,所以用aHash来存放数据,最多存放124个,当超过时把p平均分割成125份,每一份长度为p-> iDivisor,当p被分割后,联合体u用apSub来表示每个分割后的子对象。
3. 代码实现
首先创建一个Bitvec对象,设置iSize为该对象的数据长度:
Bitvec *sqlite3BitvecCreate(u32 iSize){
Bitvec *p;
assert( sizeof(*p)==BITVEC_SZ );
p = sqlite3MallocZero( sizeof(*p) );
if( p ){
p->iSize = iSize;
}
return p;
}
接下来在Bitvec对象p中用sqlite3BitvecSet() 函数把数据i对应的bit位置1,如上一节所说,这里分为3种情况考虑,还使用了递归的技术代码如下。
int sqlite3BitvecSet(Bitvec *p, u32 i){
u32 h;
if( p==0 ) return SQLITE_OK;
assert( i>0 );
assert( i<=p->iSize );
i--;//在SQLite中页号从1开始,在Bitmap中从第0比特开始
//这里需要注意的是p->iDivisor>0必有p->iSize > 4000
//因为只有p->iSize >4000,才有必要分割
//但是当p->iSize >4000,不一定需要分割,可能有p->iDivisor=0
//如果该对象分割后,找出最后没有被分割的子对象
while((p->iSize > BITVEC_NBIT) && p->iDivisor) {
//由第70行p->iDivisor = (p->iSize + 125 - 1)/125可知
// p->iSize被分割成了125份,存放在 p->u.apSub子对象中
//bin为子对象的索引
u32 bin = i/p->iDivisor;
i = i%p->iDivisor;
if( p->u.apSub[bin]==0 ){
//子对象不存在新建一个
p->u.apSub[bin] = sqlite3BitvecCreate( p->iDivisor );
if( p->u.apSub[bin]==0 ) return SQLITE_NOMEM_BKPT;
}
p = p->u.apSub[bin];
}
//此时p->iDivisor=0,如果p->iSize<4000,把i在p->u.aBitmap中的相应的bit置1
if( p->iSize<=BITVEC_NBIT ){
p->u.aBitmap[i/BITVEC_SZELEM] |= 1 << (i&(BITVEC_SZELEM-1));//BITVEC_SZELEM=8
return SQLITE_OK;
}
//如果p->iSize>4000则把i存放在对应的hash数组里
h = BITVEC_HASH(i++);//i%125
/* if there wasn't a hash collision, and this doesn't */
/* completely fill the hash, then just add it without */
/* worring about sub-dividing and re-hashing. */
//当对应的hash表中还没有元素时
if( !p->u.aHash[h] ){
if (p->nSet<(BITVEC_NINT-1)) {
//hash表中元素个数小于125时,直接对p->u.aHash[h]赋值
goto bitvec_set_end;
} else {
//hash表元素超了后,将对象分割
goto bitvec_set_rehash;
}
}
/* there was a collision, check to see if it's already */
/* in hash, if not, try to find a spot for it */
//如果元素存在冲突,找一个空闲的hash元素存放
//由上面的代码知道这个空闲的元素是必定存在的
do {
if( p->u.aHash[h]==i ) return SQLITE_OK;
h++;
if( h>=BITVEC_NINT ) h = 0;
} while( p->u.aHash[h] );
/* we didn't find it in the hash. h points to the first */
/* available free spot. check to see if this is going to */
/* make our hash too "full". */
bitvec_set_rehash:
// BITVEC_MXHASH是125/2=62
//如果hash表中元素存在冲突,那么大于62就开始分割
if( p->nSet>=BITVEC_MXHASH ){
unsigned int j;
int rc;
//分配1个临时的hash数组
u32 *aiValues = sqlite3StackAllocRaw(0, sizeof(p->u.aHash));
if( aiValues==0 ){
return SQLITE_NOMEM_BKPT;
}else{
//把原来的数存放在临时数组里
memcpy(aiValues, p->u.aHash, sizeof(p->u.aHash));
memset(p->u.apSub, 0, sizeof(p->u.apSub));
p->iDivisor = (p->iSize + BITVEC_NPTR - 1)/BITVEC_NPTR;
//这里采用递归调用来创建分割后i所在的子对象
rc = sqlite3BitvecSet(p, i);
//对象p分割后,对原来存放在hash表中的元素重新设定
for(j=0; j<BITVEC_NINT; j++){
if( aiValues[j] ) rc |= sqlite3BitvecSet(p, aiValues[j]);
}
sqlite3StackFree(0, aiValues);
return rc;
}
}
bitvec_set_end:
p->nSet++;
p->u.aHash[h] = i;
return SQLITE_OK;
}
理解了数值如何在Bitvec对象存放后,其他接口也就不难理解了,只要分3种情况讨论即可。
int sqlite3BitvecTestNotNull(Bitvec *p, u32 i)
该函数判断元素i是否在对象p中,如果p被分割了,那么找到i在哪个分割后的子对象里,在子对象里根据p->iSize是否小于4000来决定在aBitmap中找还是在aHash里找。
void sqlite3BitvecClear(Bitvec *p, u32 i, void *pBuf)
清除对象p中的数值i,同上分为3种情况处理,pBuf是用来重新排列hash表时的临时数组。
void sqlite3BitvecDestroy(Bitvec *p)
销毁对象p和p中包含的所有子对象
4. 测试代码
SQLite提供了Tcl的测试接口:
int sqlite3BitvecBuiltinTest(int sz, int *aOp)
sz为设置Bitvec对象的数据长度,aOp是一组操作命令,每4个元素组成一组操作命令,最后以0结尾
aOp[0]为命令号, aOp[1]为重复的次数,aOp[2]为赋值的页号,aOp[3]为每次重复后页号的递增值。
Tcl脚本使用方法为:
sqlite3BitvecBuiltinTest SIZE PROGRAM
参数SIZE的值传给sz,PROGRAM的值传给aOp
使用举例:
sqlite3BitvecBuiltinTest 400000 {1 400000 1 1 0}
该命令表示设置对象的长度为400000,{1 400000 1 1 0}为一个命令组,从页号1开始对Bitvec对象赋值,重复400000次,每次递增1
更多的测试用例见bitvec.test文件。