代码文件
util/arena.h
util/arena.cc
level db有个内存池不叫xxxpool,而叫Arena。
从操作系统申请内存的方式是直接new一个空的char数组,得到一大块连续内存作为数据块,并维护数据块内的空间使用情况。
当有app申请内存时,将数组里某个char的地址返回给app,让它使用从这个char开始的一段连续内存。
当一个数据块不能满足app的连续内存需求时,就会创建新的char数组。
每个char数组,也就是数据块的地址都记录在Arena的一个vector里。当Arena销毁时,就根据vector里记录的数据块地址逐个释放空间。
成员变量
-
std::vector<char*> blocks_; // 已分配的内存块的地址队列
-
size_t blocks_memory_; // 已分配所有内存块的总内存
-
char* alloc_ptr_; // 当前内存块下次分配的地址。比如已经分配了4个单位,alloc_ptr_就指向5
-
size_t alloc_bytes_remaining_; // 当前块内还剩多少内存
-
static const int kBlockSize = 4096; // 默认的标准块是4k,也支持按程序要求申请指定大小的块
私有函数
-
char* AllocateNewBlock(size_t block_bytes); //按照给定大小分配一个新数据块,将新块的地址推入blocks_
-
char* AllocateFallback(size_t bytes); //当前块的alloc_bytes_remaining_小于请求的内存大小时,用这个函数分配新内存
公共函数
-
char* Allocate(size_t bytes); //从当前块中分配内存,如果内存不够就调用AllocateFallback
-
char* AllocateAligned(size_t bytes); //和Allocate类似,但是这个会做内存对齐
-
size_t MemoryUsage() const {... //返回当前已分配的内存块。是估算值,以创建内存块但是还没分配给app使用的部分也算在内。
MemoryUsage函数的内容很简单,就是返回blocks_memory_加上blocks_里存放的指针大小之和
-
size_t MemoryUsage() const {
-
return blocks_memory_ + blocks_.capacity() * sizeof(char*);
-
}
Arena最基本的操作是分配一个新内存块。
只是申请一个块新内存,并不更新alloc_ptr_和alloc_bytes_remaining_ 。
内存块的形式就是简单地以char数组的形式占用一片连续内存。
-
char* Arena::AllocateNewBlock(size_t block_bytes) {
-
// 用空的char数组占用一片连续内存,将地址保存到result里,当做返回值。
-
char* result = new char[block_bytes];
-
// 整个Arena已分配的总内存需要加上新申请的块的大小
-
blocks_memory_ += block_bytes;
-
// 将新块的地址追加到blocks_队列
-
blocks_.push_back(result);
-
返回新块地址
-
return result;
-
}
AllocateNewBlock只是最基本的分配新块,没办法实用,比如给app分配内存时关键的alloc_ptr_ 和alloc_bytes_remaining_ 它不会维护。
Arena内部分配新块的功能函数实际是AllocateFallback。通过AllocateFallback调用AllocateNewBlock
-
char* Arena::AllocateFallback(size_t bytes) {
-
// 它会先看一下要分配多大内存。如果是大于标准块的四分之一,就直接新建一个这么大内存的非标准块,给app专用了。
-
// 剩下的一起都有app自己解决,Arena保持当前块的alloc_ptr_和alloc_bytes_remaining_,给以后分配小空间使用。
-
if (bytes > kBlockSize / 4) {
-
char* result = AllocateNewBlock(bytes);
-
return result;
-
}
-
-
// 如果申请的bytes小于标准块的四分之一,并且剩余空间alloc_bytes_remaining_还是不够,就创建新的标准块,将地址赋给alloc_ptr_。
-
// 新的标准块有Arena维护,作为当前块给app分配内存。alloc_bytes_remaining_自然就是标准块大小。
-
alloc_ptr_ = AllocateNewBlock(kBlockSize);
-
alloc_bytes_remaining_ = kBlockSize;
-
-
char* result = alloc_ptr_; // 由于是新的标准块,因此块的起始地址就是给app分配内存的起始地址,作为返回值。
-
alloc_ptr_ += bytes; // alloc_ptr_后移bytes,作为以后分配内存的起始位置。
-
alloc_bytes_remaining_ -= bytes; // 块内剩余内存减少已分配给app的bytes
-
return result;
-
}
外部app调用的是Allocate函数。
由于Arena记录了当前可分配的起始地址alloc_ptr_,以及当前块剩余的内存alloc_bytes_remaining_,因此只需要操作这两个值即可。
blocks_memory_在分配新块的时候已经加过块大小了,就不用每次分配小内存都去维护了。
-
inline char* Arena::Allocate(size_t bytes) {
-
assert(bytes > 0);
-
if (bytes <= alloc_bytes_remaining_) {
-
char* result = alloc_ptr_; // alloc_ptr_是当前可分配内存的起始地址,肯定就是新分配内存的地址了。
-
alloc_ptr_ += bytes; // 分配后alloc_ptr_需要向后移bytes,执行下一次分配的起始地址。
-
alloc_bytes_remaining_ -= bytes; // 块内剩余内存减少了bytes
-
return result;
-
}
-
return AllocateFallback(bytes); // 如果app申请的内存大于块内剩余内存,就调用AllocateFallback来申请新块。
-
}
以上就是Arena自身创建数据块,以及将数据块内的空间分配给app使用的过程。
Arena里还有一个函数,和Allocate一样供外部app调用,区别是它会做内存对齐。
关于内存对齐,看下面一段程序。
-
#include <stdio.h>
-
#include <unistd.h>
-
-
struct st {
-
char c;
-
int i;
-
};
-
-
int main()
-
{
-
char c = 'A';
-
int i = 100;
-
struct st st1;
-
printf("size char : %d\n", sizeof(c));
-
printf("size int : %d\n", sizeof(i));
-
printf("size st : %d\n", sizeof(st1));
-
printf("addr st1.c : %x\n", &(st1.c));
-
printf("addr st1.i : %x\n", &(st1.i));
-
return 0;
-
}
输出结果
[root@mysql1 c]# ./a
size char : 1
size int : 4
size st : 8
addr st1.c : a5334f40
addr st1.i : a5334f44
从结果看,char占1个字节,int占四个。但是结构体的体积确实8。
打印结构体内里char c和int i的地址,会发现char c实际占用了4字节。
上面就是系统自动做了内存对齐,在char c后面填了3字节。
Arena在创建new char[block_bytes]这么大的块后,需要在里面为app分配内存。
由于块内的空间是Arena自己维护的,系统不会做内存对齐,就需要Arena自己对齐。
下面是Arena内存对齐版的Allocate函数
-
char* Arena::AllocateAligned(size_t bytes) {
-
const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
-
assert((align & (align-1)) == 0); // Pointer size should be a power of 2
-
size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);
-
size_t slop = (current_mod == 0 ? 0 : align - current_mod);
-
size_t needed = bytes + slop;
-
char* result;
-
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;
-
}
Allocate做判断时直接拿app申请的内存大小bytes与alloc_bytes_remaining_比较。
而AllocateAligned是先把bytes对齐,然后再和alloc_bytes_remaining_比较。
下面逐行分析内存对齐的过程
-
const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
首先需要确认按多少对齐。必须保证对齐单位必须能放下一个完整的指针(void* ),如果指针的体积小于8,就按8字节对齐。
在我的环境里sizeof(void*)就是8字节,下面都已8字节为例。
-
assert((align & (align-1)) == 0);
这句是要求这个对齐单位必须是2的n次方。也即是说它的2进制必须是一个1后面跟的全是0。
比如从8开始
8: 1000
16:10000
...
...
-
size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);
这句的意思是看看alloc_ptr_的“零头”是多少
以10进制为例
如果以10为标准(align = 10),大于等于10的部分都不用看,只看小于10(也就是align-1)的部分有没有值。因此108的零头是8。
换到2进制
1 当前分配的起始地址是alloc_ptr_,把alloc_ptr_强行转换成长整形。
2 对齐单位以8为例,align-1就是7,也就是111
3 reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1)
由于align-1 只有低三位是1,其它全是0。和它按位与过之后,大于align-1的部分就全没了,只保留“零头”
alloc_ptr_作为地址,它的二进制可能是 ...101111010这种形式
计算后就得到了“零头”
...10111010
& ...00000111
--------------
...00000010
4 计算之后,发现上次分配后,多出的“零头” current_mod = 2
size_t slop = (current_mod == 0 ? 0 : align - current_mod);
用对其单位align减去零头,就得到了需要手工填补的字节数。
上面例子里的结构体,char c是1字节,对齐单位是4,就填补了4-1=3字节。
我们的对齐单位是align,因此需要填align - "零头"current_mod。
-
size_t needed = bytes + slop;
app请求的内存bytes加上需要填补的slop,就得到了实际需要分配的内存量。
-
char* result;
-
if (needed <= alloc_bytes_remaining_) {
-
result = alloc_ptr_ + slop;
-
...
下面就和Allocate函数差不多了,只是返回给app的地址result不是alloc_ptr_ ,而是alloc_ptr_ 加上补充的空字节后的。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/26239116/viewspace-1832774/
,如需转载,请注明出处,否则将追究法律责任。