STL空间配置器详解-《STL源码剖析第二章学习笔记》

个人学习笔记,可能有点乱,有理解不对的地方可以给我留言
个人网站www.liujianhua.xyz

STL空间配置器:https://www.cnblogs.com/lang5230/p/5556611.html

空间配置器概括

空间配置器详解:

空间配置器的来源(allocator):STL容器底部并不是直接使用mallo和free抑或operator new和operator delete来控制内存的存取,主要是为了优化。STL的主旨就是方便快捷,然而在程序中总是会出现两个问题:1、内存碎片化,造成内存浪费;2、因为小块内存就去调动malloc,造成性能障碍;为了使STL更完美,就有了空间配置器。举例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvYHUW43-1633743701124)(C:\Users\31772\AppData\Roaming\Typora\typora-user-images\image-20211006222212719.png)]

图片来源:https://www.cnblogs.com/lang5230/p/5556611.html

稍微解释一下图片:系统是四字节对齐,也就是一次分配四个或四的倍数个字节,那假如我需要5个字节来储存数据,那么就不得不分配8个字节给我,就产生了3个内存碎片(内部碎片)//还有一种情况就是内存不连续,如图中的这个例子,明明有12个字节空闲内存,但是由于内存空间不连续,也就无法分配,造成浪费。

还不明白可以看:https://blog.csdn.net/tong5956/article/details/74937178或者https://blog.csdn.net/qq_22238021/article/details/80209062

普通用户一般不用考虑了解allocator,因为allocator在容器内部默默执行,我们看不见执行过程,例如我们一般的代码:

std:vector<int> vec;

展开后是这样:

template<class T,class Alloc=allocator<int>>
    class vector
    {
        Alloc data_allocator;
    }

也就是说,我们也可以指定我们自己的空间配置器——当我们需要设计我们自己的容器的时候

atd:vector<int,My_alloc<int>> vec;   //自定义空间配置器

当然,allocator不只是解决内存碎片化问题,也有内存分配失败的异常处理,多线程中的内存分配处理等等完善的功能,这都是大师们优化到极致的代码。注意,本文说的STL指的是SGI STL标准。

class Test{...};     //类定义
Test* a = new Test;  //配置内存,然后构造对象  allocate() + construct()
delete a;            //将对象析构,释放内存    destory() + deallcate()

为了解决内存碎片化问题,allocator拥有两层配置器,

一级配置器内部就直接使用malloc和free来进行内存处理

二级配置器则有两种做法,当程序索要的内存不大时,通过内部的内存池来分配;繁殖,索要的内存过大,就交给一级配置器处理,这里界限通常为128bytes

alovator的必要接口:

allocator::value_type

allocator::pointer

allocator::const_pointer

allocator::reference

allocator::const_reference

allocator::diffeerencr_type   //这些type接口暂时不用深入了解

allocator::rebind  //一个嵌套的模板(template),class rebind\<U>拥有唯一成员other,other是typedef,代表allocator\<U>.

allocator::allocator()   //default constructor

allocator::allocator(const allocator&)   //copy constructor

template \<class U> allocator::allocator(const alloctor<U\>&)   //泛化的copy constructor

allocator::~allocator()  //default constructor

pointer allocator::address(reference x) const   //返回某个对象的地址  a.address(x) == &(x)

const_pointer allocator::address(const_reference x) const   //返回某个对象的地址  a.address(x) == &(x)const

pointer allocator::allocate(size_type n,const void* = 0)  //配置空间,足以储存n个对象

void allocator::deallocate(pointer p,size_type n)  //归还配置的空间

size_type allocator::max_size() const  // 返回可成功配置的最大量

void allocator::construct(pointer p,const T& x)  //等同于new((const void*)p)  T(value)

void allocatoe::destory(pointer p)  //等同于p->~T()

设计一个简单的空间配置器:只说明个人觉得难理解的部分,源码在《STL源码剖析》p44

set_new_handler(0); //在配置空间失败时调用参数来进行处理

原型:new_handler set_new_handler(new_handler p) throw();

在set_new_handler(0)后紧跟operator new来配置空间,set_new_handler原型中的new_handler型参数p为一个函数指针,无返回值(void)无参数,该函数至少有以下几种功能:

>产生更多的内存,来使下一次的operator new成功

>安装另一个new_handler函数,目的也是获得更多的内存

>卸载new-handler,也就是传空指针给set_new_handler,然后下次调用就会报std::bad_alloc异常

>抛出std::bad_alloc或从std::bad_alloc继承的其他类型的异常

set_new_handler就像是异常处理一样,就介绍这些,更多可以看《effective C++》p49或者 set_new_handler

这里用0作为参数目的是获得一个不会抛出异常的new,以便于自己设计,更自由。

new§ T(value); //placement new在不使用更多空间的基础上进行数据赋值

p是指针,指向一个内存空间,可以把这个内存空间看成是缓冲器,plaxement new将在这个缓冲器上分配一个对象,返回值是被构造完成的这个对象的地址,被构造的对象的内容是(value),value一般也是一个对象。类似于reslloc函数,这样减少了分配空间所耗的时间,因为如果是普通的new,需要去内存里找符合的空间,内存没有的话还需要再分配。

这个简单的自定义空间配置器只能有限度地搭配PJ STL和RW STL、且完全无法用于SGI STL,所以大家在自己仿造是一定要注意环境~~~~~

std::allocator //SGI标准的空间配置器。不讲,因为不好,甚至SGI自己也不用

std:alloc //SGI的特殊空间配置器

C++普遍的内存配置和释放操作

class Foo{...};
Foo* pf = new Foo;  //配置内存,然后构造对象
delete pf;          //将对象析构,然后释放

new中有两个步骤:1、::operator new配置内存 2、调用FOO::Foo()构造

delete也有两个操作:1、调用Foo::~Foo()析构 2、调用::operator delete()释放内存

STL allocator将这两阶段操作分开,内存配置由alloc::allocatr()完成,内存释放有alloc::deallocate()完成;对象构造由::consytuct()完成,析构由::destory()完成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ciJpuTa-1633743701127)(C:\Users\31772\AppData\Roaming\Typora\typora-user-images\image-20211008072642207.png)]

construct()和destory:同样,只讲个人觉得不好理解的地方

construct()就使用placement new来初始化,而dectory()则有几个不同版本,用来分类处理,更高效

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rZDAKUUH-1633743701129)(C:\Users\31772\AppData\Roaming\Typora\typora-user-images\image-20211008075356876.png)]

在泛化版的destory()中,由判断是否是trivial destructor来进行不同操作,所以如果这些析构函数对程序来说影响不大,例如只占用了极少内存等,那么for循环调用析构函数来destory对象,就会对效率(时间)造成影响,那就不如不进行任何操作,反之,才析构。

空间的配置与释放:std::alloc

SGI以malloc()和free()完成内存的配置与释放::;考虑到了小型区块所可能造成的内存破碎问题,上面已经简单介绍了这两级配置器,是只开放一级还是同时开放二级,取决于_USE_MALLOC是否被定义(SGI STL未定义_USE_MALLOC) 注意:alloc不接受任何template型参数,无论alloc定义为哪一级,SGI都会对它再进行封装,使其接口符合STL规范。

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));}
        ststic void deallocate(T *p)
        	{Alloc::deallocate(p,sizeof(T));}
    }

SGI STL容器全部使用simple_alloc接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-47RmWIy6-1633743701132)(C:\Users\31772\AppData\Roaming\Typora\typora-user-images\image-20211008130523093.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LKzkEmqI-1633743701134)(C:\Users\31772\AppData\Roaming\Typora\typora-user-images\image-20211008130550920.png)]

__malloc_alloc_template(一级配置器)

函数:

void * allocate(size_t n) 一级配置器直接使用malloc(),malloc失败(各种原因)则调用oom_malloc(),返回申请的空间

void deallocate(void *p, size_t) 一级配置器直接使用free()

void * reallocate(void *p, size_t old_sz, size_t new_sz) 一级配置器直接使用realloc(),realloc()失败,在调用oom_realloc(),返回realloc的空间

set_malloc_handler 就是实现一个自己的set_new_handler,前面介绍了set_new_handler,不再介绍

oom_malloc() 会不断尝试释放、配置、再释放、再配置

oom_realloc() 也是会不断尝试、配置、再释放、再配置、和oom_malloc()之间的区别就是内部调用的函数不同,malloc()和realloc();

SGI不是由::operatoe new来配置内存,所以无法运用C++ new handler机制,new handler就是::operator new在执行失败后抛出std::bad_alloc异常之前会先调用指定的处理例程

__default_alloc_template(二级配置器)

二级配置器还有其他机制,用来处理内存碎片和配置内存时产生的额外负担(记录被配置内存状态),二级配置器结构为一个free_list表,有16个free_list,各自管理8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes的小区块,free_list的节点结构:

union obj    //联合
{
    union obj * free_list_link;
    char client_data[1];    //The client sees this.
};

这里不仔细看会以为又使用了一个free_list_link来链接,消耗了内存,其实不然,这里我们使用的是联合,对联合不甚了解的可以复盘一下。假如我们有四个节点A、B、C、D,此时都是空节点,程序可以随便调用,此时A节点的内存空间的头部储存着B的地址,即A.free_list_link == &B,C、D以此类推,将ABCD链接起来;然后程序调用了B空间,B空间被程序重写,其储存的C的地址信息也被抹去,我们就需要将A链接上C,也就是改变A的A.free_list_link == &C.这样就节省了空间。

部分代码解读:

((bytes + _ALIGN - 1) & ~(_ALIGN - 1));  //_ALIGN == 8;result:将bytes上调至8的倍数

详解:https://blog.csdn.net/lijun538/article/details/49202785

static void *refill(size_t n); 返回一个大小为n的对象,并可能加入大小为n的其他区块到free list

static char *chunk_alloc(size_t size, int &nobjs); 配置一块空间,容纳nobjs个大小size的区块,nobjs也可能降低

static char *start_free; 内存起始位置,只在chunk_alloc()中变化

static char*end_free; 内存池结束位置,只在chunk_alloc()中变化

static size_t heap_size;

空间配置函数allocate()详解

先判断需要区块大小,大于128bytes就调用一级配置器,小于则检查free list,有就拿来用,没有就refill()为free list重新填充空间,调整free list的步骤如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uQ3WBPi5-1633743701136)(C:\Users\31772\AppData\Roaming\Typora\typora-user-images\image-20211008205617200.png)]

空间释放函数deallocate()

判断须操作区块大小,大于128bytes就调用一级配置器,小于就找到对应的free list将区块回收,回收调整free list步骤如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sw9KiBhi-1633743701137)(C:\Users\31772\AppData\Roaming\Typora\typora-user-images\image-20211008210509127.png)]

重新填充free list:

refill()函数为free list填充空间,这些空间来自内存池(由chunk_alloc()完成,前面介绍了这个函数),获得20个新节点,如果内存池不够,可能少于20.

如果只获得了一个区块,直接把这个区块给调用者,free list无新节点,否则就准备调整free list,纳入新节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ORql0DsW-1633743701138)(C:\Users\31772\AppData\Roaming\Typora\typora-user-images\image-20211008221903612.png)]

**内存池(memorypool)**chunk_alloc()

内存池满足refill()需求,则直接从内存池里取;不能完全满足,但能提供一个及以上的区块,则重置nonjs为最大可提供的区块数,然后最大可能提供出去,内存池可能清零(可能会有碎片); 内存池连一个区块的大小都无法提供,先将内存池里残留的内存配给适当的free list,同时调整start_free。

然后配置heap空间来补充内存池

start_free = (char *)malloc(bytes_to_get); //bytes_to_get是需要获得的大小,一般为两倍调用者所需
//heap空间不足,malloc()失败
//首先搜索未使用且足够大的free list
for(i = size;i <= _MAX_BYTES;i += _ALIGN)//循环遍历16个free list
{
    my_free_list = free_list + FREELIST_INDEX(i); //my_free_list是这个free list的头
    p = *my_free_list;  //
    if(0 != p)   //free list内还有未用区块
    {
        *my_free_list = p->free_list_link;  //释放出一个未用区块,此处可看为p
        start_free = (char *)p;  //改变内存池的start_free,即增加内存池
        end_free = start_free + i;  //改变end_free
        return(chunk_alloc(size, nobjs));  //递归调用,为了修正nobjs(此时内存池已经有了空间)
    }
}

free list中也没有可用区块了,就调用一级配置器,看看out-of-memory机制是否能找到更多空间,即调用malloc_alloc::allocate() -> oom_malloc() -> my_handler机制,这时会抛出异常或者使内存不足的情况改善,然后再递归调用chunk_malloc(),修正nobjs

整体步骤如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yt98UFly-1633743701139)(C:\Users\31772\AppData\Roaming\Typora\typora-user-images\image-20211009082420245.png)]

内存基本处理工具uninitialized_copy(),uninitialized(),uninitialized_fill_n()

uninitialized_copy()在已经配置完成的内存区块上构造函数,要么就构造出所有必要元素,要么就什么也不构造

uninitialized_fill_n()源码:三个参数:迭代器first指向欲初始化空间的起始处;n表示初始化空间大小;x表示初值

首先取出迭代器first的类型,判断是否是POD类型,POD型可采用最有效率的初值填写手法(fill_n()),no-POD就采用construct()的安全方法

uninitialized_copy()源码:三个参数:迭代器first指向输入端的起始位置;迭代器last指向输入端的结束位置;迭代器result指向输出端(欲初始化空间)的起始位置

首先也是取出first的类型,判断是不是POD类型,是就调用copy(),不是就调用construct()一个一个构造,对于char* 和 wchar_t*可以直接采用memove,所以SGI设计了特化版本

uninitialized_fill()源码:三个参数:迭代器first指向输出端(欲初始化空间的起始处);迭代器last指向输出端的结束处;x表示初值。同样,是POD类型就采用fill(),否则construct()。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值