深度剖析STL空间配置器

STL空间配置器:

  • 空间配置器(allocator),顾名思义就是用来配置,管理,分配,释放内存空间,它隐藏在一切容器的背后,默默工作,默默付出。
1.空间配置器为什么不叫内存配置器:

因为空间不一定是内存,配置的空间可以是内存也可以是硬盘,额是的,SGI STL提供的空间配置器是内存配置的对象,是内存

2.为什么要有空间配置器:
  • 小块内存带来的内存碎片问题
    单从分配的角度来看。由于频繁分配、释放小块内存容易在堆中造成外碎片(极端情况下就是堆中空闲的内存总量满足一个请求,但是这些空闲的块都不连续,导致任何一个单独的空闲的块都无法满足这个请求)。

  • 小块内存频繁申请释放带来的性能问题。
    关于性能这个问题要是再深究起来还是比较复杂的,下面我来简单的说明一下。
    开辟空间的时候,分配器会去找一块空闲块给用户,找空闲块也是需要时间的,尤其是在外碎片比较多的情况下。如果分配器其找不到,就要考虑处理假碎片现象(释放的小块空间没有合并),这时候就要将这些已经释放的的空闲块进行合并,这也是需要时间的。

3.STL标准的 空间配置器:`std::allocator`

虽然SGI也定义有一个符合部分标准、名为allocator的配置器,但SGI自己从未使用过它,也不建议我们使用。
主要原因是效率不佳,它只是把C++的::operator new和::operator delete做一层薄薄的包装而已。下面是SGI的std:allocator
的源码:

G++ 2.91.57,include\g++\defalloc.h 完整列表
// 我们不赞成包含此文件,这是原始的HP default allocator. 提供它只是为了
// 回溯兼容
//
// DO NOT USE THIS FILE  不要使用这个文件,除非你手上的容器是以旧式做法
// 完成——那就需要一个拥有HP-style interface 的空间配置器。SGI STL使用
// 不同的 allocator 接口。SGI-style allocator 不带有任何与对象型别相关
// 的参数;它们只响应 void* 指针(侯捷注:如果是标准接口,就会响应一个
// “指向对象型别” 的指针,T*),此文件并不包含于其他任何SGI STL 头文件中

#ifndef DEFALLOC_H
#define DEFALLOC_H

#include 
#include 
#include 
#include 
#include 
#include 

template <class T>
inline T* allocate(ptrdiff_t size, T*){
    set_new_handler(0);
    T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
    if (tmp == 0){
        cerr << "out of memory" <exit(1);
    }
    return tmp;
}

template <class T>
inline void deallocate(T* buffer){
    ::operator delete(buffer);
}

template <class T>
class allocator{
public:
    typedef T value_type;
    typedef T* pointer;
    typedef const T* const_pointer;
    typedef T& reference;
    typedef const T& const_reference;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;

    pointer allocate(size_type n){
        return ::allocate(difference_type)n, pointer(0));
    }
    void deallocate(pointer p){ ::deallocate(p); }
    pointer address(reference x){ return (pointer)&x; }
    const_pointer const_address(const_reference x){ 
        return (const_pointer)&x;
    }
    size_type init_page_size(){
        return max(size_type(1),size_type(4096/sizeof(T)));
    }
    size_type max_size(){
        return max(size_type(1),size_type(UNIT_MAX/sizeof(T)));
    }
};

#endif
4.SGI特殊的空间配置器,`std::alloc`
二级空间配置器:

二级空间配置器使用内存池+自由链表的形式避免了小块内存带来的碎片化,提高了分配的效率,提高了利用率。SGI的做法是先判断要开辟的大小是不是大于128,如果大于128则就认为是一块大块内存,调用一级空间配置器直接分配。否则的话就通过内存池来分配,假设要分配8个字节大小的空间,那么他就会去内存池中分配多个8个字节大小的内存块,将多余的挂在自由链表上,下一次再需要8个字节时就去自由链表上取就可以了,如果回收这8个字节的话,直接将它挂在自由链表上就可以了。
为了便于管理,二级空间配置器在分配的时候都是以8的倍数对齐。也就是说二级配置器会将任何小块内存的需求上调到8的倍数处(例如:要7个字节,会给你分配8个字节。要9个字节,会给你16个字节),尽管这样做有内碎片的问题,但是对于我们管理来说却简单了不少。因为这样的话只要维护16个free_list就可以了,free_list这16个结点分别管理大小为8,16,24,32,40,48,56,64,72,80,88,86,96,104,112,120,128字节大小的内存块就行了。
- 二级空间配置器的逻辑步骤:
深度剖析STL空间配置器_第1张图片
假如现在申请n个字节:
1、判断n是否大于128,如果大于128则直接调用一级空间配置器。如果不大于,则将n上调至8的倍数处,然后再去自由链表中相应的结点下面找,如果该结点下面挂有未使用的内存,则摘下来直接返回这块空间的地址。否则的话我们就要调用refill(size_t n)函数去内存池中申请。
2、向内存池申请的时候可以多申请几个,STL默认一次申请nobjs=20个,将多余的挂在自由链表上,这样能够提高效率。
进入refill函数后,先调chunk_alloc(size_t n,size_t& nobjs)函数去内存池中申请,如果申请成功的话,再回到refill函数。
这时候就有两种情况,如果nobjs=1的话则表示内存池只够分配一个,这时候只需要返回这个地址就可以了。否则就表示nobjs大于1,则将多余的内存块挂到自由链表上。
如果chunk_alloc失败的话,在他内部有处理机制。
3、进入chunk_alloc(size_t n,size_t& nobjs )向内存池申请空间的话有三种情况:
3.1、内存池剩余的空间足够nobjs*n这么大的空间,则直接分配好返回就可以了。
3.2、内存池剩余的空间leftAlloc的范围是n<=leftAlloc则这时候就分配nobjs=(leftAlloc)/n这么多个的空间返回。
3.3、内存池中剩余的空间连一个n都不够了,这时候就要向heap申请内存,不过在申请之前先要将内存池中剩余的内存挂到自由链表上,之后再向heap申请。
3.3.1、如果申请成功的话,则就再调一次chunk_alloc重新分配。
3.3.2、如果不成功的话,这时候再去自由链表中看看有没有比n大的空间,如果有就将这块空间还给内存池,然后再调一次chunk_alloc重新分配。
3.3.3、如果没有的话,则就调用一级空间配置器分配,看看内存不足处理机制能否处理。

  • 第一级空间配置器 __malloc_alloc_template 剖析:
// 以下是第一级配置器 
// 注意,无“template型别参数”。至于 “非型别参数” inst,则完全没派上用场
template <int inst>
class __malloc_alloc_template{
private:
// 以下函数将用来处理内存不足情况
// oom:out of memory.
static void *oom_malloc(size_t);
static void *oom_realloc(void*, size_t);
static void (* __malloc_alloc_oom_handler)();
public:
static void * allocate(size_t n)
{
    void *result = malloc(n); //第一级配置器直接使用malloc()
    // 以下无法满足需求时,改用 oom_malloc()
    if ( result == 0) result = oom_malloc(n);
    return result;
}
static void deallocate(void *p, size_t /*n*/)
{   
    free(p); // 第一级配置器直接使用 free()
}
static void *reallocate(void *p, size_t /*old_sz*/,size_t new_sz)
{
    void* result = realloc(p,new_sz); // 第一级配置器直接使用realloc()
    // 以下是无法满足需求时,改用oom_realloc()
    if(result == 0) result == oom_realloc(p,new_sz);
    return result;
}

// 以下是仿真C++的set_new_handler().换句话说,你可以通过它
// 指定你自己的out_of_memory handler
static void (* set_malloc_handler(void (*f)()))()
{
    void (* old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_hander = f;
    return (old);
}
};
// malloc_alloc out-of-memeory handling
// 初值为0,有待客户端设定
template <int inst>
void (* __malloc_alloc_template::__mallocalloc_oom_handler)() = 0;

template <int inst>
void* __malloc_alloc_template::oom_malloc(size_t n)
{
    void (* my_malloc_handler)();
    void *result;

    for(;;){        // 不断尝试释放,配置,再释放,配置...
        my_malloc_hander = __malloc_alloc_oom_handler;
        if (my_malloc_handler == 0){ __THROW_BAD_ALLOC; }
        (*my_malloc_handler)(); // 调用处理例程,企图释放内存
        result = malloc(n);    // 再次尝试配置内存
        if ( result == 0) return result;
    }
}

template <int inst>
void* __malloc_alloc_template::oom_realloc(size_t n)
{
    void (* my_malloc_handler)();
    void *result;

    for(;;){        // 不断尝试释放,配置,再释放,配置...
        my_malloc_hander = __malloc_alloc_oom_handler;
        if (my_malloc_handler == 0){ __THROW_BAD_ALLOC; }
        (*my_malloc_handler)(); // 调用处理例程,企图释放内存
        result = realloc(p,n);    // 再次尝试配置内存
        if ( result == 0) return result;
    }
// 以下直接将参数inst设置成0
typedef __malloc_alloc_template<0> malloc_alloc;
第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置,释放,重配置操作,并实现出类似
C++ new-handler的机制。当然,它并不能直接运用C++ new-handler机制,因为它并非使用`::operater new`
来配置内存

一般来说,我们所习惯的 C++ 内存配置和操作是这样的:
class Foo{…};
Foo* pf = new Foo;// 配置内存,然后构造对象
delete pf; // 将对象析构,然后释放内存
new有两阶段操作,(1)调用::operator new配置内存;(2)调用Foo::Foo()构造对象内容
delete也内含两阶段操作:(1)调用Foo::~Foo()将对象析构;(2)调用::operator delete 释放内存

SGI STL不使用new来配置内存的原因:一是历史因素,另一个原因是 C++ 并未提供相应的realloc()的内存配置操作
________
注意,SGI第一级配置器的 allocate() 和 realloc() 都是在调用 malloc() 和 realloc() 不成功后,改调用 
oom_malloc()和 oom_realloc()。后两者都有内循环,不断调用 “内存不足处理流程”,期望在某次调用之后,获得
足够的内存而圆满完成任务,但如果 "内存不足处理例程"并未被客户端设定,oom_malloc() 和 oom_realloc() 便
毫不客气的调用 _THROW_BAD_ALLOC,丢出bad_alloc异常信息,或利用exit(1)硬生生终止程序。


  • 第二级空间配置器 __default_alloc_template 剖析:

     如果申请的空间小于128byte时,则以内存池(`memory pool`)管理:
         每次配置一大块内存,并维护对应之自由链表(free-list)。下次若再有相同大小的内存需求,就直接
         从free-list中拔出,如果客端释还小额区块,就由配置器回收到free-list中。
    
//以下是第二级配置器的实现内容:
enum {__ALIGN = 8};  // 小型区块的上调边界
enum {__MAX_BYTES = 128};  // 小型区块的上限
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};  // free-lists 个数

template <bool threads, int inst>
class _default_alloc_template{
private:
//ROUND_UP() 将 byte 上调至8的倍数
    static size_t ROUND_UP(size_t bytes){
        return (((bytes)  + __ALIGN - 1) & ~(__ALIGN - 1));
    }
private:
    union obj {  // free-lists的节点构造
        union obj * free_list_link;
        char client_data[1];  /* The client sees this */
    };
private:
    // 16个free-lists
    static obj * volatile free_list[__NFREELISTS];
    // 以下函数根据区块大小,决定使用第n号free-list. n 从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”的区块
    // 如果objs 个区块有所不便,nobjs可能会降低
    static char *chunk_alloc(size_t size, int &nobjs);

    // Chunk allocation state
    static char * start_free; // 内存池起始位置。只在chunk_alloc()中变化
    static char *   end_free; // 内存池结束位置。只在chunk_alloc()中变化
    static size_t heap_size;
public:
    static void * allocate(size_t n){/*详述于后*/}
    static void * deallocate(void *p, size_t n){/*详述于后*/}
    static void * reallocate(void *p, size_t old_sz, size_t new_sz){/*详述于后*/}
};


  1. 空间配置函数allocate()

身为一个配置器,__default_alloc_template拥有配置器的标准接口函数allocate(). 此函数首先判断区块大小,大
来用,如果没有可用的区块,就讲区块大小上调至8倍数边界然后调用 refill(),准备为free list 重新填充空间。
refill() 在后面详细介绍。

`
“`C++
static void * allocate(size_t n)
{
obj* volatile * my_free_list;
obj* 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)
{
// 没找到可用的freelist,准备重新填充free list
void *r = refill(ROUND_UP(n));
return r;
}
// 调整free list
*my_free_list = result->free_list_link;
return (result);
}深度剖析STL空间配置器_第2张图片

2.重新填充 free_list

当发现free_list中没有可用区块了时,就调用refill(),为free_list重新填充空间,新的空间将取自
内存池(由chunk_alloc() 完成)。缺省取得20个新节点,但万一内存池空间不足,获得的节点数可能少
于20:
C++
// 返回一个大小为 n 的对象,并且有时候会为适当的free_list增加节点
// 假设 n 已经适当上调至 8 的倍数
template
void* __default_alloc_template::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;
// 如果只获得一个区块,这个区块就分配给调用者,free_list无新节点
if( nobjs == 1) return (chunk);
// 否则准备调整free_list 纳入新节点
my_free_list = free_list + FREELIST_INDEX(n);
// 以下在chunk空间内建立free_list
result = (obj *)chunk;
*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);
if(nobjs - 1 == i){
current_obj -> free_list_link = 0;
break;
}else
current_obj->free_list_link = next_obj;
}
return (result);
}

3.内存池(memory pool)

    从内存池中取空间给free_list使用,是chunk_alloc() 的工作:

你可能感兴趣的:(数据结构)