STL空间配置器

在 C++ 里,当我们调用 new 和 delete 进行对象的创建和销毁的时候,也同时会有内存配置操作和释放操作:

这其中的 new 和 delete 都包含两阶段操作:

  • 对于 new 来说,编译器会先调用 ::operator new 分配内存;然后调用 Obj::Obj() 构造对象内容。

  • 对于 delete 来说,编译器会先调用 Obj::~Obj() 析构对象;然后调用 ::operator delete 释放空间。

为了精密分工,STL allocator 决定将这两个阶段操作区分开来。

  • 对象构造由 ::construct() 负责;对象释放由 ::destroy() 负责。

  • 内存配置由 alloc::allocate() 负责;内存释放由 alloc::deallocate() 负责;

1. 构造construct和析构destroy

在 STL 里面,construct() 函数接受一个指针 P 和一个初始值 value,该函数的用途就是将初值设定到指针所指的空间上。

destroy() 函数有两个版本,第一个版本接受一个指针,准备将该指针所指之物析构掉。直接调用析构函数即可。第二个版本接受 first 和 last 两个迭代器,将[first,last)范围内的所有对象析构掉。

#include <new.h>//欲使用placement new。

//版本1
template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
    new (p) T1(value);
    //在分配好的内存中,重新构建对象,p指向分配好的内存,然后重新创建T1,通过T2初始化。
    //并调用T1::T1(value)构造函数
}

template <class T>
inline void destroy(T* pointer) {
    pointer->~T();
}   //直接调用对象里面的析构函数而已

//版本2,接受迭代器进行析构范围对象
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
    __destroy(first, last, value_type(first));
}

其中 destroy() 只截取了部分源码,全部实现还考虑到特化版本,比如判断元素的数值类型 (value type) 是否有 trivial destructor 等限于篇幅,完整代码请参阅《STL 源码剖析》。

2. 内存管理

考虑到小型区块可能造成的内存破碎问题,SGI 为此设计了双层级配置器。当配置区块超过 128bytes 时,称为足够大,使用第一级配置器,直接使用 malloc() 和 free()当配置区块不大于 128bytes 时,为了降低额外负担,直接使用第二级配置器,采用复杂的 memory pool 处理方式。无论使用第一级配接器(malloc_alloc_template)或是第二级配接器default_alloc_template),alloc 都为其包装了接口,使其能够符合 STL 标准。

#ifdef __USE_MALLOC
...
    
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc; // 令alloc为第一级配置器
#else

...
    
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; // 令alloc为第二级配置器 !_USE_MALLOC
#endif

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

其中, __malloc_alloc_template 就是第一级配置器,__default_alloc_template 就是第二级配置器。

其中 SGI STL 将配置器多了一层包装使得 Alloc 具备标准接口。

2.1 一级配置器

第一级配置器以 malloc(), free(), realloc() 等 C 函数执行实际的内存配置、释放和重配置操作,并实现类似 C++ new-handler 的机制(因为它并非使用 ::operator new 来配置内存,所以不能直接使用C++ new-handler 机制)。

SGI 第一级配置器的 allocate() 和 reallocate() 都是在调用malloc() 和 realloc() 不成功后,改调用 oom_malloc()oom_realloc()

/*******************第一级内存配置器模板***********************/
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搞起来
        if (0 == result) 
            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搞起来,扩大或缩小原分配内存空间。
        if (0 == result) 
            result = oom_realloc(p, new_sz);
        return result;
    }

    static void (* set_malloc_handler(void (*f)() ) )() // 回调函数,将自己的函数传进来,传进函数指针,返回函数指针,所以看起来比较复杂
    {
        void (* old)() = __malloc_alloc_oom_handler;
        __malloc_alloc_oom_handler = f;
        return(old);
    }

};

上面代码是第一级内存分配器,其实就是简单的封装了一下malloc和free。但是代码部分必须注意,首先成员全部是静态的,那么也就说明可以直接通过类调用成员函数而无需定义对象。

oom_mallocoom_realloc是指针函数,用来处理堆内存不足的情况。__malloc_alloc_oom_handler是函数指针,相当于回调函数,用户可以通过类成员函数set_malloc_handler设定此函数指针,当在内存分配不足的时候,此函数将被调用

oom_malloc()oom_realloc() 都有内循环,不断调用“内存不足处理例程”,期望某次调用后,获得足够的内存而圆满完成任务,如果用户并没有指定“内存不足处理程序”,这个时候便无力乏天,真的是没内存了,STL 便抛出异常。或调用exit(1) 终止程序。

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

    for (;;) { // 不断尝试释放、配置过程直到成功为止。
        my_malloc_handler = __malloc_alloc_oom_handler; // 可以用户自定义处理程序
        if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; } // 没有设置则抛出异常
        (*my_malloc_handler)(); // 调用异常处理例程,释放内存,用户定义。
        result = malloc(n); // 再次尝试分配内存。
        if (result) 
            return(result);
    }
}
template <int inst>
void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
{
    void (* my_malloc_handler)();
    void *result;

    for (;;) { // 同理,一样的做法。
        my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }
        (*my_malloc_handler)();
        result = realloc(p, n);
        if (result) 
            return(result);
    }
}

oom_malloc中当malloc失败的时候,也就堆中内存不足时候,通过此函数来榨取内存,首先会调用用户注册的回调函数__malloc_alloc_oom_handler()看是否可以释放处对应的内存,然后继续尝试malloc

2.2 二级配置器

简单来说 SGI第二级配置器的做法是:sub-allocation (层次架构):

2.2.1 自由链表free_list

一方面,自由链表中有些区块已经分配给了客户端使用,所以 free_list 不需要再指向它们;另一方面,为了维护 free-list,每个节点还需要额外的指针指向下一个节点。

那么问题来了,如果每个节点有两个指针?这不就造成了额外负担吗?本来我们设计 STL 容器就是用来保存对象的,这倒好,对象还没保存之前,已经占据了额外的内存空间了。那么,有方法解决吗?当然有!

union obj {//free_list节点构造
      union obj * free_list_link;
      char client_data[1];    /* The client sees this.        */
};

 

在 union obj 中,定义了两个字段,再结合上图来分析:

  • 从第一个字段看,obj 可以看做一个指针,指向链表中的下一个节点;

  • 从第二个字段看,obj 可以也看做一个指针,不过此时是指向实际的内存区。

2.2.1 实现源码

到这里,我们已经基本了解了第二级配置器中 free_list 的工作原理了。附上第二级配置器的部分实现内容源码:

template <bool threads, int inst>
class __default_alloc_template {
private:
    enum {__ALIGN = 8}; // 8字节对齐
    enum {__MAX_BYTES = 128}; // 上限
    enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; // free_list数目

    static size_t ROUND_UP(size_t bytes) {
        return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
    } // 将bytes上调至8的倍数
__PRIVATE:
    union obj { // free_list节点构造
        union obj * free_list_link;
        char client_data[1];    /* The client sees this.        */
    };
private:

    static obj * __VOLATILE free_list[__NFREELISTS]; // 16个free_lists,指针数组,free_list是一个指针指向的数据类型也是指针
    static  size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);
    } // 根据区块大小,决定使用第几个free_list。

    static void *refill(size_t n);
    static char *chunk_alloc(size_t size, int &nobjs); // 配置一大块空间,可容纳n个objs的大小的size区块

    static char *start_free; // 内存池起始
    static char *end_free; // 内存池结束
    static size_t heap_size; // 堆大小

public:
    static void * allocate(size_t n)
    {
        obj * __VOLATILE * my_free_list;
        obj * __RESTRICT result;

        if (n > (size_t) __MAX_BYTES) {//大于128字节,调用第一级配置器
            return(malloc_alloc::allocate(n));
        }
        my_free_list = free_list + FREELIST_INDEX(n); // 寻找16个free list中适当的一个

        result = *my_free_list; // 取出链表首节点
        if (result == 0) { // 如果对应链表为空,准备重新增加链表节点
            void *r = refill(ROUND_UP(n));
            return r;
        }
        *my_free_list = result -> free_list_link; // free_list[n]中更改为下一个节点地址
        return (result); // 将第一个节点地址返回给外部使用
    }

    // 释放内存,
    static void deallocate(void *p, size_t n)
    {
        obj *q = (obj *)p;
        obj * __VOLATILE * my_free_list;

        if (n > (size_t) __MAX_BYTES) {
            malloc_alloc::deallocate(p, n);
            return;
        }
        my_free_list = free_list + FREELIST_INDEX(n); // 寻找对应freelist
        q -> free_list_link = *my_free_list; // 调整freelist,回收区块
        *my_free_list = q;
    }

    static void * reallocate(void *p, size_t old_sz, size_t new_sz); // 声明成员函数

};

2.3 标准接口函数 allocate

2.3.1 空间申请

我们知道第二级配置器拥有配置器的标准接口函数 allocate()。此函数首先判断区块的大小,如果大于 128bytes –> 调用第一级配置器;小于128bytes–> 就检查对应的 free_list(如果没有可用区块,就将区块上调至 8 倍数的边界,然后调用 refill(), 为 free list 重新填充空间。

static void * allocate(size_t n)
{
    obj * __VOLATILE * my_free_list;
    obj * __RESTRICT result;

    if (n > (size_t) __MAX_BYTES) { // 大于128字节,调用第一级配置器
        return(malloc_alloc::allocate(n));
    }
    my_free_list = free_list + FREELIST_INDEX(n); // 寻找16个free list中适当的一个

    result = *my_free_list; // 取出链表首节点
    if (result == 0) { // 如果对应链表为空,准备重新增加链表节点
        void *r = refill(ROUND_UP(n));
        return r;
    }
    *my_free_list = result -> free_list_link; // free_list[n]中更改为下一个节点地址
    return (result); // 将第一个节点地址返回给外部使用
};

NOTE:每次都是从对应的 free_list 的头部取出可用的内存块。然后对free_list 进行调整,使上一步拨出的内存的下一个节点变为头结点。

2.3.1 空间释放

同样,作为第二级配置器拥有配置器标准接口函数 deallocate()。该函数首先判断区块大小,大于 128bytes 就调用第一级配置器。小于 128 bytes 就找出对应的 free_list,将区块回收

NOTE:通过调整 free_list 链表将区块放入 free_list 的头部。

2.3.3 重新填充 free_lists

发现 free_list 中没有可用区块时,就会调用 refill() 为free_list 重新填充空间。

  • 新的空间将取自内存池(经由 chunk_alloc() 完成)

  • 缺省取得20个新节点(区块),但万一内存池空间不足,获得的节点数可能小于 20

//模板中成员的定义,用于重新给链表分配空间
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
    int nobjs = 20; // 默认每个链表含有20个节点
    char * chunk = chunk_alloc(n, nobjs);
    obj * __VOLATILE * 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 = (obj *)chunk;
    *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); // 返回首节点
}

 

posted @ 2024-04-21 20:14  小熊酱  阅读(3)  评论(0编辑  收藏  举报