allocator源码解析之pool_allocator(_pool_alloc):
new_allocator
malloc_allocator
pool_allocator
bitmap_allocator
mt_allocator(待完成)
__pool_alloc 头文件定义在gcc-9.2.0/libstdc++-v3/include/ext/pool_allocator.h,实现代码在:gcc-9.2.0/libstdc++-v3/src/c++98/poll_allocator.cc。
__pool_alloc 的定义:
__pool_alloc 继承于__pool_alloc_base, 还是首先看关键的allocate和deallocate函数:
allocate函数的定义:
第一个红框,224-230, 如果支持aligned_new, alignof(类型) >__STDCPP_DEFAULT_NEW_ALIGNMENT__ 会直接调用operator_new的某一个重构版本,按照指定类型的对齐大小分配内存;
第二个红框是关键,是分配的核心逻辑:243-245,当分配的内存大小>_S_max_bytes(128B),直接调用::operator new分配,也就是说这个__pool_alloc只管理<128B的内存分配 ;247~259是正常的小内存的分配管理, 关键的三个函数(_M_get_free_list, _M_refill, _M_round_up)都是在__pool_alloc_base中定义稍后分析,代码粗略可以看出采用list的形式预先分配预定大小*个数的内存,然后从头部分配,指针后移,后面我们按照allocate的代码顺序分析三个函数及类成员变量值的变化;
deallocate函数的定义:
核心代码分别和allocate的核心代码对应
第一框272-276调用operator delete的重载版本回收allocate的第一个红框分配的代码;
第二个框279-288来处理allocate第二个框的情况分配的代码,279-280 如果是大内存,直接调用operator delete直接释放;
282-288只是把放到某一个list(_M_get_free_list返回)的头部,没有调用operator delete释放,提供给下次申请直接使用,这也是__pool_alloc的精髓之处;
从这里可以看到pool管理的代码都在__pool_alloc_base中实现,__pool_alloc_base才是管理的关键,__pool_alloc_base的定义如下
__pool_alloc_base类的第一个框:定义了三个enum类型, _S_max_bytes = 128B,刚才说的内存管理最大为128B原因是在这里定义的,,另外定义了_S_free_list_size = 128/8= 16,
这里有个有意思的定义:union _Obj 的定义是一般做内存池技术常用的技巧(嵌入式指针)减少额外的指针空间的浪费, 里面只有一个_Obj*的指针和 char _M_client_data[1](感觉没有用到,搜索代码中也没找到),这个结构可以修改为:struct _Obj { struct Obj* _M_free_list_link;}; 感觉更简单易懂; 这个_Obj应该是管理分配好的内存单元的指针
__pool_alloc_base类的第二个框:(对基本含义借助经验瞎理解一下)
定义了一个指针数组,数组大小为16, 每个元素为_Obj*类型;_S_free_list[i] 应该是管理某一类(大小相同8B为间隔)的内存,_S_free_list共有16种规格的内存(每个_S_free_list[i] 相差8B,把<128B的内存分成16段管理)(通过后面的源码阅读,理解是正确的)
定义了三个指针:_S_start_free和_S_end_free是某一块内存的开始和结束指针(后面分析代码再确认,为什么不用开始指针+大小来表示呢),_S_heap_size(已经使用的heap内存的大小),从读优秀源码的命名习惯,应该是这个意思,后面确认!!!!),(通过后面的源码阅读,给三个类成员明确的含义:
_S_start_free: 没有挂在任何_S_free_list[i] 中的内存的开始地址;命名为free内存;
_S_end_free: 没有挂在任何_S_free_list[i] 中的内存的结束地址;命名为free内存;
_S_heap_size:表示使用::operator new申请的heap内存的总和(包括已经分配出去的和现在挂在_S_free_list[i] 上没有被分配出去的heap内存的和; 这个变量在下次内存分配的时候会参考
__pool_alloc_base类的第三个红框:有几个函数的定义:
1)size_t _M_round_up(size_t _bytes) 返回>_bytes 的是_S_align倍数的第一个数,这个函数很巧妙,_S_align必须是计算机整数;
2)_Obj* _M_get_free_list(size_t _bytes) throw(); 根据_bytes大小返回某一个_Obj*指向的内存链,实现在:gcc-9.2.0/libstdc++-v3/src/c++98/poll_allocator.cc, 计算方式为入下图,找到满足_bytes大小最小的内存链:_S_free_list[i];
3)void* _M_refill(size_t __n), 从前面allocate的实现的函数调用关系可以看出来,_M_refill函数是绝对的主角,实现在:gcc-9.2.0/libstdc++-v3/src/c++98/poll_allocator.cc
核心第一个红框:这里写解释下,_M_allocate_chunk 返回的是分配内存的开始地址(__chunk),其中_nobjs 按引用传值, 传入20,传出真正的原始个数,也就是_chunk的大小= __n * __nobjs;
核心第二个红框: 如果返回的个数为1个, 直接返回给调用函数,没必要挂在_S_free_list[i]上
核心第三个红框:核心的分配内存的组织方式(串联), 这里也就是Obj的巧妙的使用方式,没有使用额外的内存来管理_S_free_list[i];内存管理方式如下图:
4)char* _M_allocate_chunk(size_t __n, int& __nobjs) 返回的是分配内存的开始地址(__chunk),其中_nobjs 按引用传值, 传入20,传出真正的元素个数,也就是_chunk的大小= __n * __nobjs;
_bytes_left :free内存区的剩余内存大小(也就是没有挂在任何_S_free_list[i]), _total_bytes :本次申请的总内存大小;
核心代码第一个红框: if(69行,_bytes_left >= __total_bytes) 如果free内存区剩余内存足够,就直接返回,并更改_S_start_free的地址;
核心代码第二个红框:if (75行, _bytes_left >= __n)如果free内存区剩余内存满足一次分配,也直接返回
核心代码第三-七个红框(85行~125行)当前free内存完全不满住上面两个条件,需要调用::operator new申请,并blabla的处理逻辑去返给上层调用函数;
核心代码第三个红框:如果free内存取的还剩下(但是不满住本次分配), 找到合适的:_S_free_list[i] 挂上去;(这一步必须做,否则会有内存泄漏)
核心代码第四个红框:计算新申请内存的大小,没有理由
核心代码第五个红框:调用::operator new申请内存,返回给free内存区;
核心代码第六个红框:如果内存分配失败, 从从_S_free_list[i],找到一个比需要分配(__n)大的_S_free_list[j]中取出一块内存,放到free内存区(修改_S_start_free, _S_end_free),递归调用自己完成内存分配;
核心代码第七个红框:申请成功后,直接修改(_S_end_free, _S_heap_size),递归调用自己去完成内存的分配(这里比较巧妙, 递归自己,让逻辑更清晰);
使用:
#include
int main(int argc, char* argv[])
{
__gun_cxx::__pool_allocator pool_alloc
(); int* intPtr = pool_alloc.allocate(1);
pool_alloc.deallocate(intPtr);
}
总结:__pool_alloc 实现了内存池
优点:提高了内存的利用率,省去了每次单独operator new(cookie) 占用的内存。而且有一些常用的技巧(嵌入式指针,round_up计算, 递归调用简化逻辑)
不足:内存不返还给底层,也就是内存使用完回收给自己直接放到_S_free_list[i]中,无论已经有多大,造成内存的浪费;分析原因:由于设计问题,他不知道【95行一次从底层申请的内存单元】是否完全被释放了。