1 概述
在允許一定誤判率的大數據量下的查找是否存在問題上可以使用布隆過濾器,詳情上篇文章。布隆過濾器在工程應用方面已經比較成熟了,上一篇文章中,談到了布隆過濾器的一些缺點,比如不支持刪除操作、查詢效率弱,因為多個隨機哈希函數探測的是bit數組中多個不同的點,所以會導致低CPU緩存命中率。
針對此2014年的一篇文章《Cuckoo Filter:Better Than Bloom》基於布谷鳥哈希算法提出了布谷鳥過濾器,不過看文章的名字有點碰瓷的感覺了,這篇文章解決了布隆過濾器存在的問題。
布谷鳥過濾器用更低的空間開銷解決了布隆過濾器不能刪除元素的問題,做到了更好的效果,具體的
- It supports adding and removing items dynamically;
- It provides higher lookup performance than traditional Bloom filters, even when close to full (e.g., 95% space utilized);
- It is easier to implement than alternatives such as the quotient filter;
- It uses less space than Bloom filters in many practical applications, if the target false positive rate is less than 3%
- 支持動態的添加和刪除元素
- 提供了比傳統布隆過濾器更高的查找性能,即使在接近滿的情況下(比如空間利用率達到 95% 的時候)
- 比起商過濾器它更容易實現
- 如果要求誤判率低於3%,它比布隆過濾器有更低的空間開銷
2 布谷鳥哈希
布谷鳥哈希是2001 年由Rasmus Pagh 和Flemming Friche Rodler 提出。本質上來說它為解決哈希沖突提供了另一種策略,利用較少計算換取了較大空間。它具有占用空間小、查詢迅速等特性。名稱源於采取了一種和布谷鳥一樣的養娃方法
布谷鳥交配后,雌性布谷鳥就准備產蛋了,但它卻不會自己築巢。它會來到像知更鳥、刺嘴鶯等那些比它小的鳥類的巢中,移走原來的那窩蛋中的一個,用自己的蛋來取而代之。相對於它的體形來說,它的蛋是偏小的,而且蛋上的斑紋同它混入的其他鳥的蛋也非常相似,所以不易被分辨出來。如果不是這樣,它的蛋肯定會被扔出去。
是一種鳩占鵲巢的策略,最原始的布谷鳥哈希方法是使用兩個哈希函數對一個key
進行哈希,得到桶中的兩個位置,此時
- 如果兩個位置都為為空則將
key
隨機存入其中一個位置 - 如果只有一個位置為空則存入為空的位置
- 如果都不為空,則隨機踢出一個元素,踢出的元素再重新計算哈希找到相應的位置
當然假如存在絕對的空間不足,那老是踢出也不是辦法,所以一般會設置一個踢出閾值,如果在某次插入行為過程中連續踢出超過閾值,則進行擴容。
3 布谷鳥過濾器
上圖(a)(b)展示了一個基本的布谷鳥哈希表的插入操作,是由一個桶數組組成,每個插入項都有由散列函數h1(x)和h2(x)確定的兩個候選桶,具體操作上文中已經描述,此處不再贅述。
而基本的布谷鳥過濾器也是由兩個或者多個哈希函數構成,布谷鳥過濾器的布谷鳥哈希表的基本單位稱為條目(entry)。 每個條目存儲一個指紋(fingerprint),指紋指的是使用一個哈希函數生成的n位比特位,n的具體大小由所能接受的誤判率來設置,論文中的例子使用的是8bits的指紋大小。
哈希表由一個桶數組組成,其中一個桶可以有多個條目(比如上述圖c中有四個條目)。而每個桶中有四個指紋位置,意味着一次哈希計算后布谷鳥有四個“巢“可用,而且四個巢是連續位置,可以更好的利用cpu高速緩存。也就是說每個桶的大小是4*8bits。
3.1 插入
布谷鳥過濾器的插入是重點,與朴素的布谷鳥哈希不同,布谷鳥過濾器采取了兩個並不獨立的哈希函數,具體的
\(i_1i_2\)即計算出來兩個桶的索引,其中第一個桶的索引是通過某個哈希函數計算出來,第二個是使用第一個索引和指紋的哈希做了一個異或操作,進行異或操作的好處是,因為異或操作的特性:同為0不同為1,且0和任何數異或是這個數的本身。那么\(i_1\)也可以通過\(i_2\)和指紋異或來計算。 換句話說,在桶中遷走一個鍵,我們直接用當前桶的索引\(i\)和存儲在桶中的指紋計算它的備用桶。
具體的指紋是通過哈希函數取一定量的比特位
為什么不直接用索引1和指紋做異或操作,關於這個問題文中給了解釋,因為指紋一般只是key映射出來的少量bit位置,那么假如不進行哈希操作,當指紋的比特位與整個桶數組相比很小時,那么備用位置使用“i⊕指紋”,將被放置到離桶\(i_1\)很近的位置,比如使用八位的指紋大小,最多只能改變\(i_1\)的低八位,所以也就是兩個候選通的位置最多相差256,不利於均勻分配。
3.2 查找
布谷鳥過濾器的查找過程很簡單,給定一個項x,算法首先根據上述插入公式,計算x的指紋和兩個候選桶。然后讀取這兩個桶:如果兩個桶中的任何現有指紋匹配,則布谷鳥過濾器返回true,否則過濾器返回false。此時,只要不發生桶溢出,就可以確保沒有假陰性。
3.3 刪除
標准布隆過濾器不能刪除,因此刪除單個項需要重建整個過濾器,而計數布隆過濾器需要更多的空間。布谷鳥過濾器就像計數布隆過濾器,可以通過從哈希表刪除相應的指紋刪除插入的項,其他具有類似刪除過程的過濾器比布谷鳥過濾器更復雜。
具體刪除的過程也很簡單,檢查給定項的兩個候選桶;如果任何桶中的指紋匹配,則從該桶中刪除匹配指紋的一份副本。
4 布谷鳥過濾器不足以及性能、參數分析
4.1 缺點
- 刪除不完美,存在誤刪的概率。刪除的時候知識刪除了一份指紋副本,並不能確定此指紋副本是要刪除的key的指紋。同時這個問題也導致了假陽性的情況。
- 插入復雜度比較高。隨着插入元素的增多,復雜度會越來越高,因為存在桶滿,踢出的操作,所以需要重新計算,但綜合來講復雜度還是常數級別。
- 存儲空間的大小必須為2的指數的限制讓空間效率打了折扣。
- 同一個元素最多插入kb次,(k指哈希函數的個數,b指的桶中能裝指紋的個數也可以說是桶的尺寸大小)如果布谷鳥過濾器支持刪除,則必須存儲同一項的多個副本。 插入同一項kb+1次將導致插入失敗。 這類似於計數布隆過濾器,其中重復插入會導致計數器溢出。
4.2 不同過濾器比較
上圖是布谷鳥過濾器其他過濾器比較,假陽性率與每個元素的空間成本。對於低假陽性率(低於3%),布谷鳥過濾器比空間優化的布隆過濾器每個元素需要更少的存儲空間。
布谷鳥過濾器有一個負載閾值。 在達到最大可行負載因子后,插入不再穩定,並且越來越有可能失敗,因此哈希表必須擴容才能存儲更多的項。 而對於布隆過濾器來說可以繼續將新項,不過是以增加假陽性率為代價。 為了保持相同的目標假陽性率,布隆過濾器也必須擴容。
4.3 桶的尺寸
桶的尺寸是指每個桶能放的指紋個數,保持布谷鳥過濾器的總大小(桶數組)不變,但改變桶的大小(上述例子使用的是大小為4)會導致兩個后果:
(1) 較大的桶可以提高表的利用率(即b越大假陽性率越大) ,使用k=2個哈希函數時,當桶大小b=1(即直接映射哈希表)時,負載因子α為50%,但使用桶大小b=2、4或8時則分別會增加到84%、95%和98%。
(2) 較大的桶需要較長的指紋才能保持相同的假陽性率(即b越大f越大)。 使用較大的桶時,每次查找都會檢查更多的條目,從而有更大的概率產生指紋沖突。
所以要基於以上尋找一個最合適的桶大小
上圖是,在不同的不同桶大小下(b=2,4,8),每個項的均攤空間成本與測量的假陽性率。文章的作者對此作了實驗,基於上述的結果,空間最優桶大小取決於目標假陽性率ϵ:當ϵ>0.002時,每桶有兩個條目比每桶使用四個條目產生的結果略好,當ϵ減小到0.00001<ϵ≤0.002時,桶的大小選取4可以最小化空間。
另外在大多數應用情況下,選擇兩個哈希函數,桶的大小選擇4,能夠達到最佳或接近最佳的空間效率的假陽性率。
5 實現
原文是CPP實現
https://github.com/efficient/cuckoofilter
像原文中說的那樣,限定了哈希函數個數以及桶的尺寸大小為4,可以適用於大多數情況。
github上有很多優秀的實現,其中不乏自由度更高的優化操作,可以參考。