126.STL 之 空间配置器(allocator)

发布时间 2023-09-07 22:12:01作者: CodeMagicianT

126.STL 之 空间配置器(allocator)

1.SGI 标准的空间配置器,std::allocator

SGI也定义了一个符合部分标准,名为allocator的配置器,但是它自己不使用,也不建议我们使用,主要原因是效率不佳。

它只是把C++的操作符::operator new和::operator delete做了一层简单的封装而已。

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

由于SGI 标准的空间配置器只是把C++的操作符::operator new和::operator delete做了一层简单的封装,没有考虑到任何效率上的强化。

所以 SGI 出现了一个特殊的空间配置器,供 STL标准模板库使用。

通常,C++中用new操作符来分配内存都包括两个阶段:

(1)调用::operator new配置内存

(2)调用构造函数来构造对象内容

同理,delete操作也包括两个阶段:

(1)调用析构函数将对象析构

(2)调用::operator delete释放内存

为了精密分工,SGI STL allocator将两个阶段分开

内存配置操作由alloc:allocate负责,内存释放由alloc:deallocate负责;对象构造操作由::contructor()负责,对象析构由::destroy()负责。

配置器定义在头文件<memory>中,它里面又包括两个文件:

#include <stl_alloc.h>        // 负责内存空间的配置和器释放  
#include <stl_construct.h>        // 负责对象的构造和析构  

3.STL 空间配置器:

3.1STL 空间配置器解决的问题

1.内存碎片问题(外碎片)

内碎片:操作系统在分配给申请者未被利用的部分。产生原因:内存对齐(效率高);

外碎片:内存充足时但分配不出大块内存。产生原因:由于多次分配小块内存,将大块内存分割成小块;

2.频繁系统分配,释放小块内存,调用 malloc,delete 系统调用产生性能问题;

STL 空间配置器的实现机制:

3.2双层级的配置器

注:

当需要开辟的空间大于 128 bytes 时,视为“足够大”,调用一级配置器,直接调用 malloc,free;

当需要开辟的空间小于 128 bytes 时,视为“过小”,为了降低额外负担,调用二级空间配置器

A.一级空间配置器

//以下是第第一级配置器
template <int inst>
class __malloc_alloc_template 
{

private:
    //以下函数用来处理内存不足的情况
    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_alloc()来申请内存
        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()
        //当内存不足时,则调用内存不足处理函数oom_realloc()来申请内存
        if (0 == result) result = oom_realloc(p, new_sz);
        return result;
    }

    //设置自定义的out-of-memory handle就像set_new_handle()函数
    static void (*set_malloc_handler(void (*f)()))()
    {
        void (*old)() = __malloc_alloc_oom_handler;
        __malloc_alloc_oom_handler = f;
        return(old);
    }
};

template <int inst>
void (*__malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;  //内存处理函数指针为空,等待客户端赋值

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;            //设定自己的oom(out of memory)处理函数
        if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }         //如果没有设定自己的oom处理函数,毫不客气的抛出异常
        (*my_malloc_handler)();                                    //设定了就调用oom处理函数
        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; }    //如果自己没有定义oom处理函数,则编译器毫不客气的抛出异常
        (*my_malloc_handler)();                                //执行自定义的oom处理函数
        result = realloc(p, n);                                //重新分配空间
        if (result) return(result);                            //如果分配到了,返回指向内存的指针
    }
}

上面代码看似繁杂,其实流程是这样的:

1.我们通过allocate()申请内存,通过deallocate()来释放内存,通过reallocate()重新分配内存。

2.当allocate()或reallocate()分配内存不足时会调用oom_malloc()或oom_remalloc()来处理。

3.当oom_malloc() 或 oom_remalloc()还是没能分配到申请的内存时,会转如下两步中的一步:

(a)调用用户自定义的内存分配不足处理函数(这个函数通过set_malloc_handler() 来设定),然后继续申请内存!

(b)如果用户未定义内存分配不足处理函数,程序就会抛出bad_alloc异常或利用exit(1)终止程序。

看完这个流程,再看看上面的代码就会容易理解多了!

B.二级配置器 _ _default_alloc_template

第二级配置器的代码很多,这里我们只贴出其中的 allocate() 和 dellocate()函数的实现和工作流程(参考侯捷先生的《STL源码剖析》),而在看函数实现代码之前,我大致的描述一下第二层配置器配置内存的机制。

我们之前说过,当申请的内存大于 128 bytes时就调用第一层配置器。当申请的内存小于 128bytes时才会调用第二层配置器。第二层配置器如何维护128bytes一下内存的配置呢? SGI 第二层配置器定义了一个 free-lists,这个free-list是一个数组,如下图:

这数组的元素都是指针,用来指向16个链表的表头。这16个链表上面挂的都是可以用的内存块。只是不同链表中元素的内存块大小不一样,16个链表上分别挂着大小为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes的小额区块,图如下:

就是这样,现在我们来看allocate()代码:

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

    //要申请的空间大于128bytes就调用第一级配置
    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;
    }
    *my_free_list = result->free_list_link;
    return (result);
};

其中有两个函数我来提一下,一个是ROUND_UP(),这个是将要申请的内存字节数上调为8的倍数。因为我们free-lists中挂的内存块大小都是8的倍数嘛,这样才知道应该去找哪一个链表。另一个就是refill()。这个是在没找到可用的free list的时候调用,准备填充free lists。意思是:参考上图,假设我现在要申请大小为 56bytes 的内存空间,那么就会到free lists 的第 7 个元素所指的链表上去找。如果此时 #6元素所指的链表为空怎么办?这个时候就要调用refill()函数向内存池申请N(一般为20个)个大小为56bytes的内存区块,然后挂到 #6 所指的链表上。这样,申请者就可以得到内存块了。当然,这里为了避免复杂,误导读者我就不讨论refill()函数了。allocate()过程图如下:

学过链表的操作的人不难理解上图,我就不再讲解。下面看deallocate(),代码如下:

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;
    }
    //寻找对应的位置
    my_free_list = free_list + FREELIST_INDEX(n);
    //以下两步将待释放的块加到链表上
    q->free_list_link = *my_free_list;
    *my_free_list = q;
}

deallocate()函数释放内存的步骤如下图:

其实这就是一个链表的插入操作,也很简单。不再赘述!上面忘了给链表结点的结构体定义了,如下:

union obj
{
    union obj * free_list_link;
    char client_date[1]; 
};

4.标准库allocator类及其算法

allocator<T> a 定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存
a.allocate(n) 分配一段原始的、未构造的内存,保存n个类型为T的对象
a.deallocate(p, n) 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象,调用deallocate前,用户必须对每个在这块内存中创建的对象调用destroy
a.construct(p, args) args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象
a.destory(p) p为T*类型的指针,此算法对p指向的对象执行析构函数
#include <iostream>
#include <memory>

using namespace std;

class Example
{
public:
    Example() 
    {
        cout << "Example default constructor..." << endl;
    }
    Example(int x) : a(x) 
    {
        cout << "Example constructor..." << endl;
    }
    ~Example() 
    {
        cout << "Example destructor..." << endl;
    }
    int a = 0;
};

int main() 
{
    allocator<Example> alloc;

    // 使用allocate函数分配一块能够存放5个Example对象的内存空间
    Example* p = alloc.allocate(5);

    // 使用construct函数在第一个位置构造一个Example对象
    alloc.construct(p, 1);

    // 使用construct函数在第二个位置构造一个Example对象
    alloc.construct(p + 1, 2);

    // 使用destroy函数销毁第一个位置的Example对象
    alloc.destroy(p);

    // 使用deallocate函数释放分配的内存空间
    alloc.deallocate(p, 5);

    return 0;
}

输出:

Example constructor...
Example constructor...
Example destructor...

在这个例子中,我们首先创建了一个名为alloc的allocator对象,用于分配Example对象所需的内存。

然后,我们使用alloc.allocate(5)函数分配了一块能够存放5个Example对象的内存空间,并将返回的指针赋值给指针变量p。

接下来,我们使用alloc.construct()函数在刚刚分配的内存空间中构造了两个Example对象。第一个对象使用带参数的构造函数进行构造,并将参数值设置为1;第二个对象也使用带参数的构造函数进行构造,并将参数值设置为2。

然后,我们使用alloc.destroy()函数销毁了第一个位置的Example对象。最后,我们使用alloc.deallocate()函数释放了分配的内存空间。

需要注意的是,在使用allocator类进行内存分配和对象构造时,必须使用alloc.construct()函数来显式地调用构造函数进行对象的构造,并使用alloc.destroy()函数来显式地调用析构函数进行对象的销毁。

参考:STL 之 空间配置器(allocator)

浅析STL allocator