首页 > 其他分享 >126.STL 之 空间配置器(allocator)

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

时间:2023-09-07 22:11:24浏览次数:52  
标签:alloc STL void malloc free oom 内存 126 allocator

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

标签:alloc,STL,void,malloc,free,oom,内存,126,allocator
From: https://www.cnblogs.com/codemagiciant/p/17686219.html

相关文章

  • CF1266D
    原题翻译其实这题的翻译反而不如原题好理解,建议先阅读原题后重新思考做法         \[\large{\color{#ff0000}{\text{分割线}}}\]                        翻译把原来简单的东西复杂了原题的题意是有若......
  • STL学习笔记
    迭代器迭代器(iterator)是一种抽象的设计概念,现实程序语言中并没有直接对应于这个概念的实物。在<>一书中提供了23中设计模式的完整描述,其中iterator模式定义如下:提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。迭代器案例#include<ios......
  • 【C++STL基础入门】队列的基础使用
    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档@TOC前言C++标准模板库(STL)提供了一系列强大的容器和算法,方便我们在编程中处理数据和实现各种功能。其中,queue(队列)是STL中的一个重要容器,用于按照先进先出(FIFO)的顺序处理元素。本文将介绍queue的基础使用方法,帮助读者初......
  • STL(3) 分配器 allocator
    目录使用分配器使用分配器分配器并不在gnuc的标准库中,需要从ext中引入对分配器的效率进行测试直接使用分配器,参数的数字代表分配了多少大小的内存,这里凸显了分配器的一个弊端,就是分配内存的时候需要指定大小归还内存的时候也需要指定大小,造成编程上的困难......
  • STL标准模板之容器
    一、vector向量容器头文件:#include<vector>采用顺序结构存储数据,可以使用下标进行随机访问,有时候也叫数组容器(C++11中增加了array容器,定长数组容器,相比普通数组它是类类型,增加成员函数,提高安全性)vector是可变长的顺序表结构,可以自动扩容,容器中的元素存储在连续内存,支......
  • STL模版 -- day02
    一、deque双端队列容器头文件#include<deque>是下标顺序容器,它允许在首尾两端快速地插入、删除数据deque的元素不是全部相邻存储的:采用单独分配的固定大小数组的序列存储数据,以及额外的登记表(中控数组),该表中记录了所有序列的地址,这表示通过下标访问元素时必须经过......
  • STL(2)
    目录容器的分类array测试程序vector测试程序deque原理容器的分类序列式sequencecontainerarray固定长度vector能在后部插入元素deque两边都能插入元素,但是技术上不易描述list双向链表forward-list单向链表关联式associativecontainer实现一个快速的查找s......
  • servlet,jsp,jstl用到的依赖与brand.jsp简单案例
    2023-09-03<projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd">&......
  • JSTL基础部分
    在使用JSTL时记得正确引入了JSTL标签库<%@taglibprefix="c"uri="http://java.sun.com/jsp/jstl/core"%>jstlif标签判断test属性表示判断的条件(使用EL表达式输出)<br><c:iftest="${12==12}">正确<br></c:if>jstl多路判断<......
  • STL专题
    STL专题1.vector,变长数组,倍增的思想size()返回元素个数empty()返回是否为空clear()清空front()/back()push_back()/pop_back()begin()/end()[]支持比较运算,按字典序pair<int,int>first,第一个元素second,第二个......