LevelDB(v1.3) 源码阅读之 Arena(内存管理器)

时间:2021-11-21 16:43:20

LevelDB(v1.3) 源码阅读系列使用 LevelDB v1.3 版本的代码,可以通过如下方式下载并切换到 v1.3 版本的代码:

$ git clone https://github.com/google/leveldb.git
$ cd leveldb
$ git checkout -b v1.3 v1.3

本文涉及的代码文件为:

util/arena.h
util/arena.cc

一、简介

Arena 是一个简单的内存分配器。通过一个 Arena 对象,可以进行一些内存分配操作。Arena 对象会将所有分配的内存都记录在案,在 Arena 对象析构的时候一起释放所有记录在案的已分配内存。

Arena 默认分配的内存大小是 4096KB,也就是 4K。每当有一个内存分配请求分配 X Bytes 的内存的时候,Arena 的分配原则如下:

  1. 如果当前分配的内存块中剩余的内存能够满足该分配请求,也就是说大于等于 X,那么就在当前的内存块中分配 X Bytes 给用户,否则:
  2. 如果 X 大于默认的内存块大小的 25%,也就是说 X > 1KB,那么就单独分配大小恰好是 X Bytes 的内存给用户,否则:
  3. 新分配一块大小为 4KB 的内存,在里面分配 X Bytes 给用户(也就是说,上一块内存中可能有些内存没有被分配给用户就被 Arena 抛弃了,这里会造成小于 1KB 的内存浪费)

这里需要注意两个数字:4KB1/4

  • 1/4 是一个经验值:是减少内存碎片和提高内存分配器性能的一个 trade off;
  • Linux 内核的 page size 为 4KB

可以通过如下命令查看 page size:

$ getconf PAGESIZE

此外,Arena 还提供了分配地址对齐的内存块的接口:调用这个接口,用户会得到他想要的 X Bytes 的内存块,该内存块的起始地址是 8 的倍数:形如 0x_______00x_______8,如果在分配内存的时候发现当前的地址并不满足这种形式,那就多分配点内存用来填补不够的 Byte(最多浪费 7 个 Bytes),之后的分配原则就和上面讲的三大原则一致了。这样的内存块的带来的优势是 CPU 寻址速度快:

  • 在 32 位机器上,地址总线是 32 bits 的,指针的 size 为 4 Bytes;64 位机器上,地址总线是 64 bits 的,指针的 size 为 8 Bytes。
  • CPU 取内存中的数据并不是一个 Byte 一个 Byte 的取,在 32 位机器上,一次取的数量是 4 Bytes,64 位机器上一次取 8 Bytes。

值得一提的是,每次我们从堆中 new 一块内存的时候,操作系统返回的该内存块的首地址是一定能被 8 整除(地址对齐)的。

关于内存分配部分,这里都讲的差不多了,代码我就贴一点点吧。

成员变量

// Allocation state
char* alloc_ptr_;                  // 指向当前内存块中剩余未分配内存块的首部
size_t alloc_bytes_remaining_;     // 保存当前内存块中剩余的可分配出去的内存大小

// Array of new[] allocated memory blocks
std::vector<char*> blocks_;        // 保存所有分配的内存块

// Bytes of memory in blocks allocated so far
size_t blocks_memory_;             // 保存该 Arena 对象总共申请内存的大小

对外接口

Arena();
~Arena();
char* Allocate(size_t bytes);
char* AllocateAligned(size_t bytes);
size_t MemoryUsage() const {
  return blocks_memory_ + blocks_.capacity() * sizeof(char*);
}

其中,Allocate(size_t) 用来分配非地址对齐的内存,AllocateAligned(size_t) 用来分配地址对齐的内存

值得一提的是,Arena 对象不允许拷贝操作,为了完成这个,它把以一个 Arena 对象作为参数的构造函数声明称私有的,并重载 '=' 为私有

关于分配内存的三条原则,可以在 Allocate(size_t) 的实现和一个私有成员函数 char* AllocateFallback(size_t bytes); 中查看:

static const int kBlockSize = 4096;

inline char* Arena::Allocate(size_t bytes) {
  // The semantics of what to return are a bit messy if we allow
  // 0-byte allocations, so we disallow them here (we don't need
  // them for our internal use).
  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::AllocateFallback(size_t bytes) {
  if (bytes > kBlockSize / 4) {
    // Object is more than a quarter of our block size.  Allocate it separately
    // to avoid wasting too much space in leftover bytes.
    char* result = AllocateNewBlock(bytes);
    return result;
  }

  // We waste the remaining space in the current block.
  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_memory_ += block_bytes;
  blocks_.push_back(result);
  return result;
}

代码中的注释已经很好的解释其行为了,我就不多嘴了。