LevelDB(v1.3) 源码阅读系列使用 LevelDB v1.3 版本的代码,可以通过如下方式下载并切换到 v1.3 版本的代码:
$ git clone https://github.com/google/leveldb.git
$ cd leveldb
$ git checkout -b v1.3 v1.3
本文涉及的代码文件为:
util/arena.h
util/arena.cc
一、简介
Arena 是一个简单的内存分配器。通过一个 Arena 对象,可以进行一些内存分配操作。Arena 对象会将所有分配的内存都记录在案,在 Arena 对象析构的时候一起释放所有记录在案的已分配内存。
Arena 默认分配的内存大小是 4096KB,也就是 4K。每当有一个内存分配请求分配 X Bytes 的内存的时候,Arena 的分配原则如下:
- 如果当前分配的内存块中剩余的内存能够满足该分配请求,也就是说大于等于 X,那么就在当前的内存块中分配 X Bytes 给用户,否则:
- 如果 X 大于默认的内存块大小的 25%,也就是说 X > 1KB,那么就单独分配大小恰好是 X Bytes 的内存给用户,否则:
- 新分配一块大小为 4KB 的内存,在里面分配 X Bytes 给用户(也就是说,上一块内存中可能有些内存没有被分配给用户就被 Arena 抛弃了,这里会造成小于 1KB 的内存浪费)
这里需要注意两个数字:4KB 和 1/4。
- 1/4 是一个经验值:是减少内存碎片和提高内存分配器性能的一个 trade off;
- Linux 内核的 page size 为 4KB
可以通过如下命令查看 page size:
$ getconf PAGESIZE
此外,Arena 还提供了分配地址对齐的内存块的接口:调用这个接口,用户会得到他想要的 X Bytes 的内存块,该内存块的起始地址是 8 的倍数:形如 0x_______0 和 0x_______8,如果在分配内存的时候发现当前的地址并不满足这种形式,那就多分配点内存用来填补不够的 Byte(最多浪费 7 个 Bytes),之后的分配原则就和上面讲的三大原则一致了。这样的内存块的带来的优势是 CPU 寻址速度快:
- 在 32 位机器上,地址总线是 32 bits 的,指针的 size 为 4 Bytes;64 位机器上,地址总线是 64 bits 的,指针的 size 为 8 Bytes。
- CPU 取内存中的数据并不是一个 Byte 一个 Byte 的取,在 32 位机器上,一次取的数量是 4 Bytes,64 位机器上一次取 8 Bytes。
值得一提的是,每次我们从堆中 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;
}
代码中的注释已经很好的解释其行为了,我就不多嘴了。