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*);
- }
只是申请一个块新内存,并不更新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;
- }
由于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
从结果看,char占1个字节,int占四个。但是结构体的体积确实8。
打印结构体内里char c和int i的地址,会发现char c实际占用了4字节。
上面就是系统自动做了内存对齐,在char c后面填了3字节。
Arena在创建new char[block_bytes]这么大的块后,需要在里面为app分配内存。
由于块内的空间是Arena自己维护的,系统不会做内存对齐,就需要Arena自己对齐。
- 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。
- size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);
这句的意思是看看alloc_ptr_的“零头”是多少
如果以10为标准(align = 10),大于等于10的部分都不用看,只看小于10(也就是align-1)的部分有没有值。因此108的零头是8。
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这种形式
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博客 ” ,链接:http://blog.itpub.net/26239116/viewspace-1832774/,如需转载,请注明出处,否则将追究法律责任。