SGI STL考虑到小型内存区块的碎片问题,设计了双层级配置器,第一级配置直接使用malloc()和free();第二级配置器则视情况采用不同的策略,当配置区大于128bytes时,直接调用第一级配置器;当配置区块小于128bytes时,遍不借助第一级配置器,而使用一个memory pool来实现。究竟是使用第一级配置器还是第二级配置器,由一个宏定义来控制。SGI中默认使用第二级配置器。
第一级配置器实现的比较简单,调用malloc()申请内存,申请失败的时候,将抛出bad_alloc异常。下边我着重介绍下二级配置器的实现思路。
上边提到了第二级配置器视情况不同而采用不同的策略;其主要目的是避免太多小额区块造成内存的碎片。通常情况下,当配置区大于128bytes时,配置器视之为“足够大”而直接调用一级配置器,当配置区块小于128bytes时,就以内存池来管理,具体做法是:
sgi二级配置器会将任何小额区块的内存需求量上调至8的倍数,(例如需求是30bytes,则自动调整为32bytes),并且在它内部会维护16个free-list, 各自管理大小分别为8, 16, 24,…,128bytes的小额区块,这样当有小额内存配置需求时,直接从对应的free list中拔出对应大小的内存(8的倍数);当客户端归还内存时,将根据归还内存块的大小,将需要归还的内存插入到对应free list的最顶端。如下图:
图1
那么什么是free-list呢?
首先我们看下free-list的定义
上边free-list节点定义得非常巧妙,与普通链表节点采用struct不同,这里采用的是union;主要原因是由于为了维护free-list链表,每个节点需要额外的指针(指向下一个节点),这样就会造成一种内存浪费。为了避免这种负担,所以采用的union,根据union的特点,从第一个字段看,obj可以看作一个指针,指向链表中的下一个节点;从第二个字段看,obj也可以视为一个指针,不过是指向实际的内存区,如图1所示,这种一物两用的结果,就是不会为了维护链表所必须的指针而造成内存浪费。(因为stl容器是保存对象的,所以其自身信息当然要求是尽可能的少占用内存)。
下边我们根据一个图来讲下第二级空间配置器分配及归还内存区块的过程:
内存分配:
图2
如上,我们要申请96bytes的内存(由于其内部有自动调整机制,所以有可能89~95bytes的申请也会自动上调到96bytes), 首先确定需要在第几号free-list中获取,如图示是第11号free-list my_free_list,然后获取这个第一个元素(即第一块内存)的地址赋值给result,再调整my_free_list让其指向下一个区块。
内存归还:
图3
跟配置内存类似,同样是先寻找对应的free list,然后将要归还的内存块插入到对应free list的头部。
不过在内存配置的过程中,我们还需要处理这种情况:当对应free list没有可用区块时,就需要给对应的free list重新填充内存。如下图:
图4
//传回一个大小为 n的对象,并且有时候会为适当的freelist增加节点. //假设 n已经适当上调至 8的倍数。 template <bool threads, int inst> void* __default_alloc_template<threads, inst>::refill(size_t n) { int nobjs = 20; // 呼叫 chunk_alloc(),尝试取得 nobjs个区块做为 free list的新节点。 // 注意参数 nobjs是pass by reference。 char * chunk =chunk_alloc(n, nobjs); obj * volatile * my_free_list; obj * result; obj * current_obj, * next_obj; int i; 第 2 章空间配置器(allocator) // 如果只获得一个区块,这个区块就拨给呼叫者用,free list无新节点。 if (1 == nobjs) return(chunk); // 否则准备调整 free list,纳入新节点。 my_free_list = free_list + FREELIST_INDEX(n); // 以下在 chunk空间内建立freelist result = (obj *)chunk; //这一块准备传回给客端 // 以下导引 free list指向新配置的空间(取自记忆池) *my_free_list = next_obj = (obj *)(chunk + n); // 以下将 free list 的各节点串接起来。 for (i = 1; ; i++) {//从 1 开始,因为第 0 个将传回给客端 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); }
refill()函数完成的主要功能是:根据所需要申请的区块的大小n,调用chunk_alloc(n, nobjs); 默认申请nobjs个区块(默认为20,又可能不足20,nobjs是引用传递);如果申请的区块只有一个,那么直接返回,free list仍旧无可用区块,如果大于1,那么将第一个chunk块作为返回值,其余的chunk按照n划分为free list的节点,并将其串接到free list中区。最后free list头节点会指向到新分配的chunk。
综上,我便将stl二级空间配置的空间的配置,回收,以及重新填充大小详细介绍了一遍,下篇将继续介绍内存池的设计。