Arena 是一个简单的内存分配器。通过一个 Arena 对象,可以进行一些内存分配操作。Arena 对象会将所有分配的内存都记录在案,在 Arena 对象析构的时候一起释放所有记录在案的已分配内存。
Arena 默认分配的内存大小是 4096KB,也就是 4K。每当有一个内存分配请求分配 X Bytes 的内存的时候,Arena 的分配原则如下:
1、如果当前分配的内存块中剩余的内存能够满足该分配请求,也就是说大于等于 X,那么就在当前的内存块中分配 X Bytes 给用户,否则:
2、如果 X 大于默认的内存块大小的 25%,也就是说 X > 1KB,那么就单独分配大小恰好是 X Bytes 的内存给用户,否则:
3、新分配一块大小为 4KB 的内存,在里面分配 X Bytes 给用户(也就是说,上一块内存中可能有些内存没有被分配给用户就被 Arena 抛弃了,这里会造成小于 1KB 的内存浪费)
所以,这里有两个数字我很疑惑,一个就是默认内存块的大小 4KB,另一个就是是否需要单独分配内存块的判断标准:1/4=25%,这两个数字我觉得才是整个内存分配机制中的核心,然而目前为止我还没有搞明���。有人告诉我 4KB 是内核中默认一个 page 的大小,这一点我还不是特别确定。至于 25% 估计是一个经验值,想要内存分配器的性能高,就要尽量减少内存碎片(堆上的碎片)和内存分配的次数,权衡各方利弊之后,25% 估计就是这样一个经验值
此外,Arena 还提供了分配地址对齐的内存块的接口。什么意思呢?调用这个接口,用户会得到他想要的 X Bytes 的内存块,而这个内存块的起始地址是 8 的倍数。我们知道,在 32 位机器上,地址总线是 32 bits 的,也就是说,指针的 size 是 4 Bytes,而 64 位机器上,地址总线是 64 bits 的,指针的 size 是 8 Bytes 的。CPU 取内存中的数据并不是一个 Byte 一个 Byte 的取,在 32 位机器上,一次取的数量是 4 Bytes,64 位机器上一次取 8 Bytes。
扯远了,Arena 的这个分配地址对齐的内存块的接口,其返回的内存块的起始地址是 8 的整数倍。也就是说,返回的地址是这两种形式:"0x_______0" 和 "0x_______8"。要是在分配内存的时候发现当前的地址并不是这种形式,那就多分配点内存用来填补不够的 Byte,最多可能浪费 7 个 Byte。接下来的分配原则就和上面讲的三大原则一致了。
值得一提的是,每次我们从堆中 new 一块内存的时候,操作系统返回的该内存块的首地址是一定能被 8 整除(地址对齐)的。
关于内存分配部分,这里都讲的差不多了,代码我就贴一点点吧。
// Allocation state
char* alloc_ptr_; // 指向当前内存块中剩余未分配内存块的首部
size_t alloc_bytes_remaining_; // 保存当前内存块中剩余的可分配出去的内存大小
// Array of new[] allocated memory blocks
std::vector<char*> blocks_; // 保存所有分配的内存块
// Bytes of memory in blocks allocated so far
size_t blocks_memory_; // 保存该 Arena 对象总共申请内存的大小
Arena();
~Arena();
char* Allocate(size_t bytes);
char* AllocateAligned(size_t bytes);
size_t MemoryUsage() const {
return blocks_memory_ + blocks_.capacity() * sizeof(char*);
}
其中,Allocate(size_t) 用来分配非地址对齐的内存,AllocateAligned(size_t) 用来分配地址对齐的内存
值得一提的是,Arena 对象不允许拷贝操作,为了完成这个,它把以一个 Arena 对象作为参数的构造函数声明称私有的,并重载 '=' 为私有
关于分配内存的三条原则,可以在 Allocate(size_t) 的实现和一个私有成员函数 char* AllocateFallback(size_t bytes);
中查看:
static const int kBlockSize = 4096;
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_memory_ += block_bytes;
blocks_.push_back(result);
return result;
}
代码中的注释已经很好的解释其行为了,我就不多嘴了。