倒排索引壓縮:改進的PForDelta算法


由於倒排索引文件往往占用巨大的磁盤空間,我們自然想到對數據進行壓縮。同時,引進壓縮算法后,使得磁盤占用減少,操作系統在query processing過程中磁盤讀取效率也能提升。另外,壓縮算法不僅要考慮壓縮效果,還要照顧到query processing過程的解壓縮效率。

總的來說,好的索引壓縮算法需要最大化兩個方面:

           1、減少磁盤資源占用

           2、加快用戶查詢響應速度

其中,加快響應速度比減少磁盤占用更為重要。本文主要介紹PForDelta壓縮算法,其簡單易懂,能夠提供可觀的數據壓縮,同時具備非常高的響應速度,因此廣泛的運用於很多實際信息檢索系統中。

 

待壓縮的倒排索引數據

一個posting單元由<DocID、TF、Term position…>組成。對於每個DocID,其保存在硬盤中的大小取決於文件集最大文檔編號的大小。這樣造成編號較小的DocID分配了和編號較大的DocID(上百萬)一樣的存儲空間,浪費了資源。由於每個posting是根據DocID順序存儲,所以不需要保存DocID,只需要保存前后兩個DocID的差值,這樣可以大大減小DocID儲存空間,這種方式成為Delta Encoding。如下圖:

          

對於tf值,根據Zipf定律,tf值較小的term占大多數,我們可以對這類tf值少分配一些空間保存。而tf大的term占少數,對這些tf分配多空間儲存。基於上述排列特性,往往將docID和tf及其他數據分開放置,方便數據壓縮。最終,整體的存儲結構如下圖所示:

          

為了方便分布式存儲倒排索引文件,Data Block是硬盤中的基礎存儲單元。由於建立過程需要,每個term 的postinglist被拆分為多個部分保存在多個block中(如圖不同顏色的block代表存儲不同term的postinglist)。也就是說,每個block內部可能包含多個term的postinglist片段。

Data block的基本組成單元是數據塊(chunk),每個chunk一般包含固定數量的posting,圖中所示一個chunk包含128個posting,這些posting都屬於同一個term。其中將DocID、tf和position分開排放,方便壓縮。

這樣以block為單元,以chunk為基礎元素的索引存儲的方式,一方面可以支持使用caching的方法緩存最常用term的postinglist,提高query響應速度。另一方面,所有壓縮解壓縮過程都以chunk為單位,都在chunk內部進行。當需要查找某一term的postinglist時,不需要對所有文件進行解壓縮。對於不相關的chunk直接忽略,只需要對少部分block中的目標chunk進行處理,這樣又從另一個方面大大縮短了query響應時間。這也是chunk機制設置的初衷。接下來,我們討論如何對一個chunk結構進行壓縮和解壓縮處理。

 

PForDelta算法

PForDelta算法最早由Heman在2005年提出(Heman et al ICDE 2006),它允許同時對整個chunk數據(例128個數)進行壓縮處理。基礎思想是對於一個chunk的數列(例128個),認為其中占多數的x%數據(例90%)占用較小空間,而剩余的少數1-x%(例10%)才是導致數字存儲空間過大的異常值。因此,對x%的小數據統一使用較少的b個bit存儲,剩下的1-x%數據單獨存儲。

舉個例子,假設我們有一串數列23, 41, 8, 12, 30, 68, 18, 45, 21, 9, ..。取b = 5,即認為5個bit(32)能存儲數列中大部分數字,剩下的超過32的數字單獨處理。從可見的隊列中,超過32的數字有41, 68, 45。那么PForDelta壓縮后的數據如下圖所示(圖中將超過32的數字稱為異常值exception):

          

圖中第一個單元(5bit)記錄第一個異常值的位置,其值為“1”表示間隔1個b-bit之后是第一個異常值。第一個異常值出現在“23”之后,是“41”,其儲存的位置在隊列的最末端,而其在128個5bit數字中的值“3”表示間隔3個b-bit之后,是下一個異常值,即“68”,之后依次類推。異常值用32bit記錄,在隊列末尾從后向前排列。

上述隊列就對應一個chunk(DocID),還需要另外記錄b的取值和一個chunk壓縮后的長度。這樣就完整的對一個chunk數據進行了壓縮。

但是這樣算法有一個明顯的不足:如果兩個異常值的間隔非常大(例如超過32),我們需要加入更多的空間來記錄間隔,並且還需要更多的參數來記錄多出多少空間。為了避免這樣的問題,出現了改進的算法NewPFD。

 

改進的PForDelta算法

在PForDelta算法基礎上,H. Yan et.al  WWW2009提出NewPFD算法及 OptPFD算法。

NewPFD算法

由於PForDelta算法最大的問題是如果異常值間隔太大會造成b-bit放不下。NewPFD的思路是:128個數最多需要7個bit就能保存,如果能將第二部分中保存異常值的32bit進行壓縮,省出7bit的空間用於保存這個異常值的位置,問題就迎刃而解了。同時更自然想到,如果異常值位置信息保存在隊列后方的32bit中,那么隊列第一部分原用於記錄異常值間隔的對應部分空間就空余出來了,可以利用這部分做進一步改進。

因此,NewPFD的算法是,假設128個數中,取b=5bit,即32作為閾值。數列中低於32的數字正常存放,數列中大於32的數字,例如41 (101001) 將其低5位(b-bit)放在第一部分,將其剩下的高位(overflow)存放在隊列末端。我們依然以PForDelta中的例子作為說明,一個128位數列23, 41, 8, 12, 30, 68, 18, 45, 21, 9, ..。經過NewPFD算法壓縮后的形式如下圖所示:

          

NewPFD算法壓縮后的數據依然包括兩部分,第一部分128個b-bit數列,省去了第一個異常值位置單元;第二部分異常值部分包含異常值的位置和異常值的高位數字。例如,對於異常值“41”其2進制碼為101001,那么低5位01001保存在數據塊第一部分。在第二部分中,先保存位置信息(“41”的位置是“1”,表示原數列第2個),再以字節為單位保存高位“1”即“0000 0001”,這樣反而只需要附加2個字節(一個保存位置,一個保存高位)就可以儲存原本需要4個字節保存的異常值。而對於高位字節,還可以繼續使用壓縮算法進行壓縮,本文不再繼續討論。

除了數據列,NewPFD算法還需要另外保存b值和高位占的字節數(稱為a值)。因為參數ab已經確定了數據塊的長度,因此chunk長度值不用再單獨記錄。

OptPFD算法

OptPFD算法在NewPFD之上,認為每個數據壓縮單元chunk應該有適應自己數據的獨立a值和b值,這樣雖然需要保存大量的ab值,但是畢竟數據量小不會影響太大的速度,相反,由於對不同chunk單獨壓縮,使壓縮效果更好,反而提高了解壓縮的效果。

對於b的選取,通常選擇2^b可以覆蓋數列中90%的數字,也就是控制異常值在10%左右,這樣可以獲得壓縮效果和解壓縮效率的最大化。

 

索引壓縮步驟

了解了壓縮算法原理,下面我們來看倒排索引文件具體如何壓縮。我們采用OptPFD算法,先定義數據結構和參數:

1、定義一次壓縮64個數字;每次選擇其中的90%數字作為正常數字,剩余10%作為異常值;

2、建立一個結構體PForBlock,保存壓縮后的數據,內部包含數據和壓縮參數a、b;

3、定義一個PForDelta算法對象,主要包括兩個成員函數GetAB和compress,分別表示計算壓縮參數ab值以及實施具體壓縮算法。其中數組bsets_[9],記錄可取的b值,為了方便計算參數b。為了數據對齊,刪除了一些b的取值。

4、以4字節為一個單元存放壓縮數據,為了解壓縮時代碼更簡潔高效。例如64個待壓縮數字經計算后取b = 3,則4個字節32bit一共能存放10個壓縮后的數字,剩余32-3*10 = 2bit高位留空。(實際編碼中可任意選擇編程方案)

 1 #define COMPRESS_BLOCK_SIZE 64
 2 #define PFOR_THRESHOLD 0.9
 3 
 4 struct PForBlock {
 5   unsigned a,b;
 6   vector<unsigned> data;
 7 };
 8 
 9 class PForCompressor {
10  public:
11   PForCompressor() {
12     bsets_[0]=1;bsets_[1]=2;bsets_[2]=3;bsets_[3]=4;bsets_[4]=5;bsets_[5]=6;
13     bsets_[6]=8;bsets_[7]=10;bsets_[8]=16;
14   }
15   PForBlock compress(const vector<unsigned> &v);
16 
17  protected:
18   void getAB(const vector<unsigned> &v);
19   unsigned bsets_[9];
20   unsigned a_, b_;
21 };

其中,getAB()和compress()函數具體代碼如下:

 1 void PForCompressor::getAB(const vector<unsigned> &v) {
 2   vector<unsigned> u = v;
 3   sort(u.begin(), u.end());
 4   unsigned threshold = u[((unsigned)(double)u.size()*PFOR_THRESHOLD - 1)];
 5   unsigned max_num = u[u.size() - 1];
 6   // Get b
 7   unsigned bn = 0;
 8   for (; bn < 8; ++bn) {
 9     if ((threshold >> bsets_[bn]) <= 0)
10       break;
11   }
12   b_ = bsets_[bn];
13   // Get a
14   max_num >>= b_;
15   a_ = 1;
16   for (; a_ < 4; ++a_) {
17     if ((1 << (a_ * 8)) > max_num)
18       break;
19   }
20 }
 1 PForBlock PForCompressor::compress(const vector<unsigned> &v) {
 2   getAB(v);
 3   unsigned threshold = 1 << b_;
 4   vector<unsigned> tail;
 5   vector<unsigned>::iterator it;
 6   PForBlock block;  
 7   block.a = a_;
 8   block.b = b_;
 9   
10   //90% fit numbers and the low b-bit of exceptions are stored at the head
11   unsigned head_size = (v.size() + 32 / b_ + 1) / (32 / b_);
12   for(unsigned i = 0; i < head_size; ++i) 
13     block.data.push_back(0);
14   for(unsigned i = 0; i < v.size(); ++i) {
15     unsigned low = v[i] & (threshold - 1);
16     block.data[i / (32 / b_)] |= low << i % (32 / b_) * b_;
17     if(v[i] >= threshold) {
18       tail.push_back(i);
19       unsigned high = v[i] >> b_;
20       for(unsigned l = 0; l < a_; ++l) {
21         tail.push_back((high >> (l * 8)) & 255);
22       }
23     }
24   }
25 
26   // high-bit of exceptions are stored at the end using a-bytes each.
27   unsigned temp = 0;
28   unsigned i;
29   for(i = 0; i < tail.size(); ++i) {
30     temp |= tail[i] << (i * 8 % 32);
31     if(i % 4 == 3) {
32       block.data.push_back(temp);
33       temp = 0;
34     }
35   }
36   if(i % 4 != 0) 
37     block.data.push_back(temp);
38 
39   return block;
40 }

 

索引解壓步驟

1、解壓前已經知道壓縮數據的參數a和b(由程序或配置文件另外保存),先對壓縮數據的第一部分解壓,以壓縮64個數字為例,則將64個b-bit從壓縮數據中第一部分提取出來;

2、之后再對第二部分解壓,由於已經知道異常值在數列中的索引位置(1字節)和異常值的高位bit(a字節),將異常值的高位比特加入其第一部分的低位比特中,就完成了解壓過程。

3、由於壓縮算法按照4字節為一個單元進行存儲壓縮數據,而不同的參數b值對應不同的存放方案,因此最高效的解壓縮編程是每種壓縮方案單獨編寫一個解壓縮程序。

先來看解壓縮對象定義

 1 class PForDecompressor {
 2   public:
 3     PForDecompressor();
 4     void Decompress(unsigned a,unsigned b,unsigned item_num,unsigned *data,unsigned data_length,unsigned* res);
 5 
 6   private:
 7     typedef void(PForDecompressor::*step1Funs)();
 8     typedef void(PForDecompressor::*step2Funs)(unsigned);
 9     step1Funs step1_[17];
10     step2Funs step2_[5];
11   
12     unsigned item_num_;
13     unsigned data_length_;
14     unsigned* data_;
15     unsigned* res_;
16 
17     void step3();
18     void step1B1();
19     void step1B2();
20     void step1B3();
21     void step1B4();
22     void step1B5();
23     void step1B6();
24     void step1B8();
25     void step1B10();
26     void step1B16();
27     void step1Ex();
28 
29     void step2A1(unsigned b);
30     void step2A2(unsigned b);
31     void step2A3(unsigned b);
32     void step2A4(unsigned b);
33 };

1、壓縮時設定參數b的取值可以是1、2、3、4、5、6、8、10、16,解壓時分別對應step1Bx()函數。這個9個函數以函數指針的形式,以b的數值作為索引,保存在一個具有17個元素的數組里。為了方便檢測b的合法性,非法的b取值也設置在了數組中,由step1Ex表示。當step1Ex被調用時,會返回一個異常。

2、壓縮時參數a的取值可能是1~4,解壓時分別對應step2Ax(),解壓第二部分時,需要用到參數b,因此將其作為函數入參。和解壓第一部分一樣,也將這4個函數以函數指針的形式保存在數組里,方便讀取。

所以,根據算法初始化的要求,自然可以得出類構造函數定義:

 1 PForDecompressor::PForDecompressor() {
 2   step1_[0]=&PForDecompressor::step1Ex;
 3   step1_[1]=&PForDecompressor::step1B1;
 4   step1_[2]=&PForDecompressor::step1B2;
 5   step1_[3]=&PForDecompressor::step1B3;
 6   step1_[4]=&PForDecompressor::step1B4;
 7   step1_[5]=&PForDecompressor::step1B5;
 8   step1_[6]=&PForDecompressor::step1B6;
 9   step1_[7]=&PForDecompressor::step1Ex;
10   step1_[8]=&PForDecompressor::step1B8;
11   step1_[9]=&PForDecompressor::step1Ex;
12   step1_[10]=&PForDecompressor::step1B10;
13   step1_[11]=&PForDecompressor::step1Ex;
14   step1_[12]=&PForDecompressor::step1Ex;
15   step1_[13]=&PForDecompressor::step1Ex;
16   step1_[14]=&PForDecompressor::step1Ex;
17   step1_[15]=&PForDecompressor::step1Ex;
18   step1_[16]=&PForDecompressor::step1B16;
19   step2_[1]=&PForDecompressor::step2A1;
20   step2_[2]=&PForDecompressor::step2A2;
21   step2_[3]=&PForDecompressor::step2A3;
22   step2_[4]=&PForDecompressor::step2A4;
23 }
View Code

獲得壓縮數據后,正式開始解壓縮,調用Decompress成員函數:

1 void PForDecompressor::Decompress(unsigned a,unsigned b,unsigned item_num,unsigned *data,unsigned data_length,unsigned* res) {
2   item_num_ = item_num;
3   data_length_ = data_length;
4   data_ = data;
5   res_= res;
6   (this->*step1_[b])();
7   (this->*step2_[a])(b);
8 }

由於已經寫好解壓第一部分和解壓第二部分的函數,因此Decompress函數代碼非常簡單,只需要將b和a作為索引值分別傳入數組中,調用對應的函數進行第一部分和第二部分的解壓。

我們以b = 3 和 a = 1為例,簡單看解壓步驟,其他參數類似。

以b=3解壓縮第一部分:

 1 void PForDecompressor::step1B3()
 2 {
 3   unsigned l = (item_num_ + 9) / 10;
 4   unsigned i, block;
 5   unsigned *con = res_;
 6   for(i = 0; i < l; ++i)
 7   {
 8     block = *(data_++);
 9     data_length_--;
10     con[0] = block & 7;
11     con[1] = (block>>3) & 7;
12     con[2] = (block>>6) & 7;
13     con[3] = (block>>9) & 7;
14     con[4] = (block>>12) & 7;
15     con[5] = (block>>15) & 7;
16     con[6] = (block>>18) & 7;
17     con[7] = (block>>21) & 7;
18     con[8] = (block>>24) & 7;
19     con[9] = (block>>27) & 7;
20     con += 10;
21   }
22 }

第一部分數據,包含原始數據的正常值和異常數據的低b-bit值,它們都以b-bit存放,共64個。

當b=3時,也就是4個字節32bit可以存放10個b-bit。

因此程序中,data_表示原始待解壓縮的數據流,l表示壓縮數據第一部分的長度,res_/con 是4字節為單位的數組,用來存放解壓縮后的數據,block每次取出data_的4個字節來完成解壓縮過程;

1、因為32個bit存放10個數據,可以容易計算出l值,注意防止除法向下取整造成結果錯誤;

2、每次取出4字節數據放在block中;

3、從低位到高位,依次取出block中的bit,每次3個,將其存放入res_中;

 

第一部分解壓縮完畢,來看a = 1時的第二部分:

 1 void PForDecompressor::step2A1(unsigned b)
 2 {
 3   unsigned block;
 4   while(data_length_ > 0)
 5   {
 6     block = *(data_++);
 7     data_length_--;
 8     res_[block & 255] += ((block >> 8) & 255) << b;
 9     res_[(block >> 16) & 255] += ((block >> 24)) << b;
10   }
11 }

由於當a = 1時,一個異常值占2個字節(一個字節索引,一個字節存放高位bit),那么一個4字節單元可以存放2個異常值。

1、解壓縮的過程非常簡單,每次取出原始數據流的4個字節,其中包含兩個異常值。

2、第一個字節是第一個異常值在原數列中的索引,第二個字節是它的高bit,將高bit左移b位,加入到第一部分已經解壓出的對應位置中;

3、對於第三個字節和第四個字節也是同理。

當a = 2時,一個異常值占3個字節(一個字節索引,二個字節存放高位bit),一個4字節不能完全包含完整的異常值,因此每次需要取出12個字節,從而對4個異常值進行解壓縮。

 

其余參數取值對應的代碼:

 

 1 void PForDecompressor::step1B1()
 2 {
 3   unsigned l=(item_num_+31)/32;
 4   unsigned i,block;
 5   unsigned *con=res_;
 6   for(i=0;i<l;i++)
 7   {
 8     block=*(data_++);
 9     data_length_--;
10     con[0] = block & 1;
11     con[1] = (block>>1) & 1;
12     con[2] = (block>>2) & 1;
13     con[3] = (block>>3) & 1;
14     con[4] = (block>>4) & 1;
15     con[5] = (block>>5) & 1;
16     con[6] = (block>>6) & 1;
17     con[7] = (block>>7) & 1;
18     con[8] = (block>>8) & 1;
19     con[9] = (block>>9) & 1;
20     con[10] = (block>>10) & 1;
21     con[11] = (block>>11) & 1;
22     con[12] = (block>>12) & 1;
23     con[13] = (block>>13) & 1;
24     con[14] = (block>>14) & 1;
25     con[15] = (block>>15) & 1;
26     con[16] = (block>>16) & 1;
27     con[17] = (block>>17) & 1;
28     con[18] = (block>>18) & 1;
29     con[19] = (block>>19) & 1;
30     con[20] = (block>>20) & 1;
31     con[21] = (block>>21) & 1;
32     con[22] = (block>>22) & 1;
33     con[23] = (block>>23) & 1;
34     con[24] = (block>>24) & 1;
35     con[25] = (block>>25) & 1;
36     con[26] = (block>>26) & 1;
37     con[27] = (block>>27) & 1;
38     con[28] = (block>>28) & 1;
39     con[29] = (block>>29) & 1;
40     con[30] = (block>>30) & 1;
41     con[31] = (block>>31) & 1;
42     con+=32;
43   }
44 }
setp1B1
 1 void PForDecompressor::step1B2()
 2 {
 3   unsigned l=(item_num_+15)/16;
 4   unsigned i,block;
 5   unsigned *con=res_;
 6   for(i=0;i<l;i++)
 7   {
 8     block=*(data_++);
 9     data_length_--;
10     con[0] = block & 3;
11     con[1] = (block>>2) & 3;
12     con[2] = (block>>4) & 3;
13     con[3] = (block>>6) & 3;
14     con[4] = (block>>8) & 3;
15     con[5] = (block>>10) & 3;
16     con[6] = (block>>12) & 3;
17     con[7] = (block>>14) & 3;
18     con[8] = (block>>16) & 3;
19     con[9] = (block>>18) & 3;
20     con[10] = (block>>20) & 3;
21     con[11] = (block>>22) & 3;
22     con[12] = (block>>24) & 3;
23     con[13] = (block>>26) & 3;
24     con[14] = (block>>28) & 3;
25     con[15] = (block>>30) & 3;
26     con+=16;
27   }
28 }
step1B2
step1B4
 1 void PForDecompressor::step1B5()
 2 {
 3   unsigned l=(item_num_+5)/6;
 4   unsigned i,block;
 5   unsigned *con=res_;
 6   for(i=0;i<l;i++)
 7   {
 8     block=*(data_++);
 9     data_length_--;
10     con[0] = block & 31;
11     con[1] = (block>>5) & 31;
12     con[2] = (block>>10) & 31;
13     con[3] = (block>>15) & 31;
14     con[4] = (block>>20) & 31;
15     con[5] = (block>>25) & 31;
16     con+=6;
17   }
18 }
step1B5
 1 void PForDecompressor::step1B6()
 2 {
 3   unsigned l=(item_num_+4)/5;
 4   unsigned i,block;
 5   unsigned *con=res_;
 6   for(i=0;i<l;i++)
 7   {
 8     block=*(data_++);
 9     data_length_--;
10     con[0] = block & 63;
11     con[1] = (block>>6) & 63;
12     con[2] = (block>>12) & 63;
13     con[3] = (block>>18) & 63;
14     con[4] = (block>>24) & 63;
15     con+=5;
16   }
17 }
step1B6
 1 void PForDecompressor::step1B8()
 2 {
 3   unsigned l=(item_num_+3)/4;
 4   unsigned i,block;
 5   unsigned *con=res_;
 6   for(i=0;i<l;i++)
 7   {
 8     block=*(data_++);
 9     data_length_--;
10     con[0] = block & 255;
11     con[1] = (block>>8) & 255;
12     con[2] = (block>>16) & 255;
13     con[3] = (block>>24) & 255;
14     con+=4;
15   }
16 }
step1B8
 1 void PForDecompressor::step1B10()
 2 {
 3   unsigned l=(item_num_+2)/3;
 4   unsigned i,block;
 5   unsigned *con=res_;
 6   for(i=0;i<l;i++)
 7   {
 8     block=*(data_++);
 9     data_length_--;
10     con[0] = block & 1023;
11     con[1] = (block>>10) & 1023;
12     con[2] = (block>>20) & 1023;
13     con+=3;
14   }
15 }
step1B10
 1 void PForDecompressor::step1B16()
 2 {
 3   unsigned l=(item_num_+1)/2;
 4   unsigned i,block;
 5   unsigned *con=res_;
 6   for(i=0;i<l;i++)
 7   {
 8     block=*(data_++);
 9     data_length_--;
10     con[0] = block & ((1<<16)-1);
11     con[1] = block>>16;
12     con+=2;
13   }
14 }
step1B16
1 void PForDecompressor::step1Ex()
2 {
3   cerr<<"Invalid b value"<<endl;
4 }
step1Ex

 

 1 void PForDecompressor::step2A2(unsigned b)
 2 {
 3   unsigned block1,block2;
 4   while(data_length_ > 0)
 5   {
 6     block1 = *(data_++);
 7     data_length_--;
 8     res_[block1 & 255]+=((block1>>8) & 65535)<<b;
 9     if(data_length_ == 0) break;
10     block2 = *(data_++);
11     data_length_--;
12     res_[block1>>24]+=(block2 & 65535)<<b;
13     if(data_length_ == 0) break;
14     block1 = *(data_++);
15     data_length_--;
16     res_[(block2>>16) & 255]+=((block2>>24) + ((block1 & 255)<<8))<<b;
17     res_[(block1>>8) & 255]+=(block1>>16)<<b;
18   }
19 }
step2A2
 1 void PForDecompressor::step2A3(unsigned b)
 2 {
 3   unsigned block;
 4   while(data_length_ > 0)
 5   {
 6     block= *(data_++);
 7     data_length_--;
 8     res_[block & 255]+=(block>>8)<<b;
 9   }
10 }
step2A3
 1 void PForDecompressor::step2A4(unsigned b)
 2 {
 3   unsigned block1,block2;
 4   while(true)
 5   {
 6     if(data_length_<=1) break;
 7     block1 = *(data_++);
 8     block2 = *(data_++);
 9     data_length_ -= 2;
10     res_[block1 & 255]+=( (block1>>8) + ((block2 & 255)<<24) )<<b;
11     if(data_length_ == 0) break;
12     block1 = *(data_++);
13     data_length_--;
14     res_[(block2>>8) & 255]+=( (block2>>16) + ((block1 && 65535)<<16) )<<b;
15     if(data_length_ == 0) break;
16     block2 = *(data_++);
17     data_length_--;
18     res_[(block1>>16) & 255]+=( (block1>>24) + ((block2 && 16777215)<<8) )<<b;
19     if(data_length_ == 0) break;
20     block1 = *(data_++);
21     data_length_--;
22     res_[block2>>24]+=block1;
23   }
24 }
step2A4

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM