LevelDB源码剖析(1) Arena内存管理
1. 背景
对于数据库来说,内存的分配非常重要,当我们使用C++默认的内存分配方式 malloc/free或者new/delete的时候,如果遇到很小的键值对时,每次调用的平均开销就会比较大,同时会产生很多内存碎片。
由于在MemTable中经常会遇到需要为较小键值对分配内存的原因,LevelDB在MemTable使用了自己的内存管理,其为每个MemTable都绑定了一个Arena来管理内存。
- 在MemTable以外的地方都使用了malloc/free, 因为他们申请较大的内存或者并不会频繁的申请内存
2. 原理
2.1 内存分配
内存分配中有一种常见的思想,是使用new预分配一块比较大的内存,需要使用小内存的时候,就从这块大内存里继续分配,这种分配只需要移动指针并且更新变量即可,相比于调用malloc会更加高效。
Arena就是基于这种思想进行内存管理,如下图所示,Arena将内存分为多个块,其中块分为4KB的基本块和超过1KB的大块,当需要分配内存时,会首先使用当前预申请的基本块中的内存分配,当需要分配的内存大于当前基本块中的数据时,就会申请新的空间,此时Arena会进行判断,如果需要分配的内存大于1KB,则为这块内存单独申请空间。如果小于1KB,则申请一个新的基本块,并从新的基本块为其分配空间,此时旧的基本块不再使用,多出来的空间就会会被浪费。
2.2 内存销毁
Arena不支持单独释放某个块,只能销毁整个Arena。这个和他的使用方式有关,对于MemTable来说,对于内存只有插入键值对的操作,没有删除的操作,所以不需要释放某一块内存,而是当MemTable的数据都Dump到SSTable中的时候,才会对整个Arena进行释放。
3. 源码解析
3.1 Arena.h
class Arena {
public:
Arena();
~Arena();
//加上=delete意味禁止编译器自动生成,Arena负责内存分配,每次使用都应该独立初始化,应当禁止发生拷贝
Arena(const Arena&) = delete;
Arena& operator=(const Arena&) = delete;
char* Allocate(size_t bytes); // 请求分配bytes个字节的内存,返回分配到的内存的指针
char* AllocateAligned(size_t bytes); //按照字节对齐来分配内存
// 返回内存的使用量
size_t MemoryUsage() const {
return memory_usage_.load(std::memory_order_relaxed); //memory_order_relaxed是atomic的一种memory ordering
}
private:
char* AllocateFallback(size_t bytes); //分配一块超出当前余量的内存
char* AllocateNewBlock(size_t block_bytes); //申请一个新的块并分配内存
char* alloc_ptr_; //指向当前块的第一个free字节
size_t alloc_bytes_remaining_; //当前块的余量
std::vector<char*> blocks_; //指向内存块的数组
std::atomic<size_t> memory_usage_; //当前Arena的内存用量,其中atomic代表这是一个原子类型的变量
};
头文件通过注释可以获知每个函数以及成员变量的作用,特别的,其中memory_usage是一个原子类型的变量,而其获取这个变量的值的时候使用了memory_usage_.load(std::memory_order_relaxed), 是在获取的时候使用了一种ordering,让编译器优化生成的代码,从而提高性能。
3.2 Arena.cc
Allocate
Allocate是Arena暴露对外的内存分配入口,当需要分配内存时,首先检查申请量是否小于alloc_bytes_remaining_(即当前内存块剩余的内存),如果小于则直接分配,否则触发AllocateFallback
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);
}
AllocateFallback
AllocateFallback 是当前内存块不足时Arena执行的内存分配逻辑。
char* Arena::AllocateFallback(size_t bytes) {
if (bytes > kBlockSize / 4) {
//当申请的内存超过块大小的1/4时,会为其单独分配一块内存,这是为了防止申请浪费过多的申请内存。毕竟如果直接申请新的内存块,原来内存块的剩余空间就浪费了,这保证了4KB的基本内存块最多只会浪费1KB的空间。
char* result = AllocateNewBlock(bytes);
return result;
}
//当申请的内存块小于1/4时,会重新申请一块新的4KB基本内存块并将其放入。
alloc_ptr_ = AllocateNewBlock(kBlockSize);
alloc_bytes_remaining_ = kBlockSize; //kBlokSize是一个全局静态常量,大小为4096,表示一个基本块的大小
char* result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
AllocateNewBlock
AllocateNewBlock负责通过new向内存申请一块新的空间,作为新的基本内存块或大块,其逻辑就是申请内存、指针加入blocks、增加统计指标、返回指针
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;
}
AllocateAligned
AllocateAligned负责申请一块对齐的内存。
char* Arena::AllocateAligned(size_t bytes) {
//align表示需要对齐的字节数,使用机器的void*的大小来对齐,最多8字节
const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
//判断对齐字节数是否是2的次幂,只有2的幂次的数据x & (x - 1)为0
static_assert((align & (align - 1)) == 0,
"Pointer size should be a power of 2");
//这里用到了一个公式:x & (y - 1) = x % y
//所以current_mod实际上是在计算alloc_ptr_与对齐字节的偏差量
size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);
//在知道了偏差之后,slop指的是需要向后多申请多少个字节,needed指的是将多申请的字节与需要分配的字节加起来,就是实际需要分配的字节
size_t slop = (current_mod == 0 ? 0 : align - current_mod);
size_t needed = bytes + slop;
//最终只要按照needed来分配内存,并将返回的指针变为alloc_ptr_向后移动slop个字节的位置,就可以得到一块字节对齐的内存
char* result;
if (needed <= alloc_bytes_remaining_) {
result = alloc_ptr_ + slop;
alloc_ptr_ += needed;
alloc_bytes_remaining_ -= needed;
} else {
//因为AllocateFallback是需要申请一块新的内存(无论是基本块还是大块)来分配的,所以新申请的内存总是字节对齐的,就不用再使用slop和needed来做偏移了
result = AllocateFallback(bytes);
}
//校验最终结果是否内存对齐
assert((reinterpret_cast<uintptr_t>(result) & (align - 1)) == 0);
return result;
}