本篇将主要总结归纳《STL源码剖析》的空间配置器的相关STL实现。在此之前,我们也将总结归纳一些基本的C++知识和技法。
class Foo{ ... };
Foo *pf = new Foo;
delete pf
#include
#include
#include
#include
using namespace std;
class Foo
{
public:
Foo() { std::cout << "constructor of Foo" << std::endl; }
~Foo() { std::cout << "destructor of Foo" << std::endl;}
// override operator new
void* operator new(size_t sz, string str)
{
std::cout << "operator new size "<< sz
<<" with string "<< str << std::endl;
// allocate memory
return ::operator new(sz);
}
// override operator delete
void operator delete(void* ptr)
{
std::cout<<"operator delete" << std::endl;
// delete memory
::operator delete(ptr);
}
};
int main()
{
// new operator
Foo *pFoo = new("Foo class") Foo;
delete pFoo;
return 0;
}
运行结果如下:
void* operator new(size_t, void *p) throw() { return p; }
placement new允许用户将一个对象放置到一个特定的地方。
char *buf= new char[sizeof(Foo)];
// 在已有的内存地址上调用构造函数。
Foo* pi = new(buf) Foo;
所以,使用placement new的主要原因是,用new分配缓存时,调用默认构造函数,可能导致效率上的不佳。若没有默认的构造函数,则会发生编译错误。如果想在预分配的内存上创建对象,用缺省的new是行不通的,只能通过placement new进行构造。
#include // 使用placemen new
template
inline void construct(T1* p, const T2& value) {
//placement new, 调用T1::T1(value), p为已申请的内存缓存
new (p) T1(value);
}
// 以下为destroy()的第一版本,接受一个指针
template
inline void destroy(T* pointer){
// 这是placement new 的标准操作,先调用析构函数
pointer->~T();
}
// 以下是destroy() 的第二版本,接受两个迭代器,设法找出元素的数值型别
// 进而利用__type_traits<> 调用适当的操作
template
inline void destroy(ForwardIterator first, ForwardIterator last){
__destroy(first, last, value_type(first));
}
// 判断元素的数值型别(value_type)是否有trivial destructor,
template
inline void __destroy(ForwardIterator first, ForwardIterator last, T*)
{
typedef typename __type_traits::has_trivial_destructor trivial_destructor;
__destroy_aux(first, last, trivial_destructor);
}
// 如果元素的数值型别(value type)有nontrivial destructor, 就是需要显示调用析构函数
template
inline void
__destroy_auxx(ForwardIterator first, ForwardIterator last, __false_type){
for(; first < last; ++ first)
destroy(&*first);
}
// 如果元素的数值型别(value type)有trivial destructor, 比如数值类型
template
inline void __destroy_auxx(ForwardIterator, ForwardIterator, __true_type){}
// 以下是destroy() 对第二版本针对迭代器为char* 和 wchar_t* 的特化版本
inline void destroy(char*, char*)()
inline void destroy(wchar_t*, wchar_t*)()
对象构造前的空间配置和对象析构后的空间释放,由负责,主要考虑以下几个准则
- 向system heap 要求空间
- 考虑多线程(multi_thread)状态
- 考虑内存不足时的应变措施
- 考虑过多“小型区块”可能造成的内存碎片(fragment)的问题。 --> 二级配置器
SGI STL以malloc()和free()完成内存的配置和释放。
正如上面的准则4,SGI采用双层级配置器,第一级配置器直接使用malloc和free,用来配置大于128byte的区域块;第二级配置器采用memory pool管理方式来配置小的区域块。其中区域块利用free_list来进行管理8,16,24, ..., 128等共16个等级的小额区域块。
一二级配置器军采用以下接口进行包装以符合STL规格:
template
class simple_alloc { public: static T* allocate(size_t n) { return n == 0 ? 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)); } };
第一级配置器以malloc(), free(), realloc()等C函数执行实际的内存配置,释放,重配置等操作,并实现了类似C++ new_handler的机制。因为不是用C++的::operator new来分配内存的。这里的new handler机制,就是如果::operator new 无法完成任务,在丢出std::bad_alloc异常状态之前,会先调用客户端制定的处理历程。 其中,第一级配置器还分成两种情况,一是如果内存足够,直接调用allocate, deallocate, 和 reallocate,对应malloc, free, 和realloc。 当内存不足时,也就是out of memory, 就调用相应的oom_malloc(), oom_realloc()。
第二级配置器是为了避免太多小额区块造成的内存碎片而存在的。当区块大于128byte时,交给第一级配置器,否则,则以内存池(memory pool)管理,交给第二级配置器,又称次层配置(sub_allocation),其用一个自由链表free_list进行维护。配置器除了负责配置,也负责回收,同时也会将所有申请的小额区块主动上调到8的倍数,比如30 bytes->32 bytes。维护16个free_list分别管理16个不同区块,分别是8 bytes, 16 bytes, ..., 128 bytes,方便分配和回收。
这里维护free_list的节点数据结构如下:
union obj { union obj* free_list_link; char client_data[1]; };
运行结果如下:#include
#include #include #include using namespace std; struct Bar { int bar; char client_data[0]; }; Bar *pObj; int main() { char str1[] = "hello world"; pObj = (Bar*)malloc(sizeof(Bar) + strlen(str1) + 1); if(NULL != pObj) { pObj->bar = 1; strcpy(pObj->client_data, str1); } cout << "pObj: " << pObj->bar << " " << pObj->client_data << endl; free(pObj); return 0; }
enum {__ALIGN = 8}; //小型区块的上调边界
enum {__MAX_BYTES = 128}; //小型区块的上界
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //free-list个数
template
class __default_alloc_template {
private:
/*将bytes上调至8的倍数
用二进制理解,byte整除align时尾部为0,结果仍为byte;否则尾部肯定有1存在,加上
align - 1之后定会导致第i位(2^i = align)的进位,再进行&操作即可得到8的倍数
*/
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
private:
union obj { //free-list的节点
union obj * free_list_link;
char client_data[1]; /* The client sees this. */
};
private:
//16个free-lists
static obj * __VOLATILE free_list[__NFREELISTS];
//根据区块大小,找到合适的free-list,返回其下标(从0起算)
static size_t FREELIST_INDEX(size_t bytes) {
return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}
//返回一个大小为n的对象,并可能编入大小为n的区块到相应的free-list
static void *refill(size_t n);
//配置一大块空间,可容纳nobjs个大小为“size”的区块
//如果配置nobjs个区块有所不便,nobjs可能会降低
static char *chunk_alloc(size_t size, int &nobjs);
//Chunk allocation state
static char *start_free;
static char *end_free;
static size_t heap_size;
public:
static void * allocate(size_t n);
static void * deallocatr(void *p, size_t n);
static void * reallocate(void *p, size_t old_sz, size_t new_sz);
};
//以下是static data member的定义与初值设定
template
char * __default_alloc_template::start_free = 0;
template
char * __default__alloc_template::end_free = 0;
template
size_t __default_alloc_template::heap_size = 0;
template
__default_alloc_template::obj * volatile
__default_alloc_template::free_list[__NFREELISTS] =
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
//n must be > 0 static void * allocate(size_t n) { obj * __VOLATILE * my_free_list; obj * __RESTRICT result; //大于128就调用第一级配置器 if (n > (size_t) __MAX_BYTES) { return(malloc_alloc::allocate(n)); } //寻找16个free-lists中适当的一个 my_free_list = free_list + FREELIST_INDEX(n); result = *my_free_list; if (result == 0) { //没找到可用的free-list,准备重新填充free-list void *r = refill(ROUND_UP(n)); return r; } //调整free-list,指向拨出区块的下一个区块 *my_free_list = result -> free_list_link; return (result); };
相应的示意如下:
空间释放函数deallocate()
首先判断区块大小,大于128就调用第一级配置器,否则就找出相应的free_list,将区块回收。// p不可以是0 static void deallocate(void *p, size_t n) { obj *q = (obj *)p; obj * volatile * my_free_list; // 大于128就调用第一级配置器 if(n > (size_t) __MAX_BYTES) { malloc_alloc::deallocate(p, n); return; } //寻找对应的free list my_free_list = free_list + FREELIST_INDEX(n); // 调整free list, 回收分块。 q->free_list_link = *my_free_list; *my_free_list = q; }
重新填充free lists refill()函数
当free_list中没有了可用区块时,为free list重新填充空间,新的空间从内存池(由 chunk_alloc()完成)。//返回一个大小为n的对象,并且有时候会适当的free-list增加节点 //假设n已经适当上调至8的倍数 template
void* __default_alloc_template ::refill(size_t n) { int nobjs = 20; //尝试获得nobjs个区块作为free-list的新节点 char * chunk = chunk_alloc(n, nobjs); obj * __VOLATILE * my_free_list; obj * result; obj * current_obj, * next_obj; int i; //如果只获得一个区块,这个区块就分配给调用者使用,free-list无新增区块 if (1 == nobjs) return(chunk); //否则调整free-list 纳入新节点 my_free_list = free_list + FREELIST_INDEX(n); //在chunk这段连续内存内建立free-list result = (obj *)chunk; //这一块准备返回给客户端 //将free-list指向新配置的连续内存空间 //allocate中my_free-list为0才进入本函数,故无需存储现在的*my_free-list,直接覆盖即可 *my_free_list = next_obj = (obj *)(chunk + n); //将free-list的各节点串接起来 for (i = 1; ; i++) { current_obj = next_obj; next_obj = (obj *)((char *)next_obj + n); //每一个区块大小为n if (nobjs - 1 == i) { //最后一块 current_obj -> free_list_link = 0; break; } else { current_obj -> free_list_link = next_obj; } } return(result); } 内存池(memory pool)中的chunk_alloc()
此函数用于给予free_list分配区块。注意,新加量的大小是原来的两倍,并再加上一个随着配置次数增加的越来越大的附加量。并且其中的nodjs采用pass by reference的形式,是因为参数将被修改为实际被分配的区域块大小。chunk_alloc取空间的原则如下:尽量从内存池中取,内存池不够了,才使用free-list中的可用区块。
//size此时已适当上调至8的倍数 template
char* __default_alloc_template ::chunk_alloc(size_t size, int& nobjs) { char * result; size_t total_bytes = size * nobjs; //8的倍数 size_t bytes_left = end_free - start_free; //8的倍数 if (bytes_left >= total_bytes) { //情况1 //内存池剩余空间完全满足需求量 result = start_free; start_free += total_bytes; return(result); } else if (bytes_left >= size) { //情况2 //虽不足以完全满足,但足够供应一个(含)以上的区块 //从start_free开始一共total_bytes分配出去,其中前size个bytes给客户端,剩余的给free-list nobjs = bytes_left/size; total_bytes = size * nobjs; result = start_free; start_free += total_bytes; return(result); } else { //内存池剩余空间连一个区块的大小都无法提供 size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4); // 以下尝试将内存池中的残余零头分配完 if (bytes_left > 0) { obj * __VOLATILE * my_free_list = free_list + FREELIST_INDEX(bytes_left); //找到大小相同区块所在的free-list ((obj *)start_free) -> free_list_link = *my_free_list; //将内存池剩余空间编入free-list中 *my_free_list = (obj *)start_free; } //此时内存池的空间已用完 //配置heap空间,用来补充内存池 start_free = (char *)malloc(bytes_to_get); if (0 == start_free) { //heap空间不足,malloc失败 int i; obj * __VOLATILE * my_free_list, *p; //转而从free-lists中找寻可用的区块(其大小够用) for (i = size; i <= __MAX_BYTES; i += __ALIGN) { my_free_list = free_list + FREELIST_INDEX(i); p = *my_free_list; if (0 != p) { //free-list尚有可用区块 //调整free-list以释出可用区块 *my_free_list = p -> free_list_link; start_free = (char *)p; //将改区块归还到内存池 end_free = start_free + i; //再次从内存池中索要连续空间来满足客户端需求 return(chunk_alloc(size, nobjs)); //由于此时i >= size,故此次只会进入情况1/2 } } end_free = 0; //没有可用区块归还到内存池,内存池仍为空 //调用第一级配置器,看out-of-memory机制是否能改善 start_free = (char *)malloc_alloc::allocate(bytes_to_get); } //内存池获得新的连续空间 heap_size += bytes_to_get; end_free = start_free + bytes_to_get; //再次尝试分配 return(chunk_alloc(size, nobjs)); } }