• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • SQLite3源碼學習(27) Bitmap算法

    標簽: SQLite  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文件。






    版權聲明:本文為pfysw原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/pfysw/article/details/80287900

    智能推薦

    經典算法- BitMap

    原文鏈接https://www.jianshu.com/p/6082a2f7df8e 一、問題引入 舉一個例子,有一個無序有界int數組{1,2,5,7},初步估計占用內存4X4=16字節,這倒是沒什么奇怪的,但是假如有10億個這樣的數呢,如果這樣的一個大的數據做查找和排序,那估計內存也崩潰了,有人說,這些數據可以不用一次性加載,那就是要存盤了,存盤必然消耗IO。我們提倡的是高性能,這個方案直接不...

    27.Qt操作sqlite3數據庫

    1 sqlite3操作編程 問題1 驅動無法加載 解決辦法將Qt自帶安裝目錄下的sql文件拷貝到可執行文件目錄下。 2 連接數據庫 3 查詢數據庫 4 數據庫插入...

    Sqlite3入門學習(一)

    Linux環境下sqlite3的安裝及常用Linux API說明 環境安裝 Step1:一般的linux可能會自帶sqlite,在安裝之前先使用sqlite3命令檢測一下,若顯示并未安裝,則進行下述操作 Step2:先到  https://www.sqlite.org/download.html ,下載sqlite-autoconf-*.tar.gz壓縮包 Step3:下載完了...

    HTML中常用操作關于:頁面跳轉,空格

    1.頁面跳轉 2.空格的代替符...

    freemarker + ItextRender 根據模板生成PDF文件

    1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...

    猜你喜歡

    電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!

    Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...

    requests實現全自動PPT模板

    http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...

    Linux C系統編程-線程互斥鎖(四)

    互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...

    統計學習方法 - 樸素貝葉斯

    引入問題:一機器在良好狀態生產合格產品幾率是 90%,在故障狀態生產合格產品幾率是 30%,機器良好的概率是 75%。若一日第一件產品是合格品,那么此日機器良好的概率是多少。 貝葉斯模型 生成模型與判別模型 判別模型,即要判斷這個東西到底是哪一類,也就是要求y,那就用給定的x去預測。 生成模型,是要生成一個模型,那就是誰根據什么生成了模型,誰就是類別y,根據的內容就是x 以上述例子,判斷一個生產出...

    精品国产乱码久久久久久蜜桃不卡