1. 程式人生 > >Bloom Filter 主流Hash雜湊演算法介紹

Bloom Filter 主流Hash雜湊演算法介紹

     在雲端計算中的資料儲存方面,雜湊對提高查詢效率起著很大的作用,雜湊函式是將字串或者數字作為輸入,通過計算輸出一個整數,理想的雜湊函式輸出非常均勻分佈在可能的輸出域,特別是當輸入非常相似的時候。不同於加密雜湊函式,這些函式不是為防止攻擊者找出碰撞而設計的。加密雜湊函式有這個特性但是要慢的多: SHA-1大約為0.09 bytes/cycle,而最新的非加密雜湊函式的速度大約為3 bytes/cycle。

     所以在不考慮抵禦攻擊的成本下,非加密雜湊大約要快33倍,非加密雜湊函式用的最多的地方是hash table。網路上提到雜湊的演算法很多,資料紛雜,本文僅僅對各位博主的博文進行整合,旨在給大家一個清晰的關於雜湊函式的簡要介紹。

雜湊函式經歷了多次改進,本文將改進分為三個階段,並且給出了各個階段的標誌性演算法的介紹和具體方法:

第一代:Bob Jenkins' Functions

     Bob Jenkins在1997年他在《 Dr. Dobbs Journal》雜誌上發表了一片關於雜湊函式的文章《A hash function for hash Table lookup》,這篇文章自從發表以後現在網上有更多的擴充套件內容。這篇文章中,Bob廣泛收錄了很多已有的雜湊函式,這其中也包括了他自己所謂的“lookup2”。隨後在2006年,Bob釋出了lookup3,由於它即快速(Bob自稱,0.5 bytes/cycle)又無嚴重缺陷,在這篇文章中我把它認為是第一個“現代”雜湊函式。這裡列出他的第一個版本的程式碼,其主要思想如下:

    uint32_tjenkins_one_at_a_time_hash(unsigned char *key, size_t key_len){
        uint32_t hash = 0;
        size_t i;

        for (i = 0; i < key_len; i++) {
            hash += key
;
            hash += (hash << 10);
            hash ^= (hash >> 6);
        }
        hash += (hash << 3);

        hash ^= (hash >> 11);
        hash += (hash << 15);
        return hash;
    }

        而Thomas Wang在Jenkins的基礎上,針對固定整數輸入做了相應的Hash演算法,其64位版本的 Hash演算法如下:

    uint64_thash(uint64_t key) {
        key = (~key) + (key << 21); // key = (key << 21) - key - 1;
        key = key ^ (key >> 24);
        key = (key + (key << 3)) + (key << 8); // key * 265
        key = key ^ (key >> 14);
        key = (key + (key << 2)) + (key << 4); // key * 21
        key = key ^ (key >> 28);
        key = key + (key << 31);
        return key;
    
}

     總的來說,Jenkins很好的實現了雜湊的均勻分佈,但是相對來說比較耗時,它有兩個特性,1是具有雪崩性,既更改輸入引數的任何一位都將帶來一半以上的位發生變化,2是具有可逆性,但是在逆運算時,它非常耗時,如果想了解如何進行逆變換,請參考文獻2.

第二代: MurmurHash

     Austin Appleby在2008年釋出了一個新的雜湊函式-MurmurHash。其最新版本大約是lookup3速度的2倍(大約為1byte/cycl e),它有32位和64位兩個版本。32位版本只使用32位數學函式並給出一個32位的雜湊值,而64位版本使用了64位的數學函式,並給出64位雜湊值。根據Austin的分析,MurmurHash具有優異的效能,雖然Bob Jenkins 在《Dr. Dobbs article》雜誌上聲稱“我預測[MurmurHash ]比起lookup3要弱,但是我不知道具體值,因為我還沒測試過它”。MurmurHash能夠迅速走紅得益於其出色的速度和統計特性。MurMur的具體實現如下:

unsigned long long MurmurHash64B ( const void * key, int len, unsigned int seed ){
   
const unsigned int m = 0x5bd1e995;
    
const int r = 24;
   
unsigned int h1 = seed ^ len;
    unsigned int h2 = 0;
 
    const unsigned int * data = (const unsigned int *)key;
 
    while(len >= 8){
        unsigned int k1 = *data++;
        k1 *= m; k1 ^= k1 >> r; k1 *= m;
        h1 *= m; h1 ^= k1;
        len -= 4;
 
        unsigned int k2 = *data++;
        k2 *= m; k2 ^= k2 >> r; k2 *= m;
        h2 *= m; h2 ^= k2;
        len -= 4;
    }
 
    if(len >= 4){
        unsigned int k1 = *data++;
        k1 *= m; k1 ^= k1 >> r; k1 *= m;
        h1 *= m; h1 ^= k1;
        len -= 4;
    }
 
    switch(len){
        case 3: h2 ^= ((unsigned char*)data)[2] << 16;
        case 2: h2 ^= ((unsigned char*)data)[1] << 8;
        case 1: h2 ^= ((unsigned char*)data)[0];
        h2 *= m;
    };
 
     h1 ^= h2 >> 18; h1 *= m;
     h2 ^= h1 >> 22; h2 *= m;
     h1 ^= h2 >> 17; h1 *= m;
     h2 ^= h1 >> 19; h2 *= m;
 
     unsigned long long h = h1;
     h = (h << 32) | h2; 
     return h;
}

       MurMur經常用在分散式環境中,比如hadoop,其特點是高效快速,但是缺點是分佈不是很均勻,這也可以理解,畢竟不能既讓馬兒跑,又讓馬兒不吃草...

第三代: CityHash 和 SpookyHash

       2011年,釋出了兩個雜湊函式,相對於MurmurHash ,它們都進行了改善,這主要應歸功於更高的指令級並行機制。Google釋出了CityHash(由Geoff Pike 和Jyrki Alakuijala編寫),Bob Jenkins釋出了他自己的一個新雜湊函式SpookyHash(這樣命名是因為它是在萬聖節釋出的)。它們都擁有2倍於MurmurHash的速度,但他們都只使用了64位數學函式而沒有32位版本,並且CityHash的速度取決於CRC32 指令,目前為SSE 4.2(Intel Nehalem及以後版本)。SpookyHash給出128位輸出,而CityHash有64位,128位以及256位的幾個變種。由於兩者的程式碼較長,這裡給出原始碼的連線如下:

雜湊函式哪家強?

       文章中所提到的所有雜湊函式從統計學角度來看已經足夠好。需要考慮的一個因素是CityHash/SpookyHash的輸出超過了64位,但是對於一個32位的hash table來說這輸出太多了。其他應用可能會用到128或256位輸出。

       如果你用的是32位機器,MurmurHash看起來是最明顯的贏家,因為它是唯一一個快於lookup3的32位原生版本。32位機器很可能可以編譯並執行City和Spooky,但我預計它們的執行速度和在64位機器上執行的速度比起來要慢的多,畢竟32位機器需要模擬64位的數學運算。在64位機器上,由於沒有更深層次的基準,也很難說哪種演算法是最好的。對比City我更傾向於Spooky,因為City的執行速度需要依賴於CRC32指令,畢竟這種環境並不是任何機器上都有的。 

       另一個需要考慮的是對齊和非對齊的訪問。Murmur雜湊(不像City或者Spooky)是一個僅能進行對齊讀取的變種,因為在很多架構上非對齊的讀取會崩潰或者返回錯誤的資料(非對齊的讀取操作在C中是未定義的行為)。

       City和Spooky都強調使用memcpy()把輸入資料複製到對齊的儲存結構中;Spooky使用一次memcpy()操作一個塊(如果ALLOW_UNALIGNED_READS未定義),City使用一次memcpy()操作一個整型!在可以處理非對稱讀取的機器上(像x86和x86-64),memcpy將被優化,但我在我的小ARM上做了一個測試,發現如下:如果你需要32位或者僅僅是對齊讀取的話,Murmur雜湊看起來依舊是最好的選擇。

       City雜湊和Spooky雜湊在x86-64上看起來更快,但我更傾向於認為它們是特定用於那個架構的,因為我不知道是否有其他既是64位又允許非對其讀取的架構。

參考博文: