leveldb : Arena内存池
- Arena
- Arena内存管理模型
- Arena的构造与析构实现
- Arena提供的接口
- Allocate
- AllocateFallback
- AllocateNewBlock
- AllocateAligned
- 总结
Arena
??Arena
是 leveldb
项目里面使用的轻量级的内存池对象,leveldb
用这个对象来管理内存的分配,简化了 new
和 delete
的调用。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所做的事为申请内存和分配内存,申请内存由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值分配相应的内存,分配内存的规则如下:
- 若
bytes
小于当前块剩余内存大小,则直接分配,并对alloc_ptr
和alloc_bytes_remaining
做响应调整 - 若
bytes
大于当前块剩余内存大小,且小于kBlockSize / 4 = 1024
,则申请一个新的block
,将alloc_ptr
的值移到新的block上,并将alloc_bytes_remaining
的值设为kBlockSize - bytes
。 - 若
bytes
大于kBlockSize / 4 = 1024
,则直接分配一块大小为bytes
的内存,alloc_ptr
和alloc_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
的意义如图所示:
???得到这个偏差之后,我们就能轻易得到若需要对齐还差多少字节了,我们将这个差值保存在needed
中,上图例中,needed = 8 - current_mod = 5
。
? ???接下来我们将传入的bytes
和needed
相加,就能的得到我们对齐之后总共需要分配多少字节内存了,同样我们按照最开始所提到的三个策略进行分配进行分配,但是需要注意的是,若 对齐总共需要分配的内存 < 剩余的块内存,那么调用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 memoryresult = AllocateFallback(bytes);}assert((reinterpret_cast<uintptr_t>(result) & (align - 1)) == 0);return result;
}
总结
? 相比于Nginx
的内存池实现方式,Arena的策略并没有区分大块内存和小块内存。但同样Arena并没有给出类似于free
和delete
之类的函数,相关的操作在Arena的析构函数中执行。所以通过Arena来管理程序中各个模块的内存使用,可以有效防止内存泄露的问题。