↓ 跳过正文
  1. Posts/

levelDB 代码阅读笔记 04 filter

·2552 字·6 分钟
108 - 这篇文章属于一个选集。

FilterPolicy接口
#

 1
 2class LEVELDB_EXPORT FilterPolicy {
 3 public:
 4  virtual ~FilterPolicy();
 5
 6  // Return the name of this policy.  Note that if the filter encoding
 7  // changes in an incompatible way, the name returned by this method
 8  // must be changed.  Otherwise, old incompatible filters may be
 9  // passed to methods of this type.
10  virtual const char* Name() const = 0;
11
12  // keys[0,n-1] contains a list of keys (potentially with duplicates)
13  // that are ordered according to the user supplied comparator.
14  // Append a filter that summarizes keys[0,n-1] to *dst.
15  //
16  // Warning: do not change the initial contents of *dst.  Instead,
17  // append the newly constructed filter to *dst.
18  virtual void CreateFilter(const Slice* keys, int n,
19                            std::string* dst) const = 0;
20
21  // "filter" contains the data appended by a preceding call to
22  // CreateFilter() on this class.  This method must return true if
23  // the key was in the list of keys passed to CreateFilter().
24  // This method may return true or false if the key was not on the
25  // list, but it should aim to return false with a high probability.
26  virtual bool KeyMayMatch(const Slice& key, const Slice& filter) const = 0;
27};

其中CreateFilter的含义是从n个key生成一个 std::string. 生成的std::string可以包含n个key的信息(类似于生成了一个全集) 从而后续判断某个key是否在其中。

bloomfilter
#

一个十分经典的filter 一般用于判断某个元素是否在一个集合中 如果返回false,则一定不存在。如果返回true,则大概率存在。返回true的情况存在一定的false positive.

基本原理
#

其核心原理有两部分:

  1. 通过k个hash function对元素进行映射。 类似于从高维空间映射到低维空间。不同的hash function映射到了不同的低维空间。 如果在每个低维空间,两个元素经过k个hash function的值都相同,那么这两个元素大概率相同。 如果任意一个hash function的值不同,那么这两个element一定不同。

  2. 每个元素存k个hash 值还是很占空间,怎么办?将所有的hash值放到同一个长度为m的bitmap上。 并认为如果一个元素不在集合中,那么大概率至少有一个bit不会被任何全集中的其他元素置为1.

注意,这里说的"经过hash function的值"实际上说的是 “hash(element) % m 的值”,其中m为bitmap的长度

高维空间映射到低维空间

有木有觉得和局部敏感HASH LSH很像?参考 simhash笔记 (simhash是用来处理文本相似的LSH算法,后面可以补一个LSH的笔记)

尤其是核心原理中的第一点,几乎是完全一样的。都是利用hash function来做映射从而减少信息。 最大的区别是LSH可以通过hash值的相似度来判断key的相似度,但是bloom filter中用的hash function是非LSH,只能判断相等或者不等。

false positive的来源
#

对于不在集合中的元素,有一定概率被误判为在集合中。

这一误判实际上有两方面原因

  1. hash function带来的误判。可能两个不同的元素的k组 hash(element) % m 得到的值是完全相同的。

  2. 将映射之后得到的值放到同一个bitmap上带来的误判。可能不存在于集合中的元素B算出的k个值恰好被若干个其他元素的结果置为了1. 从而带来误判

bloomfilter在levelDB中的实现
#

最佳k值
#

有几个值得说明 其一是构造函数中,hash function的个数为 bits_per_key * ln(2)

1
2  explicit BloomFilterPolicy(int bits_per_key) : bits_per_key_(bits_per_key) {
3    // We intentionally round down to reduce probing cost a little bit
4    k_ = static_cast<size_t>(bits_per_key * 0.69);  // 0.69 =~ ln(2)
5    if (k_ < 1) k_ = 1;
6    if (k_ > 30) k_ = 30;
7  }

首先要知道,hash function的个数k并不是越多越好。

  • 当k太小时,上一节所述的false positive的来源1会更容易发生
  • 当k太大时,上一节所述的false positive的来源2会更容易发生(因为每个元素占用了k个bit位,如果k比较大,很容易就把整个bitmap占满了)

那么k的值多少是比较合理的呢? 可以推导出k最好为 m/n * ln2. 其中m为bitmap的长度,n为key的个数。 m/n就是代码中的bits_per_key

最佳k值

如何得到k个hash function
#

k是一个变量,那么问题来了:上哪里找那么多不同的hash function去?

levelDB中采用了double hash的做法来生成k个不同的hash值

double hashing 本来是一种用于处理open addressing hash冲突的办法。 和一般的linear probing 或者 quadratic probing的方法不同,double hash每次probing的长度是由key经过另一个hash function计算出来的(因此称之为double hashing). 由于每次probing的step是由key决定的,从而避免了频繁重复的hash冲突。

而levelDB这里用double hashing生成k个不同的hash value. 计算成本非常低

具体来说,如下代码中的h是hash value1, delta是hash value2(没有用另一个hash function,而是简单交换前后两部分的bit来生成一个不同的hash value)。

 1
 2        uint32_t h = BloomHash(keys[i]);
 3      // kk: 这里每次+delta就相当于生成不同的hash值了
 4      // delta是另一个hash value,通过翻转左右两边得到
 5      const uint32_t delta = (h >> 17) | (h << 15);  // Rotate right 17 bits
 6						     // k 是hash函数的个数
 7      for (size_t j = 0; j < k_; j++) {
 8	      // kk: 这里就是把char* 当成bitset 使用
 9        const uint32_t bitpos = h % bits;
10        array[bitpos / 8] |= (1 << (bitpos % 8));
11        h += delta;
12      }
13    }

bitmap实现
#

leveldb中没有用std::bitset,而是用一个char数组。每个位置表示8个bit。

另外,最后生成的dst的结构为:| bitmap | … | k |,也即把hash function的个数k编码进了dst的末尾

 1    const size_t init_size = dst->size();
 2    dst->resize(init_size + bytes, 0);
 3    dst->push_back(static_cast<char>(k_));  // Remember # of probes in filter
 4    // kk: use char array as bitmap
 5    // each char has 8 position
 6    char* array = &(*dst)[init_size];
 7    for (int i = 0; i < n; i++) {
 8      // Use double-hashing to generate a sequence of hash values.
 9      // See analysis in [Kirsch,Mitzenmacher 2006].
10      uint32_t h = BloomHash(keys[i]);
11      // kk: 这里每次+delta就相当于生成不同的hash值了
12      // delta是另一个hash value,通过翻转左右两边得到
13      const uint32_t delta = (h >> 17) | (h << 15);  // Rotate right 17 bits
14						     // k 是hash函数的个数
15      for (size_t j = 0; j < k_; j++) {
16	      // kk: 这里就是把char* 当成bitset 使用
17        const uint32_t bitpos = h % bits;
18        array[bitpos / 8] |= (1 << (bitpos % 8));
19        h += delta;
20      }
21    }
22
23
24
25  // Use the encoded k so that we can read filters generated by
26    // bloom filters created using different parameters.
27    const size_t k = array[len - 1];
28    if (k > 30) {
29      // Reserved for potentially new encodings for short bloom filters.
30      // Consider it a match.
31      return true;
32    }
33
34    uint32_t h = BloomHash(key);
35    const uint32_t delta = (h >> 17) | (h << 15);  // Rotate right 17 bits
36    for (size_t j = 0; j < k; j++) {
37      const uint32_t bitpos = h % bits;
38      // 如果其中一个位置为0就返回false
39      if ((array[bitpos / 8] & (1 << (bitpos % 8))) == 0) return false;
40      h += delta;
41    }
42    return true;

一点思考
#

通常一个算法是要在空间复杂度和时间复杂度之间做取舍 但是bloom filter的空间和时间复杂度都很优秀,那么牺牲了什么呢? 牺牲了准确率

在一些场景下,的确不需要100%准确。从而给时空复杂度的优化留出了空间。

参考链接
#

108 - 这篇文章属于一个选集。

相关文章

levelDB 代码阅读笔记 02 comparator

·3968 字·8 分钟
levelDB是一个有序的KV存储,因此key的顺序是十分关键的 levelDB提供用户自己定义key顺序的能力 先看下comparator的接口 接口 include/leveldb/comparator.h # 1 2// A Comparator object provides a total order across slices that are 3// used as keys in an sstable or a database. A Comparator implementation 4// must be thread-safe since leveldb may invoke its methods concurrently 5// from multiple threads. 6class LEVELDB_EXPORT Comparator { 7 public: 8 virtual ~Comparator(); 9 10 // Three-way comparison. Returns value: 11 // < 0 iff "a" < "b", 12 // == 0 iff "a" == "b", 13 // > 0 iff "a" > "b" 14 virtual int Compare(const Slice& a, const Slice& b) const = 0; 15 16 // The name of the comparator. Used to check for comparator 17 // mismatches (i.e., a DB created with one comparator is 18 // accessed using a different comparator. 19 // 20 // The client of this package should switch to a new name whenever 21 // the comparator implementation changes in a way that will cause 22 // the relative ordering of any two keys to change. 23 // 24 // Names starting with "leveldb." are reserved and should not be used 25 // by any clients of this package. 26 virtual const char* Name() const = 0; 27 28 // Advanced functions: these are used to reduce the space requirements 29 // for internal data structures like index blocks. 30 31 // If *start < limit, changes *start to a short string in [start,limit). 32 // Simple comparator implementations may return with *start unchanged, 33 // i.e., an implementation of this method that does nothing is correct. 34 virtual void FindShortestSeparator(std::string* start, 35 const Slice& limit) const = 0; 36 37 // Changes *key to a short string >= *key. 38 // Simple comparator implementations may return with *key unchanged, 39 // i.e., an implementation of this method that does nothing is correct. 40 virtual void FindShortSuccessor(std::string* key) const = 0; 41}; 比较让人迷惑的应该是 FindShortestSeparator 和 FindShortSuccessor 函数。 其实可以简单理解成为了节省存储,而对key做了一定修改。 这个实现是不确定的,甚至完全什么都不做也是合法的。

levelDB 代码阅读笔记 01 db.h

·1551 字·4 分钟
背景 # 最近在做一个智能算力相关的项目,类似美团外卖广告智能算力的探索与实践 其中实现控制系统需要与数据库交互。虽然最后技术选型并没有使用到levelDB,但是想趁机把代码读了吧。

murmurhash源码分析

·2165 字·5 分钟
分析 levelDB 源码的时候遇到的,发现是一个广泛应用的 hash 算法,而且是纯 C 写的,于是找来了源码看。 MurmurHash 是一种非加密型哈希函数,适用于一般的哈希检索操作。[1][2][3]由Austin Appleby在2008年发明,[4][5] 并出现了多个变种,[6] 都已经发布到了公有领域(public domain)。与其它流行的哈希函数相比,对于规律性较强的key,MurmurHash的随机分布特征表现更良好。[7]

内存屏障(Memory Barriers)

·2114 字·5 分钟
起因是最近在看 levelDB 源码,其中 port 里的atomic_pointer.h 文件用到了内存屏障。。 于是来学习一下。 粗略地说下我自己的理解。 代码的顺序并不和执行的顺序完全对应,出于对效率的追求,CPU 和编译器会对指令进行重排,以期得到最大的执行效率。