SQLite3源码学习(27) Bitmap算法

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文件。






猜你喜欢

转载自blog.csdn.net/pfysw/article/details/80287900