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;
}
posted @ 2022-11-27 01:43  Hugh_Locke  阅读(327)  评论(0编辑  收藏  举报