STL的空间配置器std_alloc 笔记
C++的内存分配基本操作是 ::operator new(),内存释放是 ::operator delete(),这里两个全局函数相当于C的malloc和free;
std::alloc 设计了双层配置器,第一层直接用了malloc和free,内存不足时用函数指针模拟C++中set_new_handler方式进行自定义处理函数;第二层:超过128bytes时,便使用第一层配置器,小于128bytes则采用memory pool方式。
1,第一级配置器 __malloc_alloc_template
用C模拟C++中set_new_handler方式处理oom的原因C++并未提供realloc()的内存重分配操作加其他历史原因。
static void *oom_malloc(size_t); static void *oom_realloc(void *, size_t); static void (* __malloc_alloc_oom_handler)(); static void (* set_malloc_handler(void (*f)())) () { void (* old)() = __malloc_alloc_oom_handler; __malloc_alloc_oom_handler = f; return (old); } // 初始值设为0,留给客户端自行配置(调用set_malloc_handler配置) void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler) () = 0;
第一级的allocate和deallocate其实就是malloc和free的简单封装和oom处理,oom处理即通过上面的设置的处理函数 来 反复调用 以及 malloc或realloc来分配内存知道成功;如果不设置__malloc_alloc_oom_handler,直接__THROW_BAD_ALLOC抛出bad_alloc,直接终止程序。
2,第二级配置器__default_alloc_template
小于128bytes通过第二级配置器分配内存,避免太多小额区块造成内存碎片。
自由链表数据结构,free_lists管理的大小分别为8,16,24,…,128bytes,共16个链表结点。
自由链表结点如下,
union obj { union obj *free_list_link; char client_data[1]; };
union节省了额外内存负担,未使用时视为obj指针,指向相同形式的另一个obj;实际使用时,视为char 指针,指向实际区块。
__default_alloc_template的ROUND_UP比较有技巧性,避免除法的使用,改用位运算上调至8的倍数。
enum { __ALIGN = 8 }; //小区块的上调边界 // 将bytes上调至8的倍数 // testcase: 比如传入 bytes = 7;(0b1110 & ~7), ~7 = -8, 负数的二进制特殊0b...11111000(高位均为1), &操作的结果=8,完美避免除法的计算。 static size_t ROUND_UP (size_t bytes) { return (((bytes) + __ALIGN - 1) & ~(__ALIGN - 1)); }
2.1 allocate函数
- 1,大于128bytes,直接跳转至第一级分配器;
- 2,如上图,根据传入的大小找到free_lists中合适的区块;
- 3,返回值设置为第一个可用指针;
- 4,my_free_list指向下一个可用区块;
2.2 deallocate(void *p, size_t n)函数
- 1,大于128bytes,直接跳转至第一级分配器;调用释放函数;
- 2,临时指针q指向传入的需要释放的p;
- 3,如上图,根据传入的大小找到free_lists中合适的区块;
- 4,q->free_list_link指向my_free_list的可用区块;
- 5,my_free_list再指回q,即q再为可用区块,相当于回收(并未真正释放,只是再次纳入可用,下次直接可用于其他的数据分配)。
2.3 重新填充refill函数
前面的allocate函数失败时,会调用refill重新填充free_lists中传入大小的槽位的空间,通过调用chunk_alloc。如果只取到一个区块,直接返回;如果多个需要一个个串接起来(obj->free_list_link循环指针操作)。详细说明下面的代码注释。
... void* refill(size_t n) { ... //取到多个区块后循环指针操作 my_free_list = free_list + FREELIST_INDEX(n); result = (obj *)chunk; //这一块准备返回给客户端,即已经将要被使用,不再被纳入free范畴 *my_free_list = next_obj = (obj *)(chunk + n); //free范畴的区块,从下一块开始 //第一个i = 0已经是将要被客户端使用的区块,从1开始 for (int i = 1; ; i++) { current_obj = next_obj; next_obj = (obj *)((char *)next_obj + n); //最后一个指向空指针 if (nobjs - 1 == i) { current_obj->free_list_link = 0; break; } else { //串接各个区块 current_obj->free_list_link = next_obj; } } return result; }
2.4 内存池取空间给free_list,chunk_alloc(size_t size, int &nobjs) 函数
内存池可用空间通过size_t bytes_left = end_free - start_free; 得来;
- 1,第一种情况内存池剩余空间满足需求,直接返回start_free,再更新start_free += total_bytes; 的地址,nobjs不变,等于默认值20;
- 2,第二种情况内存池剩余空间不足以满足全部需求,但能满足一个以上的需求;做除法,能满足几个是几个,其他同情况一,唯一修改nobjs的地方,不足20时,实际分配的数量;
- 3,第三种情况内存池连一个区块的大小都无法提供;
- 首先从内存池中找残余的空间,调整free list,将内存池中残余空间编入;
- 分配两倍于需求量 + 随配置次数增加的附加量
//首先设置将要分配的更多的内存以补充内存池的区块大小 size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
- 如果分配失败,循环检测内存池各大小区块可用的空间,如图中的 #7 (64bytes) 从 32bytes末端取内存池中残余空间;递归调用chunk_alloc,最终会在1或2中终止,nobjs <= 20;
如果更严重的情况,到处都没有内存可用,调用第一级配置器,其中有oom处理机制; - 如果成功,如图96bytes对应20个区块,剩余的加入内存池,供后续使用。递归调用chunk_alloc,最终会在1或2中终止,nobjs <= 20。
小结
std::alloc 设计了双层配置器,超过128bytes时,便使用第一层配置器,简单封装malloc和free,oom处理函数等;小于128bytes通过第二级配置器分配内存,内存池方式处理小块内存,避免太多小额区块造成内存碎片。
参考
《STL 源码剖析》