levelDB的内存管理策略
levelDB的内存管理类Arena
源代码位置:utils/arena.cc utils/arena.h
对于数据库而言,内存管理策略是十分重要的,因为其正是需要为存入的数据分配内存,倘若直接使用new/delete这样每次插入数据都单独去分配内存,效率是很低的,同时由于每条插入的数据的大小并不一致,导致会出现许多的内存碎片。
对于levelDB这样一个billion级别的数据库,显然其不能采取这样的策略。
在levelDB中,由于只有MemTable
位于内存且需要频繁的插入,所以它最需要内存管理,levelDB中用于内存管理的类为Arena
,每个MemTable
都绑定一个Arena
。而其余的部分则直接使用new/delete,因为这些部分要么是不频繁,要么是块本身就很大。
Arena类
基本思想:先分配出一大块内存,然后当需要用到内存时,就在这一大块内存中移动指针,这样就能解决小块内存频繁调用new和内存碎片的问题,不过缺点是有一部分内存被浪费掉。
我们可以先来看一下arena类的头文件捏。从头文件中可以很明显的看到其用于内存分配的方法就是公有函数Allocate
和AllocateAligned
,区别是后者提供内存对齐,这对效率也有提升。
头文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
class Arena { public: Arena(); Arena(const Arena &) = delete; Arena &operator=(const Arena &) = delete; ~Arena();
char *Allocate(size_t bytes);
char *AllocateAligned(size_t bytes);
size_t MemoryUsage() const { return memory_usage_.load(std::memory_order_relaxed); }
private: char *AllocateFallback(size_t bytes); char *AllocateNewBlock(size_t block_bytes);
char *alloc_ptr_; size_t alloc_bytes_remaining_; std::vector<char *> blocks_; std::atomic<size_t> memory_usage_; };
|
头文件定义了Arena这个类,在类中只有3个公有方法,分别用于返回小内存、对齐的小内存、当前类占用的空间。
实现文件
分析完了头文件,我们接下来看一下它的cpp实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
|
static const int kBlockSize = 4096;
Arena::Arena() : alloc_ptr_(nullptr), alloc_bytes_remaining_(0), memory_usage_(0) {}
Arena::~Arena() { for (size_t i = 0; i < blocks_.size(); i++) { delete[] blocks_[i]; } }
inline char *Arena::Allocate(size_t bytes) { assert(bytes > 0); if (bytes <= alloc_bytes_remaining_) { char *result = alloc_ptr_; alloc_ptr_ += bytes; alloc_bytes_remaining_ -= bytes; return result; } return AllocateFallback(bytes); }
char *Arena::AllocateAligned(size_t bytes) { const int align = (sizeof(void *) > 8) ? sizeof(void *) : 8;
static_assert((align & (align - 1)) == 0, "Pointer size should be a power of 2");
size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);
size_t slop = (current_mod == 0 ? 0 : align - current_mod);
size_t needed = bytes + slop;
char *result;
if (needed <= alloc_bytes_remaining_) { result = alloc_ptr_ + slop; alloc_ptr_ += needed; alloc_bytes_remaining_ -= needed; } else { result = AllocateFallback(bytes); }
assert((reinterpret_cast<uintptr_t>(result) & (align - 1)) == 0); return result; }
char *Arena::AllocateFallback(size_t bytes) { if (bytes > kBlockSize / 4) { char *result = AllocateNewBlock(bytes); return result; }
alloc_ptr_ = AllocateNewBlock(kBlockSize);
alloc_bytes_remaining_ = kBlockSize; char *result = alloc_ptr_; alloc_ptr_ += bytes; alloc_bytes_remaining_ -= bytes; return result; }
char *Arena::AllocateNewBlock(size_t block_bytes) { char *result = new char[block_bytes]; blocks_.push_back(result); memory_usage_.fetch_add(block_bytes + sizeof(char *), std::memory_order_relaxed); return result; }
|
实际上总体来说思路如下:
1、当调用AllocateAligned
或Allocate
时,先判断当前分配内存块中剩余的内存是否有足够的内存去分配新的内存(以及计算内存对齐);如果足以容纳,则直接从当前内存块中提取内存作为所需内存使用。若是不足以容纳则调用AllocateFallback
去申请内存并分配。
2、在AllocateFallback
中,先判断需要申请的内存是否大于1K,若是大于1K,直接AllocateNewBlock
向系统申请足够的内存以供使用。若是小于1K,则调用AllocateNewBlock
分配一个块,并在这个块中分配需要的内存。
3、AllocateNewBlock
使用系统new操作符向系统申请内存,并更新类内参数。当所分配的内存为4kb时,会将这个申请的内存作为新的内存块(此时旧的内存块中即使还有内存未使用,也不会再拿来使用,因为alloc_ptr_会指向新申请的内存块)。然后再向新申请的内存块提取内存以供使用。
这里Level对于小于1K的内存申请才向内存池提取内存,主要是连续多次申请小的内存会容易导致内存碎片,影响系统的性能。并且多次的new和delete比较耗时(不断的构造和析构),会付出额外的空间和时间。
总结部分
Arena() |
构造函数 |
~Arena() |
析构函数 |
char* Allocate(size_t bytes) |
提供bytes大小的内存 |
char* AllocateAligned(size_t bytes) |
提供内存对齐的bytes大小的内存 |
size_t MemoryUsage() const |
返回当前申请的内存大小 |
char* AllocateFallback(size_t
bytes) |
当块中内存不够时向系统申请内存接口,由Allocate或AllocateAligned调用 |
char* AllocateNewBlock(size_t
block_bytes) |
系统new的包装函数,由AllocateFallback调用 |
alloc_ptr_ |
当前内存块中未使用空间的首地址 |
alloc_bytes_remaining_ |
当前内存块中剩余的空间 |
blocks_ |
new出来的内存的首地址组成的vector数组 |
memory_usage_ |
向系统已申请的内存大小 |
编程小技巧
a & (a - 1)
:去掉a在二进制表示中最右边的1
A & (B - 1) = A % B
:此公式仅在B是2的幂次方时成立