【STL】SGI空间配置器 Allocator

       本篇将主要总结归纳《STL源码剖析》的空间配置器的相关STL实现。在此之前,我们也将总结归纳一些基本的C++知识和技法。


一、C++基础知识--3种new的方式

       在C++中,包含3种new形式,分别是
  •  new / delete
  •  operator new / operator delete
  •  placement new


1. new, operator new,  delete, operator delete

       在C++中,我们所习惯的C++内存配置操作和释放操作是如下这样的:
class Foo{ ... };
Foo *pf = new Foo;
delete pf

       这里的new即为上述的第一种情况,也称new operator。他通常包含如下3个操作:
  • new: 调用 ::operator new 分配内存                      delete:  调用 Foo::~Foo()
  •         调用 Foo::Foo()构造对象内容                                   调用 ::operator delete 释放内存
  •         返回相应的指针
       new operator 和 delete operaror 是对应在堆中内存的申请和释放,且二者无法被重载。

       operator new/ operator delete 是可以根据operator+一样进行重载的。其工作的方式如下:
  • 只分配所要求的空间,不调用对象的构造函数。当无法满足所要求分配空间时,则有 new_handler,则调用new_handler, 否执行bad_alloc(如果有抛出异常的请求),否则返回0.
  • 同时,重载时,返回类型必须是void* ,重载时,第一个参数必须是表达要求分配空间的大小,类型是size_t,重载可以带其他参数。
#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;
}
运行结果如下:
【STL】SGI空间配置器 Allocator_第1张图片
这里的 ::operator new 和 ::operator delete 对应于C语言就是 malloc和free,只进行内存分配,而且必须配套使用。


2. placement new

       placement new 是重载operator new的一个标准的,全局的版本。函数原型如下:
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进行构造。
另外有三种方式进行提前分配缓存
  • 在堆上进行分配,也就是 char *buf = new char[sizeof(Foo)];
  • 在栈上进行分配  char buf[N * sizeof(Foo)];
  • 直接通过地址进行, void* buf = reinterpret_cast

二、STL空间配置器allocator

1. SGI特殊的空间配置器,std::alloc

       正如上面new运算符包含2个操作过程,包括分配内存空间,释放内存。STL allocator将这两阶段区分开,内存配置由alloc::allocate()负责, 内存释放由alloc::deallocate()负责,对象构造由 ::contruct() 负责, 对象析构由 ::destroy() 负责。
【STL】SGI空间配置器 Allocator_第2张图片

2. 构造和析构基本工具: construct() 和 destroy()

【STL】SGI空间配置器 Allocator_第3张图片
       上述的construct()接受一个指针p和一个初值value,该函数的用途就是将初值设定到所指的空间上,也就是上述所受的placement new。
       同时上述利用value_type()获得所指迭代器所指对象的型别,再利用__type_traits判断该型别的析构函数是不是trivial的。non trivial即至关重要,需要调用析构函数,进行释放,如果是trivial的,即如数值类别的,则不需要。这样的设定是为了能够提高效率。
#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*)()


3. 空间的配置和释放  std::alloc

对象构造前的空间配置和对象析构后的空间释放,由负责,主要考虑以下几个准则
  • 向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)); }
};


【STL】SGI空间配置器 Allocator_第4张图片

【STL】SGI空间配置器 Allocator_第5张图片


3.1 第一级配置器__malloc_alloc_template

        第一级配置器以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()。


3.2 第二级配置器 __default_alloc_template

        第二级配置器是为了避免太多小额区块造成的内存碎片而存在的。当区块大于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]; 
};

        上说,此结构一物二用的结果是,不会为了维护链表所必须的指针而造成一种内存浪费。上述用到了 柔性数组 的概念。这里的前4字节用来存储指向的地址。这里的一物二用,是指,client_data指向的是当前首地址,同时也可以直接通过obj.client_data获取内容。而free_list_link本身可以看做一个指针,指向一个obj。需要注意的是,client_data里的值可以是0或者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;
}
运行结果如下:
【STL】SGI空间配置器 Allocator_第6张图片


        回到正文。
               【STL】SGI空间配置器 Allocator_第7张图片
        以下是第二级配置器的实现源码:

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, };

         以下是 空间配置函数allocate()的实现:
//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);
};

相应的示意如下:
【STL】SGI空间配置器 Allocator_第8张图片

        空间释放函数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;
    
}
【STL】SGI空间配置器 Allocator_第9张图片

重新填充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));
    }
}
【STL】SGI空间配置器 Allocator_第10张图片

参考文献:

  • 侯捷, STL源码剖析 华中科技大学出版社
  • ww32cc的博客  http://blog.csdn.net/ww32zz/article/details/48343689
  • 梦中乐园 柔性数组 http://www.cppblog.com/Dream5/articles/148386.html
  • 阿凡卢  C++中的new, operator new 和 placement new http://www.cnblogs.com/luxiaoxun/archive/2012/08/10/2631812.html



你可能感兴趣的:(【STL】)