STL二级空间配置器的实现
前言:
本文总结概括SGI STL源码中二级配置器的工作方式,主要是学习内存池+16条链表的内存分配方式。
无论一级配置器还是二级配置器,统一的接口如下:
//其中Alloc可能是一级配置器,也有可能是二级配置器; //这个类的作用其实就是单纯地转给一级或者二级配置器调用; template<class T, class Alloc> class simple_alloc { public: static T *allocate(size_t n) { return 0 == n? 0 : (T*) Alloc::allocate(n * sizeof (T)); } static T *allocate(void) { return (T*) Alloc::allocate(sizeof (T)); } static void deallocate(T *p, size_t n) { if (0 != n) Alloc::deallocate(p, n * sizeof (T)); } static void deallocate(T *p) { Alloc::deallocate(p, sizeof (T)); } };
SGI STL的容器全部使用这个simple_alloc提供的接口来分配内存。当容器需要内存放置元素的时候的空间来自于allocate()函数:
二级配置器几个重要的数据成员:
union obj { union obj * free_list_link; char client_data[1]; /* The client sees this. */ }; //16条链表,每条链表挂载了对呀大小的空间节点 static obj * free_list[__NFREELISTS]; //内存池的位置指针 static char *start_free; static char *end_free;
由于是静态变量,所以所有容器的配置器都共享这些变量。操纵的都是同一片空间。
从allocate()函数开始:
static void * allocate(size_t n) { //my_free_list是二级指针; obj * * my_free_list; obj * result; //如果索要的空间大小大于128字节就转而调用第一级配置器 if (n > (size_t) __MAX_BYTES) { return(malloc_alloc::allocate(n)); } //看索要的空间落在16条链表的哪条链表上; //my_free_list是指向那条链表的指针 my_free_list = free_list + FREELIST_INDEX(n); //result是那条链表的第一个节点的指针;指向union obj对象 result = *my_free_list; //如果这条链表中没有挂载这节点,则需要重新填充这条链表 if (result == 0) { void *r = refill(ROUND_UP(n)); return r; } //拿出头结点的空间给用户以后,头结点指向下一个; *my_free_list = result -> free_list_link; return (result); }
1.空间配置器维护了16条链表,每天链表串联着不同大小的空间。
2.空间配置器会先判断索要的空间(会提升为8的倍数)落在16条链表中的哪条,从这条链表中拔出一个节点空间给容器。
3.如果落在的链表中已经没有挂载节点,则会调用refill()函数填充这条链表。填充这条链表的空间来自于内存池或者堆内存;
refill()函数填充16条链表空间:
template <bool threads, int inst> void* __default_alloc_template<threads, inst>::refill(size_t n) { //默认为某条空间不足的链表填充20个节点; int nobjs = 20; char * chunk = chunk_alloc(n, nobjs); obj * * my_free_list; obj * result; obj * current_obj, * next_obj; int i; //如果只拿到了一个空间节点,那么直接返回给用户了,不再填充链表节点; if (1 == nobjs) return(chunk); my_free_list = free_list + FREELIST_INDEX(n); //result这一块空间返回给用户; result = (obj *)chunk; //剩余空间的起始节点由my_free_list和next_obj指正指向; *my_free_list = next_obj = (obj *)(chunk + n); //以下将剩余的每一块节点串联起来; for (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); }
1.refill()函数默认为某条没有空间节点的链表申请20个空间节点。其中的1个返回给用户(容器)
2.refill()函数还负责将申请到的这19个空间节点串联起来;
chunk_alloc()函数:
template <bool threads, int inst> char*__default_alloc_template<threads,inst>::chunk_alloc(size_t size, int& nobjs) { char * result; size_t total_bytes = size * nobjs; //内存池剩余空间量; size_t bytes_left = end_free - start_free; if (bytes_left >= total_bytes) { //如果内存池剩余空间的大小足够用户要求,则直接从内存池拿空间返回给用户; result = start_free; start_free += total_bytes; return(result); } else if (bytes_left >= size) { //如果内存池剩余空间的大小不够用户要求,但够一个节点的大小,此处有在修正实际能分配给用户的空间数目nobjs nobjs = bytes_left/size; total_bytes = size * nobjs; result = start_free; start_free += total_bytes; return(result); } else { //内存池剩余空间连一个节点的大小都没 //heap_size一开始为0,那么这里就是用户要求的空间大小的2倍 size_t bytes_to_get = 2 * total_bytes+ROUND_UP(heap_size >> 4); //如果内存池还有剩余空间大小,将剩余的空间按大小放入16条链表中,基本是头插法的思路; if (bytes_left > 0) { obj * * my_free_list = free_list + FREELIST_INDEX(bytes_left); ((obj *)start_free) -> free_list_link = *my_free_list; *my_free_list = (obj *)start_free; } //借由malloc想heap申请得到空间 start_free = (char *)malloc(bytes_to_get); if (0 == start_free) { //如果heap内存也不足,检查维护的16条链表是否有空间 int i; obj * * my_free_list, *p; for (i = size; i <= __MAX_BYTES; i += __ALIGN) { my_free_list = free_list + FREELIST_INDEX(i); p = *my_free_list; if (0 != p) { //如果链表内有剩余的空间节点,拿出这个节点给内存池维护; *my_free_list = p -> free_list_link; start_free = (char *)p; end_free = start_free + i; //递归调用自己去拿空间 return(chunk_alloc(size, nobjs)); } } //走到这里就是所谓的山穷水尽,到处都没内存可用; //这里的到处是指:malloc的堆空间没有、从维护的16条链表内也没找出多余剩余的空间节点; end_free = 0; //调用一级配置器,看看oom机制能做些什么; start_free = (char *)malloc_alloc::allocate(bytes_to_get); } heap_size += bytes_to_get; end_free = start_free + bytes_to_get; //递归调用自己,修正nobjs return(chunk_alloc(size, nobjs)); } }
1.chunk_alloc()函数会先从空间配置器维护的内存池中取空间。
2.如果内存池内的空间连一个链表的节点大小都提供不了,则会调用malloc函数向heap堆空间中申请内存;
3.如果heap堆空间内存不足,则会整理一遍维护的16条链表。将这些空间重新放回内存池。
4.如果16条链表里面也没有空间,则会调用out-of-memory的处理函数。
总结:
STL之所以要引入二级配置器的原因是为了减少频繁申请小区块空间导致的内存碎片问题,而且每次申请小区块空间就调用一次系统调用效率不高。因此引入了二级配置器。
此外还需要特别注意,因为内存池+16条链表这些资源都是以static静态变量的方式存在,故所有容器都可以看得到这些资源,因此二级配置器里面还有加锁的操作。