leveldb 阅读笔记(1) 内存分配器 Arena

  内存管理对于任何程序都是很重要的一块,leveldb自己也实现了一个简单了内存分配器,而不是使用一些其他开源软件tcmalloc等,避免了对其他软件的依赖。

自己实现内存分配器有什么好处呢? 我认为主要有以下几点:

1. 内存池的主要作用是减少new  、 delete 等的调用次数,也就是减少系统调用的开销。

2. 减少内存碎片。

3. 方便内存使用统计和监控。

Arena 按块申请内存,每块大小为4k(这个值应该是和分页大小相关),然后使用vector保存这些块的首地址,模型如下:

成员变量与publlic调用接口:

class Arena {
 public: 

  // 返回分配好的内存块
  char* Allocate(size_t bytes);

  // 返回分配好的内存块,首地址满足字节对齐
  char* AllocateAligned(size_t bytes);

  // 已使用内存的估算大小(因为使用了stl的vector,精确大小不好确定)
  size_t MemoryUsage() const {
    return reinterpret_cast<uintptr_t>(memory_usage_.NoBarrier_Load());
  }

 private:

  // Allocation state
  char* alloc_ptr_;                //指向当前4k块使用进度
  size_t alloc_bytes_remaining_;        //当前4k块剩余大小

  // Array of new[] allocated memory blocks
  std::vector<char*> blocks_;          //记录所申请各块内存的首地址,方便回收

  // Total memory usage of the arena.
  port::AtomicPointer memory_usage_;      //已经使用内存大小的估算
}

内存分配策略:

1. 当需剩余的内存大小满足分配需求时,直接使用剩余的内存(之前一次性申请了一大块,还有些没用完)

   否则需要向系统重新申请一块。

2. 当前块剩余的内存大小不满足分配需求,并且需要分配的内存比较大时(>4096/4 = 1k),单独申请一块独立的内存。

3. 当前块剩余的内存不够并且新的分配需求不大于1k时, 另外申请一大块4k,从中取出部分返回给调用者,余下的供下次使用。

源码的注释中也说到了,上面第2点是为了避免过多的内存浪费,为什么这么做就能避免呢?  考虑一种情况:

假如当前块还剩余1k大小,分配需求是 1025 bytes > 1k, 不按上面的做法的话,就需要新申请一个4k块从中取出1025 bytes返回,然而这么做的话,上一块剩余的1k就再也不会被使用了,这就是浪费。 按上面的做法之前剩余的1k内存还可以继续使用。

因此这种做法避免了大块的浪费,然而仍有可能浪费1k之内的内存,为什么不把这个值设的很小呢?   那就和直接使用new差不多了,失去了内存分配器的原有意义,设置成这个值是一个权衡利弊的结果。

具体实现:

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_.push_back(result);
  memory_usage_.NoBarrier_Store(
      reinterpret_cast<void*>(MemoryUsage() + block_bytes + sizeof(char*)));
  return result;
}

 内存回收:

所申请的内存会随着,Arena对象生命的终结而被回收, 因此使用时需要保证所申请的内存不再使用了,然后才能析构Arena对象。

Arena::~Arena() {
  for (size_t i = 0; i < blocks_.size(); i++) {
    delete[] blocks_[i];
  }
}

此外Arena 还提供了一个保证字节对齐的方法:

char* Arena::AllocateAligned(size_t bytes) {
  const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;  // 按一个指针所占大小,与系统位数相关,字节对齐,最少为8
  assert((align & (align-1)) == 0);                  // 确保其大小是2的幂
  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);   // 相当于对align取模
  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 { // AllocateFallback always returned aligned memory result = AllocateFallback(bytes); } assert((reinterpret_cast<uintptr_t>(result) & (align-1)) == 0); return result; }

总结:

leveldb实现的内存分配器还是很简单的,有点简陋的感觉。相对于leveldb只用一个vector维护,c++ stl所实现的默认的内存分配器就精细多了,它按8、16、32、...... 字节大小做了多级管理,当前级不能再使用的内存还可以供下一级使用,基本很少有内存浪费,不过也因此带来了维护这个结构更高的复杂度,也需要额外保存更多的冗余信息。

原文地址:https://www.cnblogs.com/leogn/p/7602570.html