STL之allocator

来源:互联网 发布:淘宝图片多大合适 编辑:程序博客网 时间:2024/05/04 13:04

如果这篇内存乏味那就看我自己写的memory pool 剖析

http://blog.csdn.net/gyafdxis/article/details/49620835

内存池分配器(pool allocator)的主要代码如下(实现详见《STL源码剖析》)

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. template<class T>  
  2. class pool_alloc{  
  3. public:  
  4.     typedef size_t      size_type;  
  5.     typedef ptrdiff_t   difference_type;  
  6.     typedef T*          pointer;  
  7.     typedef const T*    const_pointer;  
  8.     typedef T&          reference;  
  9.     typedef const T&    const_reference;  
  10.     typedef T           value_type;  
  11.         
  12.     template<typename T1>  
  13.     struct rebind  
  14.     { typedef pool_alloc<T1> other; };  
  15.       
  16.     pool_alloc();  
  17.     pool_alloc(const pool_alloc&);  
  18.     template<class T1>  
  19.     pool_alloc(const pool_alloc<T1>&);  
  20.     ~pool_alloc();  
  21.     pointer address(reference);  
  22.     const_pointer address(const_reference);  
  23.     size_type max_size() const;  
  24.     pointer allocate(size_type num_elems, const void* = 0);  
  25.     void construct(pointer ptr, const_reference elem);  
  26.     void destroy(pointer ptr);  
  27.     void deallocate(pointer ptr, size_type useless_n);  
  28.       
  29. protected:    
  30.     static size_type round_up(size_type bytes);  
  31.     static size_type freelist_index(size_type bytes);  
  32.     static void* refill(size_type bytes);  
  33.     static char* chunk_alloc(size_type bytes, size_type& num_objs);  
  34.   
  35. private:  
  36.     union obj{  
  37.         obj* next;  
  38.         char client_data[1];  
  39.     };  
  40.     enum { __align = 8  };  
  41.     enum { __max_bytes = 128    };  
  42.     enum { __num_free_list = __max_bytes / __align  };  
  43.     static char* start_free;  
  44.     static char* end_free;  
  45.     static size_type heap_size;  
  46.     static obj* volatile free_list[__num_free_list];  
  47. };  

1、成员变量

free_list[]:16个自由链表,从free_list[0]到free_list[15]所指向的链表的节点的大小分别为8、16、24、...、120、128,均为8的倍数

start_free:自由内存的起始地址

end_free:自由内存的结束地址后一个地址

heap_size:内存池已经申请的空间的大小

内存池配置器维护16个自由链表(free_list[0] 到 free_list[15])和一个未加入到16个链表中连续内存(暂且称为自由内存,大小为8的倍数)。每个链表节点的大小分别为8、16、24、32、...、120、128(都是8的倍数),16个链表指向的节点都是空闲的,即待分配的。如果free_list[index]为null,则表示该链表没有空闲的节点。start_free指向自由内存的起始地址,end_free指向示自由内存的结束地址。 

2、初始化

初始化start_free、end_free、heap_size、free_list[] 为null或0,也就是说16个链表都是空的,自由内存的大小也为0。

3、分配

3.1 申请空间可以从链表空闲节点获得

假设某时刻内存池的状态如图P-1,这时需要申请20B的空间,那么就会去节点大小为24B(因为16个链表中能存下20B的最小节点大小为24B)的链表free_list[2]中搜索,看是否有空节点。状态P-1下free_list[2]有空闲节点(free_list[2]不为null),那么就返回第一个空闲节点,内存池状态转化为P-2。

纠正:start_free不可能指向地址0(0相当于null),heap_size是内存池已经申请的内存的大小,也不可能是0。下面图中只是为了方便做运算才把start_free指向0以及heap_size设为0。


申请大小为20B的空间,实际返回的是24B的内存


3.2 申请空间不能从链表空闲节点获得,可以从自由内存获得

如果某时刻内存池状态如P-1,此时用户需要申请35B的空间。首先先去自由链表中寻找是否有大小为40B的空闲节点,发现为空(即free_list[4]为null)。此时就会检查自由内存的大小,如果可以分配20(默认)40B的节点,那么1个返回给用户,另外19个形成一个链表,把free_list[4]作为该链表的头。但是由于自由内存只有96B,最多只能分配2个40B的节点,那么就分配2个出来,一个返回给用户,用一个放到free_list[4]链表中。此时内存池的状态由P-1变为P-3


3.3 申请空间不能从链表空闲节点和自由内存获得,需要申请新的空间注入内存池

在状态P-3下,假设用户要申请alloc_size = 30B的内存。检测链表free_list[3](32字节),发现为空,没有空闲节点。检查自由内存的大小,发现只有16字节,连一个节点的大小(32B)都没有,那么就要申请新的内存(申请的内存是连续的,一般用operator new或malloc获得)注入到内存池。在申请新的内存之前,先要把自由内存的16B作为一个节点加入到free_list[1]中(这个地方可以说明为什么自由内存的大小一定要是8的倍数)。申请的新的内存大小为round_up(30) * 20 * 2 + heap_size = 1280B,其中round_up(n)返回大于等于n且是8的倍数的最小整数,round_up(30)就是32了。申请新的内存注入到内存池后(假设申请到的连续内存的起始地址为1000),配置20个大小为32B的节点,第一个返回给用户,其余19个形成一个链表并由free_list[3]指向。其余剩下的内存就作为新的自由内存区。分配成功后,内存池的状态如图P-4


3.4 申请空间既不能从链表中找到空闲节点,自由内存大小也不足一个,申请新的空间注入内存池也失败

在P-3状态下,假设用户要申请30B的内存,在把原来的自由内存的16B加入到free_list[1]后,内存池申请新的内存(1280B)注入内存池中,如果申请新的内存失败,那么处理措施如下:在节点比32B大的链表中找一个最小的空闲节点作为自由内存,然后从自由内存返回一个节点给用户。那么这里从free_list[4]开始搜索,刚好发现free_list[4]有空闲节点,则把第一个空闲节点加入到自由内存区,然后返回一个32B的节点给用户,剩余的作为自由内存。分配完成全状态变为P-5


4、空间释放

空间的释放并不是真的把空间释放(free或operator delete)给系统,而是把空间放到对应的free_list[*]中。比如在状态P-2下,如果用户要释放address1内存块,配置器所做的是把这个块加入到free_list[2]中, 释放完成后,内存池状态又回到P-1状态。

5、总结与疑问

疑问1:链表节点定义为 struct obj { obj* next; }是否可以?

疑问2:内存池向系统申请的内存如何释放?

疑问3:上述的代码基本与gcc 4.7.2的pool_allocator.h中的__pool_alloc相同,但gcc中还有一个__mt_alloc(在mt_allocator.h中),难道这个才是真正的内存池配置器?

疑问1回答:我们发现,内存池配置器中自由链表的节点是union类型,而且当中的client_data至始至终都没有用到过。《STL源码剖析》--侯捷版 中有这样一段话

“诸君或许会想,为了维护链表,每个节点需要额外的指针(指向下一个节点),这不又造成另一种额外负担吗?你的顾虑是对的,但早已有好的解决办法。注意,上述obj所用的是union,由于union之故,从其第一字段观之,obj可被视为一个指针,指向相同形式的另一个obj。从其第二字段观之,obj可被视为一个指针,指向实际区块。一物二用的结果是,不会为了维护链表所必须的指针而造成内存的另一种浪费。”

个人不是很理解这段话。但是,真正维持链表所带来的开销其实就是free_list数组。

我有过两种猜测,一是union不会像struct一样有构造函数,可以提高效率,但后来自己写了一个程序验证union也可以有构造函数的,所以这种猜测不成立。二是链表的最小节点不一定是8B,也有可能比8B小,比如4B(不能小于4B,因为至少要能存储一个指针),union尽管有两个数据,但实际只占用4B;如果说client_data有被用到的话,那么这个问题就可以解释了,但是《STL源码剖析》一书中的代码,至始至终都没有用到过client_data,所以其实它是可以被删掉的。如果可以被删掉的话,那么用struct obj{ obj* next; } 也是可以的啊?我尝试去看gcc编译器的源码,但是发现pool_allocator.h 中只声明了 refill() 函数和chunk_alloc()函数,但却没有定义,也没有在其他的头文件中发现这两个函数的定义,与operator new函数一样,可能是封装到库中去了吧。

疑问2回答:对于疑问2,内存池配置器会向系统申请内存,但是由于可能会多次申请内存,而配置器又没有维持每个内存块的头指针,最后当内存池分配器对象销毁时,其申请的内存如何释放?gcc中的__pool_alloc的析构函数中什么事都没做,难道说内存池配置器销毁时意味着进程的结束,然后被系统强制回收?



根据STL的规范,allocator必须要包含以下接口:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. allocator::value_type;  
  2. allocator::size_type;  
  3. allocator::difference_type;  
  4. allocator::pointer;  
  5. allocator::const_pointer;  
  6. allocator::reference;  
  7. allocator::const_reference;  
  8. allocator::rebind::other;  
  9. allocator::allocator();     // default constructor  
  10. allocator::allocator(const allocator&);  
  11. template<class U>  
  12. allocator::allocator(const allocator<U>&);  
  13. allocator::~allocator();  
  14. size_type allocator::max_size() const;    
  15. pointer allocator::allocate(size_type num_elems, const void* = 0);    
  16. void allocator::construct(pointer ptr, const_reference elem);    
  17. void allocator::destroy(pointer ptr);    
  18. void allocator::deallocate(pointer ptr, size_type useless_n);  

记忆方式:

新建一个vector,分配空间    -------    allocate(size_t n, const void* = 0)

push_back(ele)                      -------    construct(pointer ptr, const_reference ele)

pop_back()                              -------    destroy(pointer ptr)

释放所以空间                           -------    deallocate(pointer ptr, size_t useless_n)

deque的T*分配器                   -------     template<class T1> struct rebind { typedef allocator<T1> other; }

container的内存分配与释放、对象构造与析构都是交给allocator去做的。vector要分配内存时,就有allocator的allocate函数去做;当要push_back一个对象到vector中时,由于最后一个位置还没有初始化过,所以需要调用allocator的construct对象在最后一个地址上进行构造;当要pop掉最后一个对象时,只需要调用allocator的destroy函数就好;当要释放整个分配的空间时,调用allocator的deallocate函数。在deque中,不仅需要一个allocator<T>,还需要一个allocator<T*>,所以allocator中还需要有 rebind。

我们可以根据这个规范自己实现一个allocator,然后用于vector,结果证明可以匹配的很好。而且用到的底层API就下面几个:void* operator new(size_t size), new (void* ptr) A(para_lists), void operator delete(void* ptr)。其中 new(void* ptr) A(para_lists) 实际上调用了placement new与A的构造函数,详见文章 “new深入分析”。

在学习deque的过程中,发现deque对外提供了 allocate_node() 等对外接口,通过追踪allocate_node()函数,一探编译器(gcc 4.7.2)allocator的究竟。探索的过程如下:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. template<typename _Tp, typename _Alloc>  
  2. class _Deque_base{  
  3.         typedef typename _Alloc::template rebind<_Tp>::other  _Tp_alloc_type; // (1)  
  4.     struct _Deque_impl : public _Tp_alloc_type  
  5.         {  
  6.         _Tp** _M_map;  
  7.         size_t _M_map_size;  
  8.         iterator _M_start;  
  9.         iterator _M_finish;  
  10.     }  
  11.     _Tp* _M_allocate_node()  
  12.        {   
  13.         return _M_impl._Tp_alloc_type::allocate(__deque_buf_size(sizeof(_Tp))); // (2)  
  14.        }  
  15.        _Deque_impl _M_impl;  
  16. };  
_Deque_impl继承了 _Tp_alloc_type,所以(2)行的allocate() 函数实质上是_Tp_alloc_type的。根据(1)行,要知道_Tp_alloc_type的具体类型,需要知道_Deque_base的_Alloc是什么。继续探索,发现如下代码:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. template<typename _Tp, typename _Alloc = std::allocator<_Tp> >  
  2.     class deque : protected _Deque_base<_Tp, _Alloc>  
也就是说,allocate()函数其实是 std::allocator<_Tp>::rebind<_Tp>::other的,那么这个又是什么东西呢?继续往下发现

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. // allocator.h  
  2. template<typename _Tp>  
  3. class allocator: public __glibcxx_base_allocator<_Tp>  
  4. {  
  5.     template<typename _Tp1>  
  6.     struct rebind  
  7.     { typedef allocator<_Tp1> other; };  
  8. };  
原来,std::allocator<_Tp>::rebind<_Tp>::other就是std::allocator<_Tp>;allocator<_Tp>并没有allocate()函数,但是它继承了 __glibcxx_base_allocator<_Tp>,然后又有下面代码

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. // c++allocator.h  
  2. #define glibcxx_base_allocator gnu::new_allocator  
所以其实deque用的配置器就是 new_allocator。现在我们来看下new_allocator是如何实现的,源码如下:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. // new_allocator.h  
  2. template<typename _Tp>  
  3. class new_allocator  
  4. {  
  5. public:  
  6.     typedef size_t     size_type;  
  7.     typedef ptrdiff_t  difference_type;  
  8.     typedef _Tp*       pointer;  
  9.     typedef const _Tp* const_pointer;  
  10.     typedef _Tp&       reference;  
  11.     typedef const _Tp& const_reference;  
  12.     typedef _Tp        value_type;  
  13.   
  14.     template<typename _Tp1>  
  15.     struct rebind  
  16.     { typedef new_allocator<_Tp1> other; };  
  17.   
  18.     new_allocator() _GLIBCXX_USE_NOEXCEPT { }  
  19.   
  20.     new_allocator(const new_allocator&) _GLIBCXX_USE_NOEXCEPT { }  
  21.   
  22.     template<typename _Tp1>  
  23.     new_allocator(const new_allocator<_Tp1>&) _GLIBCXX_USE_NOEXCEPT { }  
  24.   
  25.     ~new_allocator() _GLIBCXX_USE_NOEXCEPT { }  
  26.   
  27.     pointer address(reference __x) const _GLIBCXX_NOEXCEPT  
  28.     { return std::__addressof(__x); }  
  29.   
  30.     const_pointer address(const_reference __x) const _GLIBCXX_NOEXCEPT  
  31.     { return std::__addressof(__x); }  
  32.   
  33.     // NB: __n is permitted to be 0.  The C++ standard says nothing  
  34.     // about what the return value is when __n == 0.  
  35.     pointer allocate(size_type __n, const void* = 0)  
  36.     {   
  37.         if (__n > this->max_size())  
  38.                 std::__throw_bad_alloc();  
  39.         return static_cast<_Tp*>(::operator new(__n * sizeof(_Tp)));  
  40.     <span style="white-space:pre">    </span>}  
  41.   
  42.     // __p is not permitted to be a null pointer.  
  43.     void deallocate(pointer __p, size_type)  
  44.     { ::operator delete(__p); }  
  45.   
  46.     size_type max_size() const _GLIBCXX_USE_NOEXCEPT  
  47.     { return size_t(-1) / sizeof(_Tp); }  
  48.   
  49. #ifdef __GXX_EXPERIMENTAL_CXX0X__  
  50.     template<typename _Up, typename... _Args>  
  51.     void construct(_Up* __p, _Args&&... __args)  
  52.     { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }  
  53.   
  54.     template<typename _Up>  
  55.     void destroy(_Up* __p) { __p->~_Up(); }  
  56. #else  
  57.     // _GLIBCXX_RESOLVE_LIB_DEFECTS  
  58.     // 402. wrong new expression in [some_] allocator::construct  
  59.     void construct(pointer __p, const _Tp& __val)   
  60.     { ::new((void *)__p) _Tp(__val); }  
  61.   
  62.     void destroy(pointer __p) { __p->~_Tp(); }  
  63. #endif  
  64. };  
可以看出,new_allocator实现了STL规范的所有对外接口。 allocate()函数用到的就是 void* operator new(size_t)函数,construct()函数用到的就是placement new,destroy()函数就是调用对象的析构函数,deallocate()函数就是调用 void operator delete(void* ptr, size_t useless_n)函数。
在STL中(gcc 4.7.2),目前学习过的vector、list、deque都是用的new_allocator作为默认配置器。 



0 0
原创粉丝点击