最近在作性能優化相關的事情,其中涉及到了BloomFilter,因而對BloomFilter總結了下,本文組織結構以下:javascript
本文同步發佈在我的博客oserror.com。java
首先,簡單來看下BloomFilter是作什麼的?c++
A Bloom filter is a space-efficient probabilistic data structure, conceived by Burton Howard Bloom in 1970, that is used to test whether an element is a member of a set. False positive matches are possible, but false negatives are not, thus a Bloom filter has a 100% recall rate. In other words, a query returns either "possibly in set" or "definitely not in set".git
上述描述引自維基百科,特色總結爲以下:github
其次,爲何須要BloomFilter?算法
經常使用的數據結構,如hashmap,set,bit array都能用來測試一個元素是否存在於一個集合中,相對於這些數據結構,BloomFilter有什麼方面的優點呢?api
固然,BloomFilter也有它的劣勢,以下:數組
最後,以一個例子具體描述使用BloomFilter的場景,以及在此場景下,BloomFilter的優點和劣勢。性能優化
一組元素存在於磁盤中,數據量特別大,應用程序但願在元素不存在的時候儘可能不讀磁盤,此時,能夠在內存中構建這些磁盤數據的BloomFilter,對於一次讀數據的狀況,分爲如下幾種狀況:微信
若是使用hashmap或者set的數據結構,狀況以下:
假設應用不讀盤邏輯的開銷爲C1,走讀盤邏輯的開銷爲C2,那麼,BloomFilter和hashmap的開銷爲
Cost(BloomFilter) = P1 * C1 + (P2 + P3) * C2
Cost(HashMap) = (P1 + P2) * C1 + P3 * C2;
Delta = Cost(BloomFilter) - Cost(HashMap)
= P2 * (C2 - C1)複製代碼
所以,BloomFilter至關於以增長P2 * (C2 - C1)
的時間開銷,來得到相對於hashmap而言更少的空間開銷。
既然P2是影響BloomFilter性能開銷的主要因素,那麼BloomFilter設計時如何下降機率P2(即false positive probability)呢?,接下來的BloomFilter的原理將回答這個問題。
BloomFilter一般採用bit array實現,假設其bit總數爲m,初始化時m個bit都被置成0。
BloomFilter中插入一個元素,會使用k個hash函數,來計算出k個在bit array中的位置,而後,將bit array中這些位置的bit都置爲1。
以一個例子,來講明添加的過程,這裏,假設m=19,k=2,以下:
如上圖,插入了兩個元素,X和Y,X的兩次hash取模後的值分別爲4,9,所以,4和9位被置成1;Y的兩次hash取模後的值分別爲14和19,所以,14和19位被置成1。
BloomFilter中查找一個元素,會使用和插入過程當中相同的k個hash函數,取模後,取出每一個bit對應的值,若是全部bit都爲1,則返回元素可能存在,不然,返回元素不存在。
爲何bit所有爲1時,是表示元素可能存在呢?
仍是以上圖的例子說明,若是要查找的元素是X,k個hash函數計算後,取出的bit都是1,此時,X自己也是存在的;假如,要查找另外一個元素Z,其hash計算出來的位置爲9,14,此時,BloomFilter認爲此元素存在,可是,Z其實是不存在的,此現象稱爲false positive。
最後,BloomFilter中不容許有刪除操做,由於刪除後,可能會形成原來存在的元素返回不存在,這個是不容許的,仍是以一個例子說明:
上圖中,剛開始時,有元素X,Y和Z,其hash的bit如圖中所示,當刪除X後,會把bit 4和9置成0,這同時會形成查詢Z時,報不存在的問題,這對於BloomFilter來說是不能容忍的,由於它要麼返回絕對不存在,要麼返回可能存在。
放到以前的磁盤讀數據的例子來說,若是刪除了元素X,致使應用讀取Z時也會返回記錄不存在,這是不符合預期的。
BloomFilter中不容許刪除的機制會致使其中的無效元素可能會愈來愈多,即實際已經在磁盤刪除中的元素,但在bloomfilter中還認爲可能存在,這會形成愈來愈多的false positive,在實際使用中,通常會廢棄原來的BloomFilter,從新構建一個新的BloomFilter。
在實際使用BloomFilter時,通常會關注false positive probability,由於這和額外開銷相關。實際的使用中,指望能給定一個false positive probability和將要插入的元素數量,能計算出分配多少的存儲空間較合適。
假設BloomFilter中元素總bit數量爲m,插入的元素個數爲n,hash函數的個數爲k,false positive probability記作p,它們之間有以下關係(具體推導過程請參考維基百科):
若是須要最小化false positive probability,則k的取值以下
k = m * ln2 / n; 公式一複製代碼
而p的取值,和m,n又有以下關係
m = - n * lnp / (ln2) ^ 2 公式二複製代碼
把公式一代入公式二,得出給定n和p,k的取值應該爲
k = -lnp / ln2複製代碼
最後,也一樣能夠計算出m。
基礎的數據結構以下:
template<typename T>
class BloomFilter
{
public:
BloomFilter(const int32_t n, const double false_positive_p);
void insert(const T &key);
bool key_may_match(const T &key);
private:
std::vector<char> bits_;
int32_t k_;
int32_t m_;
int32_t n_;
double p_;
};複製代碼
其中bits_是用vector
整個BloomFilter包含三個操做:
根據BloomFilter原理一節中的方法進行計算,代碼以下:
template<typename T>
BloomFilter<T>::BloomFilter(const int32_t n, const double false_positive_p)
: bits_(), k_(0), m_(0), n_(n), p_(false_positive_p)
{
k_ = static_cast<int32_t>(-std::log(p_) / std::log(2));
m_ = static_cast<int32_t>(k_ * n * 1.0 / std::log(2));
bits_.resize((m_ + 7) / 8, 0);
}複製代碼
這裏開始實現的時候犯了個低級的錯誤,一開始用的是bits_.reserve
,致使BloomFilter的false positive probability很是高,緣由是reserve方法只分配內存,並不進行初始化。
即設置每一個hash函數計算出來的bit爲1,代碼以下
template<typename T>
void BloomFilter<T>::insert(const T &key)
{
uint32_t hash_val = 0xbc9f1d34;
for (int i = 0; i < k_; ++i) {
hash_val = key.hash(hash_val);
const uint32_t bit_pos = hash_val % m_;
bits_[bit_pos/8] |= 1 << (bit_pos % 8);
}
}複製代碼
即計算每一個hash函數對應的bit的值,若是全爲1,則返回存在;不然,返回不存在。
template<typename T>
bool BloomFilter<T>::key_may_match(const T &key)
{
uint32_t hash_val = 0xbc9f1d34;
for (int i = 0; i < k_; ++i) {
hash_val = key.hash(hash_val);
const uint32_t bit_pos = hash_val % m_;
if ((bits_[bit_pos/8] & (1 << (bit_pos % 8))) == 0) {
return false;
}
}
return true;
}複製代碼
下面進行了一組測試,設置指望的false positive probability爲0.1,模擬key從10000增加到100000的場景,觀察真實的false positive probability的狀況:
key_nums_=10000 expected false positive rate=0.1 real false positive rate=0.1252
key_nums_=20000 expected false positive rate=0.1 real false positive rate=0.1252
key_nums_=30000 expected false positive rate=0.1 real false positive rate=0.1257
key_nums_=40000 expected false positive rate=0.1 real false positive rate=0.1211
key_nums_=50000 expected false positive rate=0.1 real false positive rate=0.1277
key_nums_=60000 expected false positive rate=0.1 real false positive rate=0.1263
key_nums_=70000 expected false positive rate=0.1 real false positive rate=0.126
key_nums_=80000 expected false positive rate=0.1 real false positive rate=0.1219
key_nums_=90000 expected false positive rate=0.1 real false positive rate=0.1265
key_nums_=100000 expected false positive rate=0.1 real false positive rate=0.1327複製代碼
因爲實現的時候,會對k進行取整,根據取整後的結果(k=3),計算出來的理論值是0.1250,能夠,看出實際測出來的值和理論值差異不大。
前面實現的版本中,屢次調用了hash_func函數,這對於計算比較長的字符串的hash的開銷是比較大的,爲了模擬這種場景,插入1000w行的數據,使用perf top來抓取其性能數據,結果以下:
如上圖,除了生成數據的函數外,佔用CPU最高的就屬於hash_func了,佔用了13%的CPU。
分析以前的代碼能夠知道,insert和key_may_match時,都會屢次調用hash_func,這個開銷是比較大的。
leveldb和維基百科中都有提到,根據以前的研究,能夠採用兩次hash的方式來替代上述的屢次的計算,基本的思路以下:
template<typename T>
void BloomFilter<T>::insert2(const T &key)
{
uint32_t hash_val = key.hash(0xbc9f1d34);
const uint32_t delta = (hash_val >> 17) | (hash_val << 15);
for (int i = 0; i < k_; ++i) {
const uint32_t bit_pos = hash_val % m_;
bits_[bit_pos/8] |= 1 << (bit_pos % 8);
hash_val += delta;
}
}複製代碼
即先用一般的hash函數計算一次,而後,使用移位操做計算一次,最後,k次計算的時候,不斷累加兩次的結果。
通過優化後,性能數據圖以下:
和以前性能圖對比發現,hash_func的CPU使用率已經減小到4%了。
對比完性能以後,咱們還須要對比hash函數按照如此優化後,false positive probability的變化狀況:
before_opt
key_nums_=10000 expected false positive rate=0.1 real false positive rate=0.1252
key_nums_=20000 expected false positive rate=0.1 real false positive rate=0.1252
key_nums_=30000 expected false positive rate=0.1 real false positive rate=0.1257
key_nums_=40000 expected false positive rate=0.1 real false positive rate=0.1211
key_nums_=50000 expected false positive rate=0.1 real false positive rate=0.1277
key_nums_=60000 expected false positive rate=0.1 real false positive rate=0.1263
key_nums_=70000 expected false positive rate=0.1 real false positive rate=0.126
key_nums_=80000 expected false positive rate=0.1 real false positive rate=0.1219
key_nums_=90000 expected false positive rate=0.1 real false positive rate=0.1265
key_nums_=100000 expected false positive rate=0.1 real false positive rate=0.1327
after_opt
key_nums_=10000 expected false positive rate=0.1 real false positive rate=0.1244
key_nums_=20000 expected false positive rate=0.1 real false positive rate=0.1327
key_nums_=30000 expected false positive rate=0.1 real false positive rate=0.134
key_nums_=40000 expected false positive rate=0.1 real false positive rate=0.1389
key_nums_=50000 expected false positive rate=0.1 real false positive rate=0.1342
key_nums_=60000 expected false positive rate=0.1 real false positive rate=0.1548
key_nums_=70000 expected false positive rate=0.1 real false positive rate=0.141
key_nums_=80000 expected false positive rate=0.1 real false positive rate=0.1536
key_nums_=90000 expected false positive rate=0.1 real false positive rate=0.1517
key_nums_=100000 expected false positive rate=0.1 real false positive rate=0.154複製代碼
優化後,最大的false positive probability增加了2%左右,這個能夠增長k來彌補,由於,優化後的hash算法,在k增加時,帶來的開銷相對來說不大。
備註,本節採用perf抓取性能數據圖,命令以下
sudo perf record -a --call-graph dwarf -p 9125 sleep 60
sudo perf report -g graph複製代碼
本文的代碼在(bloomfilter.cpp)(github.com/Charles0429…
PS:
本博客更新會在第一時間推送到微信公衆號,歡迎你們關注。