个人学习笔记,可能有点乱,有理解不对的地方可以给我留言
个人网站www.liujianhua.xyz
STL空间配置器:https://www.cnblogs.com/lang5230/p/5556611.html
空间配置器
- 空间配置器概括
- **alovator的必要接口:**
- **设计一个简单的空间配置器**:只说明个人觉得难理解的部分,源码在《STL源码剖析》p44
- **std:alloc //SGI的特殊空间配置器**
- **construct()和destory**:同样,只讲个人觉得不好理解的地方
- **空间的配置与释放:std::alloc**
- **__malloc\_alloc\_template(一级配置器)**
- **__default_alloc_template(二级配置器)**
- **空间配置函数allocate()详解**:
- **空间释放函数deallocate()**:
- **重新填充free list:**
- **内存池(memorypool)**chunk_alloc()
- **内存基本处理工具**uninitialized_copy(),uninitialized(),uninitialized_fill_n()
空间配置器概括
空间配置器详解:
空间配置器的来源(allocator):STL容器底部并不是直接使用mallo和free抑或operator new和operator delete来控制内存的存取,主要是为了优化。STL的主旨就是方便快捷,然而在程序中总是会出现两个问题:1、内存碎片化,造成内存浪费;2、因为小块内存就去调动malloc,造成性能障碍;为了使STL更完美,就有了空间配置器。举例:
图片来源: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()完成。
construct()和destory:同样,只讲个人觉得不好理解的地方
construct()就使用placement new来初始化,而dectory()则有几个不同版本,用来分类处理,更高效
在泛化版的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接口
__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的步骤如下:
空间释放函数deallocate():
判断须操作区块大小,大于128bytes就调用一级配置器,小于就找到对应的free list将区块回收,回收调整free list步骤如下:
重新填充free list:
refill()函数为free list填充空间,这些空间来自内存池(由chunk_alloc()完成,前面介绍了这个函数),获得20个新节点,如果内存池不够,可能少于20.
如果只获得了一个区块,直接把这个区块给调用者,free list无新节点,否则就准备调整free list,纳入新节点
**内存池(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
整体步骤如下
内存基本处理工具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()。