levelDB 代码阅读笔记 02 comparator
Overview
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};
42
比较让人迷惑的应该是 FindShortestSeparator和 FindShortSuccessor 函数。 其实可以简单理解成为了节省存储,而对key做了一定修改。 这个实现是不确定的,甚至完全什么都不做也是合法的。
下面我们主要看一下levelDB中两个实现了这个接口的sub class
BytewiseComparatorImpl
默认的comparator,按照byte的字典序比较大小
主体实现
需要注意 FindShortestSeparator和 FindShortSuccessor 的实现并非唯一的
1
2class BytewiseComparatorImpl : public Comparator {
3 public:
4 BytewiseComparatorImpl() = default;
5
6 const char* Name() const override { return "leveldb.BytewiseComparator"; }
7
8 int Compare(const Slice& a, const Slice& b) const override {
9 return a.compare(b);
10 }
11
12 void FindShortestSeparator(std::string* start,
13 const Slice& limit) const override {
14 // Find length of common prefix
15 size_t min_length = std::min(start->size(), limit.size());
16 size_t diff_index = 0;
17 // diff_index是一个不同的index
18 while ((diff_index < min_length) &&
19 ((*start)[diff_index] == limit[diff_index])) {
20 diff_index++;
21 }
22
23 if (diff_index >= min_length) {
24 // 没有不同的字符,说明一个串是另一个串的前缀,不需要做任何操作
25 // Do not shorten if one string is a prefix of the other
26 } else {
27 uint8_t diff_byte = static_cast<uint8_t>((*start)[diff_index]);
28 if (diff_byte < static_cast<uint8_t>(0xff) &&
29 diff_byte + 1 < static_cast<uint8_t>(limit[diff_index])) {
30 (*start)[diff_index]++;
31 start->resize(diff_index + 1);
32 assert(Compare(*start, limit) < 0);
33 }
34 }
35 }
36
37 void FindShortSuccessor(std::string* key) const override {
38 // Find first character that can be incremented
39 size_t n = key->size();
40 for (size_t i = 0; i < n; i++) {
41 const uint8_t byte = (*key)[i];
42 if (byte != static_cast<uint8_t>(0xff)) {
43 (*key)[i] = byte + 1;
44 key->resize(i + 1);
45 return;
46 }
47 }
48 // *key is a run of 0xffs. Leave it alone.
49 }
50};
51
52
Scott Meyers's singleton
比较值得一提的可能是这个单例的实现
1
2const Comparator* BytewiseComparator() {
3 static NoDestructor<BytewiseComparatorImpl> singleton;
4 return singleton.get();
5}
Scott Meyers's singleton 的实现虽然已经人尽皆知了,不妨来罗嗦几句讲一下重点
1
2struct singleton_t
3{
4
5 static
6 singleton_t &
7 instance()
8 {
9 static singleton_t s;
10 return s;
11 } // instance
12
13 singleton_t(const singleton_t &) = delete;
14 singleton_t & operator = (const singleton_t &) = delete;
15
16private:
17
18 singleton_t() {}
19 ~singleton_t() {}
20
21
22
要注意的有三点:
- static function在第一次被访问时创建
- static local variable在第一次被执行时创建,在整个程序结束时销毁。 不被访问就不会创建,实现了"lazy loading"
- c++11之后,标准保证了local static variable的创建是thread-safe的
但是 NoDestructor 是个什么东西呢
NoDestructor
1
2// Wraps an instance whose destructor is never called.
3//
4// This is intended for use with function-level static variables.
5template <typename InstanceType>
6class NoDestructor {
7 public:
8 template <typename... ConstructorArgTypes>
9 explicit NoDestructor(ConstructorArgTypes&&... constructor_args) {
10 static_assert(sizeof(instance_storage_) >= sizeof(InstanceType),
11 "instance_storage_ is not large enough to hold the instance");
12 static_assert(
13 alignof(decltype(instance_storage_)) >= alignof(InstanceType),
14 "instance_storage_ does not meet the instance's alignment requirement");
15 new (&instance_storage_)
16 InstanceType(std::forward<ConstructorArgTypes>(constructor_args)...);
17 }
18
19 ~NoDestructor() = default;
20
21 NoDestructor(const NoDestructor&) = delete;
22 NoDestructor& operator=(const NoDestructor&) = delete;
23
24 InstanceType* get() {
25 return reinterpret_cast<InstanceType*>(&instance_storage_);
26 }
27
28 private:
29
30 typename std::aligned_storage<sizeof(InstanceType),
31 alignof(InstanceType)>::type instance_storage_;
32};
33
34
35
变长参数与std::forward
首先是看到构造函数的变长模板和变长参数列表的用法,可以支持传入任意数量个任意类型的参数,这个用法在std::make_unique之类的函数非常常见 并且一般会用std::forward将参数的形式按照传入的形式往下层传,参考 cppreference:make_unique
1
2template<class T, class... Args>
3std::enable_if_t<!std::is_array<T>::value, std::unique_ptr<T>>
4make_unique(Args&&... args)
5{
6 return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
7}
placement new
可以看到这里也很特别
1
2 new (&instance_storage_)
3 InstanceType(std::forward<ConstructorArgTypes>(constructor_args)...);
4
看到new会自然想到这个内存是内配在heap上的,然而实际上却未必。 这个语法叫做placement new 可以支持在一个预先分配好的memory(不一定是在heap上,可以是在stack上)来在指定的内存中构造对象
1
2// within any block scope...
3{
4 alignas(T) unsigned char buf[sizeof(T)];
5 // Statically allocate the storage with automatic storage duration
6 // which is large enough for any object of type `T`.
7 T* tptr = new(buf) T; // Construct a `T` object, placing it directly into your
8 // pre-allocated storage at memory address `buf`.
9 tptr->~T(); // You must **manually** call the object's destructor
10 // if its side effects is depended by the program.
11} // Leaving this block scope automatically deallocates `buf`.
12
13
需要注意的是,与普通的new和delete对应不同。 使用placement new构造出的对象,需要显式调用析构函数。 参考is there a placemnet delete?的说明
那么问题来了,为啥要有placemnet new? 有啥好处? 我理解主要有两点:
- 类似内存池,每次构造新对象的时候不需要发生malloc,速度更快
- 分配内存可能发生异常。 将分配内存的代码与构造对象的部分分开,方便处理
std::aligned_storage
这个其实就是与placement new配合使用的,只分配内存,不构造对象。 参考 aligned_storage
两个模板参数分别是对象至少的size,以及最小的aligment要求。
The type defined by std::aligned_storage<>::type can be used to create uninitialized memory blocks suitable to hold the objects of given type, optionally aligned stricter than their natural alignment requirement, for example on a cache or page boundary. As with any other uninitialized storage, the objects are created using placement new and destroyed with explicit destructor calls.
why use NoDestructor
代码看懂了,但还有最重要的一点是,为什么要用这个?
这个实现最初貌似出现在chromium的开发者讨论中 Proposal: use base::NoDestructor for leaky non-POD globals (and get rid of base::Singleton?)
chromium中的实现在这里
我看这个代码最大的疑问是,就算不用NoDestructor,一个static local variable也不会进行析构(在程序退出时才进行),那么为啥要用NoDestructor呢?
Variables declared at block scope with the specifier static or thread_local (since C++11) have static or thread (since C++11) storage duration but are initialized the first time control passes through their declaration (unless their initialization is zero- or constant-initialization, which can be performed before the block is first entered). On all further calls, the declaration is skipped.
The destructor for a block-scope static variable is called at program exit, but only if the initialization took place successfully.
看了chromium中的注释才发现,是被名字误解了
NoDestructor的目的并不是提供一种不用析构的能力,而是提供了其他优势,又恰好不需要析构。所以用了这个名字
同时,我之前理解是不对的。
static local variable是在程序退出时析构,而NoDestructor是程序退出时也不会析构。
这两个不是一回事,可以从如下这段测试中看出区别:
1
2// Copyright (c) 2018 The LevelDB Authors. All rights reserved.
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file. See the AUTHORS file for names of contributors.
5
6#include "util/no_destructor.h"
7
8#include <cstdint>
9#include <cstdlib>
10#include <utility>
11#include <iostream>
12
13#include "gtest/gtest.h"
14
15namespace leveldb {
16
17namespace {
18
19struct DoNotDestruct {
20 public:
21 DoNotDestruct(uint32_t a, uint64_t b) : a(a), b(b) {}
22 ~DoNotDestruct() {
23 std::cout<<" dtor"<<std::endl;
24 std::abort(); }
25
26 // Used to check constructor argument forwarding.
27 uint32_t a;
28 uint64_t b;
29};
30
31constexpr const uint32_t kGoldenA = 0xdeadbeef;
32constexpr const uint64_t kGoldenB = 0xaabbccddeeffaabb;
33
34} // namespace
35
36TEST(NoDestructorTest, StackInstance) {
37 NoDestructor<DoNotDestruct> instance(kGoldenA, kGoldenB);
38 ASSERT_EQ(kGoldenA, instance.get()->a);
39 ASSERT_EQ(kGoldenB, instance.get()->b);
40}
41
42TEST(NoDestructorTest, StaticInstance) {
43 static NoDestructor<DoNotDestruct> instance(kGoldenA, kGoldenB);
44 ASSERT_EQ(kGoldenA, instance.get()->a);
45 ASSERT_EQ(kGoldenB, instance.get()->b);
46}
47
48TEST(NoDestructorTest, StaticIns) {
49 static DoNotDestruct instance(kGoldenA, kGoldenB);
50 ASSERT_EQ(kGoldenA, instance.a);
51 ASSERT_EQ(kGoldenB, instance.b);
52}
53
54} // namespace leveldb
55
56
输出的结果:
1
2root@KUANZEREN-PC4:~/workspace/leveldb/build# ./leveldb_tests --gtest_filter=*NoDestructor*
3Running main() from /root/workspace/leveldb/third_party/googletest/googletest/src/gtest_main.cc
4Note: Google Test filter = *NoDestructor*
5[==========] Running 3 tests from 1 test suite.
6[----------] Global test environment set-up.
7[----------] 3 tests from NoDestructorTest
8[ RUN ] NoDestructorTest.StackInstance
9[ OK ] NoDestructorTest.StackInstance (0 ms)
10[ RUN ] NoDestructorTest.StaticInstance
11[ OK ] NoDestructorTest.StaticInstance (0 ms)
12[ RUN ] NoDestructorTest.StaticIns
13[ OK ] NoDestructorTest.StaticIns (0 ms)
14[----------] 3 tests from NoDestructorTest (0 ms total)
15
16[----------] Global test environment tear-down
17[==========] 3 tests from 1 test suite ran. (0 ms total)
18[ PASSED ] 3 tests.
19 dtor
20Aborted
21
那NoDesctructor有什么优势呢?
- 对比local static,NoDesctructor
是inline stored的,不存在malloc和指针解引用,效率更高 - 对比chromium中的 base::LazyInstance,支持对构造函数中传入参数
经常提到的一个词是"inline storage / stored inline"我理解在这个语境下说的是storage_duration中的 static storage duration (存疑)
参考:
其他 Comparator
ReverseKeyComparator
测试用的Comparator,将key颠倒顺序后使用bytewiseComparator比较大小 也就是 "abc"变为"cba"
CountComparator
测试用。 其他Comparator的一个包装器,加了个atomic的计数器,用来benchmark
InternalKeyComparator
这个值得讲一下
首先看到Compare函数
1
2int InternalKeyComparator::Compare(const Slice& akey, const Slice& bkey) const {
3 // Order by:
4 // increasing user key (according to user-supplied comparator)
5 // decreasing sequence number
6 // decreasing type (though sequence# should be enough to disambiguate)
7 int r = user_comparator_->Compare(ExtractUserKey(akey), ExtractUserKey(bkey));
8 if (r == 0) {
9
10 const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8);
11 const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8);
12 if (anum > bnum) {
13 r = -1;
14 } else if (anum < bnum) {
15 r = +1;
16 }
17 }
18 return r;
19}
最初让人比较疑惑的是,看起来只有两个比较逻辑:
- 按照user key升序排列(根据传入的comparator)
- 根据DecodeFixex64得到的值(就是把Slice中的最后8byte转换为一个uint64_t的值) 降序排列
为什么注释里写了三个比较逻辑? 这里的代码是看不出来的,带着这个疑问,我们去看下DecodeFixex64的值的意义是什么呢
我们首先看下InternalKey是什么
1
2// Modules in this directory should keep internal keys wrapped inside
3// the following class instead of plain strings so that we do not
4// incorrectly use string comparisons instead of an InternalKeyComparator.
5class InternalKey {
6 private:
7 std::string rep_;
8
9 public:
10 InternalKey() {} // Leave rep_ as empty to indicate it is invalid
11 InternalKey(const Slice& user_key, SequenceNumber s, ValueType t) {
12 AppendInternalKey(&rep_, ParsedInternalKey(user_key, s, t));
13 }
14
15 bool DecodeFrom(const Slice& s) {
16 rep_.assign(s.data(), s.size());
17 return !rep_.empty();
18 }
19
20 Slice Encode() const {
21 assert(!rep_.empty());
22 return rep_;
23 }
24
25 Slice user_key() const { return ExtractUserKey(rep_); }
26
27 void SetFrom(const ParsedInternalKey& p) {
28 rep_.clear();
29 AppendInternalKey(&rep_, p);
30 }
31
32 void Clear() { rep_.clear(); }
33
34 std::string DebugString() const;
35};
36
37
38
39
其中最重要的是 AppendInternalKey的部分,里面涵盖了InternalKey的表示 其实就是由如下三部分组成了一个string | userkey | sequence | type |
1
2static uint64_t PackSequenceAndType(uint64_t seq, ValueType t) {
3 assert(seq <= kMaxSequenceNumber);
4 assert(t <= kValueTypeForSeek);
5 // 高位是sequence number,低位是type
6 return (seq << 8) | t;
7}
8
9void AppendInternalKey(std::string* result, const ParsedInternalKey& key) {
10 // 把user key这个slice 往result后拼接
11 result->append(key.user_key.data(), key.user_key.size());
12 PutFixed64(result, PackSequenceAndType(key.sequence, key.type));
13 // Result 中分了三部分
14 // user_key , sequence, type. 后两部分合成了一个uint64_t
15}
16
要注意的是sequence和type共同放在一个uint64_t中,高56bit为sequence为sequence,低8 bit为type
1
2// Value types encoded as the last component of internal keys.
3// DO NOT CHANGE THESE ENUM VALUES: they are embedded in the on-disk
4// data structures.
5enum ValueType { kTypeDeletion = 0x0, kTypeValue = 0x1 };
6// kValueTypeForSeek defines the ValueType that should be passed when
7// constructing a ParsedInternalKey object for seeking to a particular
8// sequence number (since we sort sequence numbers in decreasing order
9// and the value type is embedded as the low 8 bits in the sequence
10// number in internal keys, we need to use the highest-numbered
11// ValueType, not the lowest).
12static const ValueType kValueTypeForSeek = kTypeValue;
13
14typedef uint64_t SequenceNumber;
15
16// We leave eight bits empty at the bottom so a type and sequence#
17// can be packed together into 64-bits.
18// 在真正pack的时候会做一个移位
19static const SequenceNumber kMaxSequenceNumber = ((0x1ull << 56) - 1);
20
21struct ParsedInternalKey {
22 Slice user_key;
23 SequenceNumber sequence;
24 ValueType type;
25
26 ParsedInternalKey() {} // Intentionally left uninitialized (for speed)
27 ParsedInternalKey(const Slice& u, const SequenceNumber& seq, ValueType t)
28 : user_key(u), sequence(seq), type(t) {}
29 std::string DebugString() const;
30};
31
这也就解释了最初的疑问。 uint64_t实际上暗含了先比较sequence,再比较type的比较逻辑
1
2 const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8);
3 const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8);
4 if (anum > bnum) {
5 r = -1;
6 } else if (anum < bnum) {
7 r = +1;
8 }
9
感觉把几个信息合并到一起表示的技巧在levelDB的实现中比较常见, 有点类似状压DP里面的技巧
Posts in this Series
- [施工中] levelDB 代码阅读笔记 06 iterator
- levelDB 代码阅读笔记 05 arena
- levelDB 代码阅读笔记 04 filter
- levelDB 代码阅读笔记 03 cache
- levelDB 代码阅读笔记 02 comparator
- levelDB 代码阅读笔记 01 db.h
- murmurhash源码分析
- 内存屏障(Memory Barriers)
- 文本相似度判断-simhash算法学习笔记