leveldb:Arena内存池


 

Arena

  Arenaleveldb 项目里面使用的轻量级的内存池对象leveldb 用这个对象来管理内存的分配,简化了 newdelete 的调用。Arena的代码并不多,但也集成了google工程师巧妙的思维和想法,接下来就慢慢揭开Arena神秘的面纱。

Arena源码链接:

头文件

源码实现

 

Arena内存管理模型

  先来看看Arena中比较重要的成员变量

//每一个block的大小为4096字节
static const int kBlockSize = 4096;

//当前block未分配内存的起始地址,也是已分配内存的结束地址
char* alloc_ptr_;	
//当前block剩余的未分配内存大小
size_t alloc_bytes_remaining_;	

//管理所有block的vector,类似于STL库中deque源码实现中的map的作用
std::vector<char*> blocks_;	
//Arena已经分配的总内存
std::atomic<size_t> memory_usage_;	

  了解了Arena的核心成员变量后,我们可以画图说明Arena的内存管理模型以及每个成员变量的意义:
Arena内存管理模型

  Arena所做的事为申请内存和分配内存,申请内存由new操作完成,而分配内存主要体现在alloc_ptr_指针后移以及alloc_bytes_remaining_减少的操作上(当然也有其他情况,后文中将提到)。

 

Arena的构造与析构实现

//构造函数
Arena::Arena()
    : alloc_ptr_(nullptr), alloc_bytes_remaining_(0), memory_usage_(0) {}

//析构函数,释放blocks_中的每块内存
Arena::~Arena() {
  for (size_t i = 0; i < blocks_.size(); i++) {
    delete[] blocks_[i];
  }
}

 

Arena提供的接口

  Arena提供的三个public接口如下:

// 基本的内存分配函数
char* Allocate(size_t bytes);

// 字节对齐分配内存
char* AllocateAligned(size_t bytes);

// 返回已分配内存的总大小
size_t MemoryUsage() const {
    return memory_usage_.load(std::memory_order_relaxed);
}

 

Allocate

  基本内存分配函数,根据传入的byte值分配相应的内存,分配内存的规则如下:

  1. bytes小于当前块剩余内存大小,则直接分配,并对alloc_ptralloc_bytes_remaining做响应调整
  2. bytes大于当前块剩余内存大小,且小于kBlockSize / 4 = 1024,则申请一个新的block,将alloc_ptr的值移到新的block上,并将alloc_bytes_remaining的值设为kBlockSize - bytes
  3. bytes大于kBlockSize / 4 = 1024,则直接分配一块大小为bytes的内存,alloc_ptralloc_bytes_remaining不做修改。

  以上步骤的第一种情况是直接在Allocate函数中调用内存申请函数AllocateNewBlock实现,而第二和第三种情况的判断及内存分配则是在Allocate函数中调用AllocateFallback实现。一下为Allocate函数的源码:

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;
  }
  //上述第二及第三种情况,调用AllocateFallback函数实现
  return AllocateFallback(bytes);
}

 

AllocateFallback

  此函数就是用来处理上述的第二或第三中情况。基本流程就是判断传入的bytes的大小以执行不同的操作,这样操作的理由是可以减少分配内存的次数,使得不管我们申请多大的内存,Arena都只需要为我们分配一次内存(而不会将一块内存分配在两个block中导致分配两次内存)。

源码如下:

char* Arena::AllocateFallback(size_t bytes) {
  //上述第三种情况,直接分配大小为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;
  }
    
  //上述第二种情况,重新创建一个block,并将相关指针和值转换为新block上的状态
  // 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;
}

​   笔者保留了源码中的英文注释,因为这两段英文注释非常清楚地解释了Google的工程师为什么要区分第二、第三种情况:由于我们每次重新创建一个block时,原来block中剩余的内存空间实际上是使用不到的,这样不可避免会造成内存的浪费情况,若我们所需分配的内存大小大于kBlockSize / 4时并不创建新的block而是直接分配一块相应大小的内存块时,这样可以保证每一个block上浪费的内存块的大小始终小于kBlockSize / 4;

​   若不理解为什么这样的设计每一块浪费的内存始终小于kBlockSize / 4,大家可尝试动手举一个浪费的内存块大于kBlockSize / 4的例子,这样也许更容易理解。

​   理解了Arena中基本的内存分配方式,我们再来看看Arena在内存申请是做了哪些事。

 

AllocateNewBlock

​   AllocateNewBlock是Arena向系统申请内存的函数。这个函数运用了new运算符向系统申请内存,并同时更新了Arena的已分配内存数memory_usage_

​   需要注意的是,因为存在分配内存块大于kBlockSize / 4的特殊处理方法,在AllocateNewBlock函数中并不会对alloc_ptr_指针以及alloc_bytes_remaining_做调整,这个指针以及值的调整操作应当是在调用AllocateNewBlock函数处决定是否执行。

​   AllocateNewBlock函数的流程简单来说就是:申请内存 --> 将新的内存块加入blocks_中 --> 修改memory_usage_的值 --> 返回所申请的内存块的指针。函数并不难理解,源代码如下:

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;
}

​  在了解了Arena的内存分配基本原理之后,我们最后来看看Arena按对齐方式分配内存的实现原理:AllocateAligned函数。

 

AllocateAligned

​  在这个函数中,首先定义需要对齐的字节数,按照机器的void*的大小来对齐,若超过8字节,则最多按照8字节对齐:

const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;

​ ​  接下来,计算当前分配的内存 % 对齐字节数,也就是计算出按照当前对齐方式分配内存的话比对齐界限多了多少字节,并将这个值存放在current_mod中,假设align = 8,则current_mod的意义如图所示:
current_mod
​  得到这个偏差之后,我们就能轻易得到若需要对齐还差多少字节了,我们将这个差值保存在needed中,上图例中,needed = 8 - current_mod = 5

​ ​  接下来我们将传入的bytesneeded相加,就能的得到我们对齐之后总共需要分配多少字节内存了,同样我们按照最开始所提到的三个策略进行分配进行分配,但是需要注意的是,若 对齐总共需要分配的内存 < 剩余的块内存,那么调用AllocateFallback(bytes)函数,此处传入的参数为bytes而不是对气后的总值,是因为使用AllocateFallback函数向系统索要的内存永远是字节对齐的(AllocateFallback always returned aligned memory)。

​ ​  老规矩贴出函数源码:

char* Arena::AllocateAligned(size_t bytes) {
  //设置对齐的字节数,按照机器的 void* 的大小来对齐,若超过8字节,则最多按照8字节对齐。
  const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
  
  // 字节对齐必须是 2 的次幂
  static_assert((align & (align - 1)) == 0,
                "Pointer size should be a power of 2");
    
  //A & (B - 1) = A % B
  //通过以上公式求出current_mod的值,并强制转换为uinitptr_t类型
  //uinitptr_t为当前机器指针大小
  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);
    
  //求出与对齐量的偏差值
  size_t slop = (current_mod == 0 ? 0 : align - current_mod);
    
  //needed为实际应当分配的内存
  size_t needed = bytes + slop;
  char* result;
    
  //这里的if-else操作可见以上分析
  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;
}

 

总结

​ 相比于Nginx的内存池实现方式,Arena的策略并没有区分大块内存和小块内存。但同样Arena并没有给出类似于freedelete之类的函数,相关的操作在Arena的析构函数中执行。所以通过Arena来管理程序中各个模块的内存使用,可以有效防止内存泄露的问题。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值