allocator源码解析之bitmap_allocator:
mt_allocator(待完成)
bitmap_allocator 源代码位于:gcc-9.2.0/libstdc++-v3/include/ext/bitmap_allocator.h, 部分实现在:gcc-9.2.0/libstdc++-v3/src/c++98/bitmap_allocator.cc,用名字上可以看出使用了bitmap(bitmap某一位表示一种状态,用做干啥呢?)通过阅读代码可以理解:bitmap用作内存块(struct _Alloc_block)的使用和释放的标志,bitmap_allocator比_pool_allcoator 高级的地方不会所有申请的内存都放到自己手里,增加了内存返回给CRT的功能。
为了方便理解先介绍几个概念:
)_Alloc_block:bitmap_allocator给容器内存的最小单元;
)block: bitmap_allocator一次内存申请(向CRT)的单位:包含M个_Alloc_block和相应的管理开销;
)free_list: bitmap_allocator的父类,负责和CRT交互,负责真正的内存申请和回收, 同时负责全回收block的管理,根据策略决定是否回收给操作系统;
)__mini_vector: 作者实现的类似于std::vector容器;
) _Block_pair 为std::pair<_Alloc_block*, _Alloc_block*> ,first指向第一个_Alloc_block, second指向最后一个_Alloc_block;
) _S_mem_blocks: 用来管理申请的正在被容器使用的block的管理, 类型为_mini_vector, 存储元素的类型为:Block_pair,
)Bitmap_counter: 用来管理block,能够遍历_S_mem_blocks存储的所有block的bitmap, 然后存储:block在_S_mem_blocks(_M_curr_index)的下标和 当前使用的bitmap的指针(_M_curr_bitmap);
) _S_last_request: 类型为:Bitmap_counter, 保存上次为容器分配内存的block和相应的bitmap;
)_S_block_size 每次分配_Alloc_block的数量,每次分配 _S_block_size*= 2, 当有释放的时候 _S_block_size /= 2;
) _S_find(_Predicate __p), 查找_S_mem_blocks中满足__p的blocks,返回指向这个block的_mini_vector::iterator ;
)_Ffit_finder :放回当前_Block_pair管理的block是否有可用内存,如果有返回true,并修改成员:_M_data_offset (这是第几个bitmap)和_M_pbitmap(指向有内存bitmap);
)Inclusive_between仿函数, 判断某一个_Alloc_block是否在某一个Block_pair管理的范围内。
block的内存结构
bitmap_allocator一次申请内存的block内存结构图(看懂结构就可以,方便后面的理解)
bitmap_allocator的核心存储
bitmap_allocator的整个内存的管理方式,bitmap_allocator的内存底层存储在_S_mem_blocks(_mini_vector类型),_ S_mem_blocks每一个元素为:std::pair<_Alloc_block*, _Alloc_block*>,指向每次申请内存块第一个的_Alloc_block和最后一个_Alloc_block
言归正传, 1)从allocate流程逐步分析函数;
allocate(size_type __n)函数:
行1019-1023, 和其他alloator一样对于特大对齐方式的元素,直接调用operator new分配;
行1027 ,__builtin_expect(__n == 1, true) ,__builtin_expect(EXP, N), 允许程序员将最有可能执行的分支(EXP == N)告诉编译器。编译器在编译过程中,会将可能性更大的代码紧跟着起面的代码,从而减少指令跳转带来的性能上的下降。
行1028,调用_M_allocate_single_object 分配内存,返回给容器
行1030-1-32, 对于>1个数的内存申请,直接调用operator new 分配;
可以看出bitmap_allocator只有在单个元素内存申请的时候才有效果;
_M_allocate_single_object()函数:
行843~845, _S_last_request 类型为:_Bitmap_counter, 保存上次分配内存的block和其bitmap相关信息, while循环从上次分配内存的block往沿着_S_mem_blocks往后寻找可以分配的_Alloc_block(具体后面看_Bitmap_counter的operator++ 实现)
如果有,_S_last_request 保存可以分配的内的block和bitmap信息
如果没有,_S_last_request._M_curr_bitmap = 0 ;_M_finished()返回True
行847-886,如果_M_finished() == True, 从_S_mem_blocks的头向后查找是否有合适的位置(具体后面_S_find 和Ffit_finder的实现)
行855-872,如果找到了,blocker信息保存在__bpi(类型为_mini_vector::iterator)里,
行859, Bit_scan_forward函数返回_Alloc_block在当前block的当前bitmap中位置,保存在__nz_bit 中:
行860,__bit_allocate 会将相应的bitmap的__nz_bit+1 位置置为0;
行862, 把__bpi的信息,保存到_S_last_request中,提供给下次allocate使用;
行865-867, 取得_Alloc_bloc看的地址 保存在_ret 【具体地址的计算参考内存图,更容易理解】
行868-872, _Alloc_block的使用计数+1;
行878-882, 如果没找到,调用_S_refill_pool()分配内存,并放到_S_mem_blocks的尾部,
同时把block信息保存在_S_last_request中,提供给下次内存分配调用;
行890-903, _M_finished() == false, _S_last_request 保存着要分配的block和bitmap信息,炒作一样: 计算_nz_bit, 置0, 获取地址, 计数+1 ,返回;
_S_refill_pool()函数:
第一个红框:行772-776, 计算需要申请内存的大小;
第二个红框:行778-781, 调用_M_get() 获取内存;行781会把使用_Alloc_block的使用个数初始化为0;
第三个红框:行784-792, 申请的内存用_Block_pair管理,并到_M_mem_blocks的尾部;
第四个红框:行794-797, 对bitmap初始化全1, 行797对:_S_block_size *2 (下次内存申请加倍)
基本完成了allocate的过程分析,指针的+-都要参考block内存图思考,更容易看明白,allcocate使用_M_get一次获取大的block,保存在 _S_mem_blocks中,后续分配都操作这个block, 如果没有内存可以分配给容器,会在_S_refill_pool调用_M_get从新获取一块大的block;有几点需要注意的:
1)使用bitmap是,先使用靠近_Alloc_block内存的bitmap, 位置从低到高使用;_Alloc_block的分配从Block_pair的first中开始使用;这样会方便计算,仔细体会;
2)bitmap的下标(offset)是相对第一个_Alloc_block的,每次计算分配_Alloc_block的位置都需要bitmap的offset和bitmap中bit的位置来计算;
2) 分析deallocate()函数过程:(基本和allocate代码相对应,看起来相对轻松)
deallocate(pointer __9, size_type __n)
_M_deallocate_single_object(pointer __p)函数:
核心代码第一个红框:行929-948, 解释几点:
_S_last_dealloc_index 是局部性原理的利用,认为这次回收内存的block,下次更有可能;
_displacement是要回收的_Alloc_block与blocks第一个的距离;
核心代码第二个红框:行958-962,调用__bit_free 置1,_Alloc_block计数-1;
核心代码第三个红框:行965-994,解释几点:如果_Alloc_block计数 =0 ,
行969, 下此申请内存的block数减半, _S_block_size /= 2;
行973,调用_M_insert 插入到free_list中,回收这个block。
行982-994,同时需要善后: 如果当前block回收,_S_last_dealloc_index 必须修改为合理值,_S_last_request如果在回收block后面, 需要修改合理的值;
deallocate函数分析相对简单,回收的善后工作要思考清楚,处理好,往往mem_pool的回收处理问题是内存泄漏的根源
总结一下:从allocate和deallocate函数来看, bitmap_allocator巧妙的设计结构,两次结构:1)free_list负责block的分配、缓存、回收,2) bitmap_allocator通过管理_S_mem_blocks来负责Alloc_block保存分配和回收信息,支持Alloc_block分配和全部block回收后的回收工作。
下面详细理解一些其中用到的一些基础类和函数
free_list
free_list 是bitmap_allocator的基类,用作block的管理,包括申请、缓存、释放;free_list 最多缓存64个block,再多根据策略选择性回收;
释放的block, 存储在 _S_free_list中,类型为_mini_vector
_M_should_i_give, 当申请的__required_size与 __block_size不匹配,计算_max_wastage_percentage<=36【这个36很神奇,作业臆断的吧】,另外按照bitmap_allocator身亲内存的计算方式,除了会出现相等,不会出现不相等的方式。
_M_clear() 会对free_list的内存直接调用::perator delete()进行释放。
_M_get(size_t __sz)函数:
_M_get(size_t __sz), bitmap_allocator像CRT申请内存的接口:
核心代码第二个红框:行94-96,如果找到且合适(不浪费),直接给调用方;
核心带带第一个红框:行62-89, 如果没找到或者不合适,试着直接从CRT分配, 失败则调用_M_clear()释放自己缓存的内存,同事再次尝试从CRT分配;
_M_insert(size_t* __addr), bitmap_allocator上层讲 完全回收的block放到free_list的接口;
_M_validate(size_t* __addr) 实际执行插入的函数:
如果free_list已经满(>=64), 则从当前要回收的block和_S_free_list中选取最大块回收;至多保留64个在_S_free_list中;
如果free_list未满,直接插入;
__mini_vector:类似于std::vector的容器
__mini_vector,自定义的一个类似于std::vector容器的类,支持存储空间自动扩充,内存的申请和释放采用调用::operator new 和::operator delete申请一块连续的内存
__mini_vector靠三个指针来管理:_M_start; 指向__mini_vector 的第一个元素, _M_finish; 指向最后一个元素,_M_end_of_storage; 指向最后一个可用空间。
实际使用中,每个元素为Block_pair(std::pair<_Alloc_block*, _Alloc_block*> ,first第一个_Alloc_block和second为最后一个_Alloc_block的指针。
bits_per_block:
bits_per_block: 表示一个size_t有bit个数,前面提到allocator使用了bitmap,哈哈这是精髓,一个size_t可以存储bits_per_block个_Alloc_block的使用状态,也就是每次申请bits_per_block个_Alloc_block,就需要额外一个size_t的空间存储。
bitmap_allocator按照block申请内存,sizeof(block) = bits_per_block * sizeof(_Alloc_block)后面可以看到,每次新申请block数=count(上次申请block数) * 2,一旦有回收情况 新申请block数= (上次申请block数) / 2。
介绍几个函数的定义:
__lower_bound函数, 返回在_first和_last之间满足__comp函数的最小_ForwardIterator。
_AddrPair是一个std:pair类型,负责管理每次分配内存, first为指向第一_Alloc_block的指针, second为指向最后一个_Alloc_block的指针。
__num_blocks表示__ap管理的内存包含_Alloc_block的个数。
__num_bitmaps表示__ap管理内存占用的bitmap数,也就是size_t的个数
仿函数(函数对象):_Inclusive_between、_Functor_Ref、_Ffit_finder
_Inclusive_between, 继承std::unary_functio<typename std::pair<_Tp, _Tp>, bool>(c++11已经废弃),_Inclusive_between的函数参数为:std::pair, 函数返回为:bool, 构造函数需要_Tp类型的参数初始化_M_ptr_value。
在实际使用中模板参数_Tp为指针(_Alloc_block*类型), Block_pair 类型为:std::pair<_Alloc_block*, _Alloc_block*> first指向第一个可用的_Alloc_block块,second代表指向最后一个_Alloc_block块, 函数的作用:计算_M_ptr_value是否在 当前__bp块内,也就是是否由这个block所管理(用于内存回收)。
_Functor_Ref,没啥特殊,内部保存一个仿函数引用,直接调用。
_Ffit_finder 代码如下,我们分三个部分介绍(代码必须配合内存图看,更清晰)
_Ffit_finder核心代码第一块:介绍两个类成员变量:_M_pbitmap(某一个block的内存管理的bitmap)和_M_data_offset(这个bitmap在内存中的offset)【这个offset相对应的地址为第一个_Alloc_block指针(Block_pair.first),这点很重要】,前面提到一个bitmap由一个size_t类型的实现;
_Ffit_finder核心代码第二块:仿函数的实现(operator()), 仿函数的理解需要借助文章开始介绍的内存出来理解,否则真的懵懵懂懂。
行359, __diff 表示的Block_pair(__bp)有多少的bitmap, 也就是需要几个size_t, 也就是内存图中N的个数【...】 ;
行361~362,判断当前 __bp的所有_Alloc_block是否已经使用完成, (reinterpret_cast<size_t*>(__bp.first) - (__diff + 1)) 为内存图中第二个size_t的指针, 该变量表示 __bp 中已经使用的_Alloc_block 的个数。
行367~378, bitmap初始值为全为1,*rover != 0 表示block仍然有_Alloc_block可以分配,
_M_pbitmap指向有内存block的bitmap, _M_data_offset表示当前是第几个bitmap(第几个block)
_Ffit_finder核心代码第三块:_M_offset() 返回第_M_data_offset个block中的第一个_Alloc_block的地址;
_Bitmap_counter类,封装对【一次申请的内存块】的管理,也实现在【多次申请的内存块(多个如第一个内存图的结构)】bitmap的遍历。
_Bitmap_counter的几个成员:
_M_vbp: 底层_S_mem_blocks的引用;
_M_curr_bmap: 当前block的的bitmap指针;
_M_last_bmap_in_block: 当前内存块的最后一个block的bitmap指针;
_M_curr_index: 当前block在_M_vbp(_S_mem_blocks)中的下标;
_Bitmap_counter的几个函数:_M_reset()、_M_get()、 _M_base()、 _M_offset()、 _M_where() 理解内存布局基本就能理解函数的具体实现。
重点介绍下operator++()函数的实现:如果当前bitmap不是最后一个bitmap,则移动到下一个bitmap, 如果是最后一个bitmap, 调用reset(_M_vbp[_M_curr_index +1] )执行初始化操作;所以_Bitmap_counter实现了在所有申请block上的游走;
几个函数:__bit_allocate、__bit_free、_Bit_scan_forward
__bit_allocate(相应的bitmap置o)
__bit_free(相应的bitmap置1)
_Bitscan_forward( 通过调用__builtin_ctzl来查找第一个为1的位置的便宜量)