我们每个人只拥有当下,而对未来的真正慷慨也就是专注当下
基本概念
为什么叫allocator空间配置器,而不叫内存配置器?
因为空间不一定是内存,也可能是磁盘或其他辅助存储介质。可以写一个allocator,直接向硬盘取空间。
不过,实际上,我们最常用的就是用于配置内存。
空间配置器的标准接口
allocator标准接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 allocator::value_type allocator::pointer allocator::const_pointer allocator::reference allocator::const_reference allocator::size_type allocator::difference_type allocator::rebind 一个嵌套的(nested)class template。class rebind<U> 拥有唯一成员other, 那是一个typedef, 代表allocator<U>. allocator::allocator() default constructor allocator : :allocator(const allocator&) copy constructor template <class U > allocator : :allocator(const allocator<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 返回某个const 对象的地址. 算式a.address(x)等同于&x pointer allocator::allocate(size_type n, const void* = 0 ) 配置空间, 足以存储n个T对象. 第二参数是个提示. 实现上可能会利用它来增进区域性(locality), 或完全忽略之 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(x) void allocator::destroy(pointer p) 等同于p->~T()
两种空间配置器
SGI STL 有2个种空间配置器:
1)std::allocator,符合STL标准,但很少使用,也不建议使用。因为只是把::operator new和::operator delete做了一层薄薄封装,效率差。
2)std::alloc,SGI特殊空间配置器,将配置器分为两级,兼顾了效率与内存碎片问题,效率高。推荐使用。
下面主要讲的也是std::alloc。
空间配置器的职责
通常,我们习惯用new、delete对C++ 内存配置进行申请、释放操作。比如,
1 2 3 class Foo {...}Foo* pf = new Foo; delete pf;
其中,new操作内含2阶段操作:
1)调用::operator new配置内存。
2)调用Foo::Foo()构造对象。
delete操作也内含2阶操作:
1)调用Foo::~Foo()析构对象;
2)调用::operator delete释放内存;
而STL的allocator(空间配置器)把这两阶段操作分开了。其中,内存配置(申请)由alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由全局::construct()负责,对象析构由全局::destroy()负责。
配置器allocator文件说明
STL标准规定,配置器定义于<memory>
,而SGI <memory>
内含3个与配置器相关的文件:
1)<stl_construct.h> 定义了全局函数construct(), destroy(), 负责对象的构造和析构。隶属于STL标准规范。
2)<stl_alloc.h> 定义了一、二级配置器,彼此合作。配置器名为alloc。
3)<stl_uninitialized.h> 定义一些全局函数,用来填充(fill)或复制(copy)大块内存数据。都属于STL标准规范:
un_initialized_copy()
un_initialized_fill()
un_initialized_fill_n()
3个函数不属于配置器范畴,但与对象初值设置有关。对于容器的大规模元素初值设置很有帮助。这些函数对于效率都有面面俱到的考虑,最差情况下会调用construct()。最佳情况则会使用C标准函数memmove()直接进行内存数据的移动。
构造和析构工具:construct, destroy
全局函数construct(), destroy()在已配置内存的基础上,用于对象的构造和析构 。
因此,construct()需要原生内存(native memory)地址和要构造对象类型,可能包含初值(列表)用于构造对象作为参数。
destroy有两种形式:1)析构单个对象,提供对象指针即可;2)析构迭代器区间所有对象,提供迭代器区间[first, last)。
注意:如果是原始类型区间,如char* [start, end),则不需要析构,因为没有构建对象。
可以得到construct和destroy的模板函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 template <class T >inline void _Construct(T1* p) { new ((void *) p) T (); } template <class T1, class T2>inline void construct (T1* p, const T2& value) { new (p) T1 (value); } template <class T>inline void destroy (T* pointer) { pointer->~T (); } template <class ForwardIterator>inline void destroy (ForwardIterator first, ForwardIterator last) { __destroy(first, last, value_type (first)); } template <class ForwardIterator , class T >inline void __destroy(ForwardIterator first, ForwardIterator last, T*) { typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor; __destroy_aux(first, last, trivial_destructor ()); } template <class ForwardIterator >inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) { for (; first < last; ++first) destroy (&*first); } template <class ForwardIterator >inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __true_type) {} inline void destroy (char *, char *) { }inline void destroy (wchar_t *, wchar_t *) { }
代码中一组__destroy()是辅助实现destroy()的,编译器会在编译期根据萃取出参数的类型(value type)。接着萃取出trivial_destructor特性,判断是否支持trivial_destructor(平凡的析构函数,说明没有申请动态内存),如果支持(特性为__true_type),就不需要专门调用析构函数;如果不支持(特性为__false_type),就需要针对迭代器区间每个元素,逐个调用析构函数。
如果迭代器区间包含元素较多,这能节省不少时间。
空间配置与释放,std::alloc
std::alloc 负责内存的配置和释放 。对象构造前的空间配置和对象析构后的空间释放,由<stl_alloc.h>负责,SGI对此设计哲学:
向system heap要求空间。
考虑多线程(multi-threads)状态。
考虑内存不足时的应变措施。
考虑过多“小型区块”可能造成的内存碎片(fragment)问题。
本文为控制问题复杂度,以下讨论及源码,皆排除多线程。
C++的内存配置基本操作::operator new(),内存释放基本操作 ::operator delete()。这2全局函数相当于C的malloc()和free()。SGI正是以malloc()和free()完成内存的配置与释放的,但SGI STL的std::alloc不能使用operator new/delete,因为new/delete会直接构造/析构对象,而这不符合std::alloc职责。
std::alloc设计基本思想
为避免小型区块可能造成的内存碎片问题,SGI STL设计了双层级配置器:
1)第一级配置器,直接使用malloc(), free();
2)第二级配置器,视情况采用不同策略:
当配置区块 > 128bytes时,视为“足够大”,便调用第一级配置器;
当配置区块 <= 128bytes时,视为“过小”,交给memory pool(内存池)来管理,不再求助于第一级配置器。
设计可以只开放第一级配置器,可以同时开启。取决于__USE_MALLOC是否被定义(SGI STL并未定义__USE_MALLOC)。
SGI STL的两级配置器:__malloc_alloc_template是第一级配置器,__default_alloc_template是第二级配置器。alloc不接受任何template型别参数。并且,SGI还在此基础上,用simple_alloc包装了一个接口,对用户屏蔽内部细节,使之符合STL规格。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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)); } };
除了转发调用,simple_alloc包装配置器还将接口的配置单位,由byte转换成了元素的大小(sizeof(T))。SGI STL容器全部使用这个simple_alloc接口。
例如,vector的专属空间配置器data_allocator,就是simple_alloc<value_type, Alloc>,当然Alloc取决于我们传入vector的配置器类型(一级或二级),缺省是std::alloc。
1 2 3 4 5 6 7 8 9 10 11 12 13 template <class T , class Alloc = alloc> class vector {... protected : typedef simple_alloc<value_type, Alloc> data_allocator; void reserve (size_t n) { if (...) data_allocator::deallocate (start, end_of_storage - start); } };
第一级配置器:__malloc_alloc_template
allocate() 直接使用malloc(), deallocate() 直接使用free();
模拟C++的set_new_handler()以处理内存不足的状况。
set_new_handler只有针对placement new申请内存时,才有效;用malloc申请时,无效。需要借助malloc返回值为空,进行申请内存失败判断。
如何选择使用哪个配置器?
定义或取消宏__USE_MALLOC,就能决定alloc的实际类型(一级配置器 or 二级配置器)。SGI STL并未定义__USE_MALLOC,因此SGI默认使用的二级配置器。
1 2 3 4 5 6 7 8 9 10 11 # ifdef __USE_MALLOC typedef malloc_alloc alloc;typedef malloc_alloc single_client_alloc;# else ... typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0 > alloc;typedef __default_alloc_template<false , 0 > single_client_alloc;... #endif
一级配置器实现
用malloc(), free(), realloc()等C函数执行实际的内存配置、释放、重新配置,并模拟实现类似于C++ new-handler异常处理机制。不能直接使用new-handler机制,因为并没有用::operator new配置内存,而是用的malloc。
C++ new handler机制是什么?
指你可以要求系统在内存配置需求无法被满足时,调用一个你指定的函数,来进行异常处理。i.e. 一旦::operator new无法完成任务,在抛出std::bad_alloc异常前,会先调用由客户端指定的处理例程。 (见《Effective C++》条款49 )
谁负责注册、设计“内存不足处理例程”?
设计“内存不足处理例程”是客户端的责任,注册“内存不足处理例程”也是客户端的责任。也就是,客户负责调用set_malloc_handler()注册内存不足处理例程,并定义传入实参(内存不足如何处理)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 #ifndef __THROW_BAD_ALLOC # if defined(__STL_NO_BAD_ALLOC) || !defined(__STL_USE_EXCEPTIONS) # include <stdio.h> # include <stdlib.h> # define __THROW_BAD_ALLOC fprintf(stderr, "out of memory\n" ); exit(1) # else # include <new> # define __THROW_BAD_ALLOC throw std::bad_alloc() # endif #endif 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); if (0 == result) result = oom_malloc (n); return result; } static void deallocate (void * p, size_t ) { free (p); } static void *reallocate (void * p, size_t , size_t new_sz) { void *result = realloc (p, new_sz); 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; } }; 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; 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 = reallocate (p, n); if (result) { return result; } } } typedef __malloc_alloc_template<0 > malloc_alloc;
第二级配置器:__default_alloc_template
与第一级配置器区别:为避免太多小额区块造成内存碎片,多了一些机制。
SGI第二级配置做法:如果区块够大,> 128bytes,移交给第一级配置器处理;当区块 < 128bytes,则以内存池(memory pool)管理。
这种方法称为次配置(sub-allocation) :每次配置一大块内存,并维护对应自由链表(free-list)。下次再有相同大小内存需求时,就直接从free-lists中取出。如果客户端归还小额区块,就由配置器回收到free-lists中。
简而言之,二级配置器负责内存配置、回收,会根据区块大小,选择自行配置,还是移交给一级配置器处理。
因此,可以知道二级配置器多了自由链表和内存池两个机制,专门为处理 <= 128bytes的内存申请而生。
自由链表 free-lists
二级配置器的核心之一就是free-lists(自由链表),每个free-list代表一类空闲区块,大小从8,16,24,…,到128 (bytes),共16个。16个free-list的头结点,用一个数组free_list[16]表示,下文称之为槽位。每个槽位指向的区块是一个链表,而该区块就是链表的第一个空闲块。
free-list的精髓就是联合体obj:
1 2 3 4 5 union obj { union obj * free_list_link; char client_data[1 ]; };
一般情况下,我们用next* 和 data的struct来描述链表节点,而obj用obj*的union,这是为什么?
因为这样可以节省维护链表的指针空间。一个区块,在分配给客户端之前,首部大小为obj那部分可以看作一个指针free_list_link,用来串成链表;而分配给客户端之后,这部分无需当做指针,可以被客户按需使用;待客户归还区块时,首部大小为obj那部分又会被当做指针,用来串成链表,加入free list。
free_list_link所指向的内存区块大小,由链表头结点,即槽位所在free_list[]中的索引决定。而归还时,调用者会记录区块大小。因此,obj不需要额外存储区块大小。
为什么free-list区块最小尺寸是8byte,而不是4byte?
因为64位系统上,指针尺寸8byte;32位系统上,指针尺寸4byte。为了兼容32位、64位系统,取较大值8。
下图是以96byte区块为例,描述free-lists结构:
__default_alloc_template 的数据结构
SGI STL中,__default_alloc_template 的数据结构签名如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 enum { __ALIGN = 8 }; enum { __MAX_BYTES = 128 }; enum { __NFREELTISTS = __MAX_BYTES / __ALIGN }; template <bool threads, int inst>class __default_alloc_template {private : static size_t ROUND_UP (size_t bytes) ; private : union obj { union obj * free_list_link; char client_data[1 ]; }; private : static obj* volatile free_list[__NFREELTISTS]; static size_t FREELIST_INDEX (size_t bytes) ; static void *refill (size_t n) ; static char * chunk_alloc (size_t size, int &nobjs) ; static char * start_free; static char * end_free; static size_t heap_size; public : static void * allocate (size_t n) { } static void deallocate (void * p , size_t n) { } static void * reallocate (void * p, size_t old_sz, size_t new_sz) ; };
关于free-lists,有两个重要操作:
1)ROUND_UP() 将参数bytes上调至8的倍数,确保所有操作都是8byte对齐;
2)FREELIST_INDEX() 根据参数bytes,在free_list[]中找到合适的槽位:确保区块大小 >= bytes。
ROUND_UP() 向上调对齐
ROUND_UP()实现如下:
1 2 3 4 static size_t ROUND_UP (size_t bytes) { return (((bytes)+__ALIGN - 1 ) & ~(__ALIGN - 1 )); }
FREELIST_INDEX() 找合适槽位
FREELIST_INDEX()实现如下:
1 2 3 4 5 6 7 8 static size_t FREELIST_INDEX (size_t bytes) { return (((bytes)+__ALIGN - 1 ) / __ALIGN - 1 ); }
allocate() 空间配置
作为一个配置器,最核心的功能莫过于分配内存、回收内存。allocate()负责分配内存,deallocate()负责回收内存。
利用allocate()申请n bytes内存流程:
有3个关键点:
1)申请内存空间n > 128bytes时,交给一级配置器处理。而一级配置器的__malloc_alloc_template<>::allocate()在前面已讲过,这里不再遨述。
2)n <= 128bytes,在free_list[]数组会找一个适当的区块链表free list,并且该free list有可用空间时,需要将free list链表的第一个空闲数据块交给客户,而自身的指针也同样需要更新。
3)在2)的基础上,free list并没有可用空间时,就会调用refill()重新填充该free list。这部分放到下面的refill()函数部分讲解,而内部涉及到memory pool(内存池)的部分,放到下面memory pool部分专门讲解。
什么时候表示free list有空闲区块?
free_list[i](i=0,1,…15)的free_list_link所指向的下一个链表节点如果非空(非0),代表有空闲区块。而二级指针my_free_list指向free_list[i](某个槽位)。
针对情形2),当free list由空闲区块时,拔出第一个空闲区块给客户
二级配置器的allocate()源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static void * allocate (size_t n) { obj* volatile * my_free_list; obj* result; if (n > (size_t )__MAX_BYTES) { return (malloc_alloc::allocate (n)); } my_free_list = free_list + FREELIST_INDEX (n); result = *my_free_list; if (result == 0 ) { void *r = refill (ROUND_UP (n)); return r; } *my_free_list = result->free_list_link; return (result); }
deallocate() 空间释放
空间释放与空间配置的逆过程,由deallocate()处理,负责回收分配出去的内存空间。总的原则是谁配置的空间,由谁来负责回收。
不过,与allocate()分配空间不同,deallocate()只有2个关键点,因为不存在内存空间不够的情况。
2个关键点:
1)回收的空间 n > 128byte时,交给第一级配置器处理;
2)n <= 128bytes时,找到适当的free list,然后调整free list,将回收空间加入其中。
二级配置器回收空间流程:
二级配置器回收空间结构变化示意图:
二级配置器deallocate()源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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); q->free_list_link = *my_free_list; *my_free_list = q; }
可以看到,当 归还空间大小 <= 128时,deallocate并没有将空间归还给系统,而是交给了free list,方便下次申请。这样可以有效避免内存碎片问题。
refill() 重新填充free list
在allocate()中,当发现合适的free list中并没有可用的空闲块时,就会调用refill()为free list重新填充空闲空间。新空间由chunk_alloc()完成,默认取自内存池。
refill() 默认向chunk_alloc()申请nobjs=20个大小为n(假定n已经调整为8倍数)的(连续)内存空间,当然实际成功申请到多少个,需要根据实际情况决定,可通过nobjs传出值判断。可以肯定的是,如果有返回值(没有出现异常终止程序),那么至少会有一个大小为n的对象。
refill()会得到一个连续空间,而把第一个大小n的对象返回给客户;至于剩余的空间,在按尺寸n找到合适的free list后,将剩余空间按链表形式加入free list。
refill() 重新填充free list流程:
refill()源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 template <bool threads, int inst>void * __default_alloc_template<threads, inst>::refill (size_t n) { int nobjs = 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); }
memory pool 内存池
chunk_alloc()源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 template <bool threads, int inst>char * __default_alloc_template<threads, inst>::chunk_alloc (size_t size, int &nobjs) { char * result; size_t total_bytes = size * nobjs; size_t bytes_left = end_free - start_free; if (bytes_left >= total_bytes) { result = start_free; start_free += total_bytes; return (result); } else if (bytes_left >= size) { nobjs = bytes_left / size; total_bytes = size * nobjs; result = start_free; start_free += total_bytes; return (result); } else { size_t bytes_to_get = 2 * total_bytes + ROUND_UP (heap_size >> 4 ); if (bytes_left > 0 ) { obj* volatile *my_free_list = free_list + FREELIST_INDEX (bytes_left); ((obj*)start_free)->free_list_link = *my_free_list; *my_free_list = (obj*)start_free; } start_free = (char *)malloc (bytes_to_get); if (0 == start_free) { int i; obj* volatile * my_free_list, *p; for (i = size; i <= __MAX_BYTES; i += __ALIGN) { my_free_list = free_list + FREELIST_INDEX (i); p = *my_free_list; if (0 != p) { *my_free_list = p->free_list_link; start_free = (char *)p; end_free = start_free + i; return (chunk_alloc (size, nobjs)); } } end_free = 0 ; start_free = (char *)malloc_alloc::allocate (bytes_to_get); } heap_size += bytes_to_get; end_free = start_free + bytes_to_get; return (chunk_alloc (size, nobjs)); } }
总结:
本文转载自:https://www.cnblogs.com/fortunely/p/16219743.html