一. 什么是内存池?
内存池(Memory Pool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocks allocation)。
通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。
内存池是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
二. 内存池的优势
1. 直接使用系统调用的弊端
调用malloc/new,系统需要根据“最先匹配”、“最优匹配”或其他算法在内存空闲块表中查找一块空闲内存,调用free/delete,系统可能需要合并空闲内存块,这些会产生额外开销。
频繁使用时会产生大量内存碎片,从而降低程序运行效率。
容易造成内存泄漏。
2. 内存池的优点
(1) 减少内存分配次数,集中一次释放。
(2) 比malloc/free进行内存申请/释放的方式快(向内存池请求,而不是直接向操作系统请求)
(3) 不会产生或很少产生堆碎片
(4) 可避免内存泄漏(直接通过内存池操作对象,无需关心释放单个对象,整块内存池一次释放)
三. Arena内存池
内存池很多地方都有用到,像linux内核也有个内存池。在leveldb中也有内存池解决方案,就是Arena内存池。下面主要讲解Arena内存池的原理。
Arena类采用vector来存储每次分配内存的指针,每一次分配的内存,我们称为一个块block。block默认大小为4096kb。
1. Arena类的成员变量
// Array of new[] allocated memory blocks
std::vector<char*> blocks_;
// Allocation state
char* alloc_ptr_;
size_t alloc_bytes_remaining_;
// Total memory usage of the arena.
port::AtomicPointer memory_usage_;
Arena类的成员变量还是相对比较简单的,blocks_是一个vector用来存储每一次向系统请求的分配的内存指针。alloc_ptr_表示当前内存块(block)偏移量指针,也就是未使用内存的首地址。 alloc_bytes_remaining_表示当前块所未使用的空间大小。 memory_usage_则是用来记录Arena类内存使用情况的。
2. Arena类的成员函数
a. 构造函数
Arena::Arena() : memory_usage_(0) {
alloc_ptr_ = NULL; // First allocation will allocate a block
alloc_bytes_remaining_ = 0;
}
非常简单的一个构造函数,负责初始化一些变量,注意刚开始是没有立刻分配内存的
b. 析构函数
Arena::~Arena() {
for (size_t i = 0; i < blocks_.size(); i++) {
delete[] blocks_[i];
}
}
析构函数负责将从系统中申请的内存返还给系统。
c. Allocate() && AllocateFallback() && AllocateNewBlock()
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;
}
Allocate是Arena向外界提供的接口,该函数会调用AllocateFallback() && AllocateNewBlock() 这两个私有函数。
如果需求的内存小于剩余的内存,那么直接从内存池中获取。
如果需求的内存大于剩余的内存,而且大于1k(4096/4),则给这个需求单独分配一块需求内存大小的内存。
如果需求的内存大于剩余的内存,而且小于1k,则重新分配一个内存块,默认大小4096,用于存储数据。原有的剩余空间浪费掉。
d. AllocateAligned()
Arena还提供了字节对齐内存分配,一般情况是8个字节对齐分配。对齐内存的好处简单的说就是加速内存访问。
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。
比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
CPU一次访问时,要么读0x01~0x04,要么读0x05~0x08…硬件不支持一次访问就读到0x02~0x05
例:如果0x02~0x05存了一个int,读取这个int就需要先读0x01~0x04,留下0x02~0x04的内容,再读0x05~0x08,留下0x05的内容,两部分拼接起来才能得到那个int的值,这样读一个int就要两次内存访问,效率就低了。
如上图所示,第一种排列方式,如果short存储位置是在奇数位置,那么读取这个变量就要访问两次内存了。
e. MemoryUsage()
size_t MemoryUsage() const {
return reinterpret_cast<uintptr_t>(memory_usage_.NoBarrier_Load());
}
Arena最后一个对外接口是返回这个内存池分配总的内存大小。