Bloom Filter 主流Hash雜湊演算法介紹
所以在不考慮抵禦攻擊的成本下,非加密雜湊大約要快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 << 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位又允許非對其讀取的架構。