1

引言

陈硕老师曾经说过大部分情况下自己实现一个内存池是没有必要的,我们要相信malloc的作者。但是leveldb的作者也不是凡夫俗子(去google jeff dean 网 上 有 真 相)。leveldb为什么自己实现一个内存池我相信一定有它的道理,虽然这篇文章不大可能道出作者的精妙想法,但还是试着解释清楚程序的基本思路和框架。

基本思想

为什么不用new或者malloc分配内存?

  • 频繁地分配内存,造成内存碎片化
  • new完忘记delete,造成内存泄漏

leveldb的做法:先向系统申请一块大的内存,外部需要申请内存时,先把已有的内存块分配给用户,如果不够用则再申请一块大的内存。当内存池对象析构时,分配的内存均被释放,保证了内存不会泄漏。

源码分析

/util/arena.h/util/arena.cc 包含了内存池的定义和实现。以下是Arena类的声明。

声明

public:
  Arena();//构造函数
  ~Arena();//析构函数
  char* Allocate(size_t bytes);//分配"bytes"大小的空间并返回指向该地址的指针
  char* AllocateAligned(size_t bytes);//分配“byte”大小的空间,保证字对齐。
  size_t MemoryUsage() const//返回已分配内存的大小(不精确)
private:
  char* AllocateFallback(size_t bytes);//当前内存块的剩余容量不足以支撑所申请内存时,调用此函数
  char* AllocateNewBlock(size_t block_bytes);//使用malloc分配一个新的内存块

  // Allocation state
  char* alloc_ptr_;//指向当前内存块剩余空间的地址
  size_t alloc_bytes_remaining_;//当前内存块剩余容量

  std::vector<char*> blocks_;//存放指向内存块地址的指针,用于析构函数中释放内存

  size_t blocks_memory_;//已分配内存大小

  Arena(const Arena&);//只申明不实现,防止编译器隐式拷贝,详情可自行查询google c++ style
  void operator=(const Arena&);//同上

实现

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;

如果申请内存大于块大小的1/4,我们将单独为其申请一块大小正好的新内存块,防止内存块的其他空间被浪费。否则我们将申请固定大小的内存块,并修改状态。

char* Arena::AllocateAligned(size_t bytes) {
  const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
//指针大小,由编译器产生的目标平台的指令集决定。譬如说x86就是4,x64就是8 by vczh
  assert((align & (align-1)) == 0);   //确保指针大小为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;
}

reinterpret_cast为暴力转换,请慎用。
最后看一下MemoryUsage的实现,

size_t MemoryUsage() const {
    return blocks_memory_ + blocks_.capacity() * sizeof(char*);
  }

返回已分配内存块的大小以及指向内存块指针的大小,忽略了Arena其他数据成员的大小,如alloc_ptr_,alloc_bytes_remaining_ 等。

总结

不推荐在实际工程中自己实现一个内存池,因为内置的malloc在一般情况下的性能是非常高的,自己实现的内存池不能保证bugfree并且性能也不一定比内置的malloc强,leveldb的arena实现仅供学习和参考。我猜想由于leveldb需要特殊的字对齐内存分配功能,所以作者用arena实现了定制的内存池。
To be continued

@See You SpaceCowboy


Jefffrey
101 声望7 粉丝

亚洲倒数第二程序员