如果这篇内存乏味那就看我自己写的memory pool 剖析
http://blog.csdn.net/gyafdxis/article/details/49620835
内存池分配器(pool allocator)的主要代码如下(实现详见《STL源码剖析》)
- template<class T>
- class pool_alloc{
- public:
- typedef size_t size_type;
- typedef ptrdiff_t difference_type;
- typedef T* pointer;
- typedef const T* const_pointer;
- typedef T& reference;
- typedef const T& const_reference;
- typedef T value_type;
- template<typename T1>
- struct rebind
- { typedef pool_alloc<T1> other; };
- pool_alloc();
- pool_alloc(const pool_alloc&);
- template<class T1>
- pool_alloc(const pool_alloc<T1>&);
- ~pool_alloc();
- pointer address(reference);
- const_pointer address(const_reference);
- size_type max_size() const;
- pointer allocate(size_type num_elems, const void* = 0);
- void construct(pointer ptr, const_reference elem);
- void destroy(pointer ptr);
- void deallocate(pointer ptr, size_type useless_n);
- protected:
- static size_type round_up(size_type bytes);
- static size_type freelist_index(size_type bytes);
- static void* refill(size_type bytes);
- static char* chunk_alloc(size_type bytes, size_type& num_objs);
- private:
- union obj{
- obj* next;
- char client_data[1];
- };
- enum { __align = 8 };
- enum { __max_bytes = 128 };
- enum { __num_free_list = __max_bytes / __align };
- static char* start_free;
- static char* end_free;
- static size_type heap_size;
- static obj* volatile free_list[__num_free_list];
- };
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必须要包含以下接口:
- allocator::value_type;
- allocator::size_type;
- allocator::difference_type;
- allocator::pointer;
- allocator::const_pointer;
- allocator::reference;
- allocator::const_reference;
- allocator::rebind::other;
- allocator::allocator(); // default constructor
- allocator::allocator(const allocator&);
- template<class U>
- allocator::allocator(const allocator<U>&);
- allocator::~allocator();
- size_type allocator::max_size() const;
- pointer allocator::allocate(size_type num_elems, const void* = 0);
- void allocator::construct(pointer ptr, const_reference elem);
- void allocator::destroy(pointer ptr);
- 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的究竟。探索的过程如下:
- template<typename _Tp, typename _Alloc>
- class _Deque_base{
- typedef typename _Alloc::template rebind<_Tp>::other _Tp_alloc_type; // (1)
- struct _Deque_impl : public _Tp_alloc_type
- {
- _Tp** _M_map;
- size_t _M_map_size;
- iterator _M_start;
- iterator _M_finish;
- }
- _Tp* _M_allocate_node()
- {
- return _M_impl._Tp_alloc_type::allocate(__deque_buf_size(sizeof(_Tp))); // (2)
- }
- _Deque_impl _M_impl;
- };
- template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
- class deque : protected _Deque_base<_Tp, _Alloc>
- // allocator.h
- template<typename _Tp>
- class allocator: public __glibcxx_base_allocator<_Tp>
- {
- template<typename _Tp1>
- struct rebind
- { typedef allocator<_Tp1> other; };
- };
- // c++allocator.h
- #define glibcxx_base_allocator gnu::new_allocator
- // new_allocator.h
- template<typename _Tp>
- class new_allocator
- {
- public:
- typedef size_t size_type;
- typedef ptrdiff_t difference_type;
- typedef _Tp* pointer;
- typedef const _Tp* const_pointer;
- typedef _Tp& reference;
- typedef const _Tp& const_reference;
- typedef _Tp value_type;
- template<typename _Tp1>
- struct rebind
- { typedef new_allocator<_Tp1> other; };
- new_allocator() _GLIBCXX_USE_NOEXCEPT { }
- new_allocator(const new_allocator&) _GLIBCXX_USE_NOEXCEPT { }
- template<typename _Tp1>
- new_allocator(const new_allocator<_Tp1>&) _GLIBCXX_USE_NOEXCEPT { }
- ~new_allocator() _GLIBCXX_USE_NOEXCEPT { }
- pointer address(reference __x) const _GLIBCXX_NOEXCEPT
- { return std::__addressof(__x); }
- const_pointer address(const_reference __x) const _GLIBCXX_NOEXCEPT
- { return std::__addressof(__x); }
- // NB: __n is permitted to be 0. The C++ standard says nothing
- // about what the return value is when __n == 0.
- pointer allocate(size_type __n, const void* = 0)
- {
- if (__n > this->max_size())
- std::__throw_bad_alloc();
- return static_cast<_Tp*>(::operator new(__n * sizeof(_Tp)));
- <span style="white-space:pre"> </span>}
- // __p is not permitted to be a null pointer.
- void deallocate(pointer __p, size_type)
- { ::operator delete(__p); }
- size_type max_size() const _GLIBCXX_USE_NOEXCEPT
- { return size_t(-1) / sizeof(_Tp); }
- #ifdef __GXX_EXPERIMENTAL_CXX0X__
- template<typename _Up, typename... _Args>
- void construct(_Up* __p, _Args&&... __args)
- { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
- template<typename _Up>
- void destroy(_Up* __p) { __p->~_Up(); }
- #else
- // _GLIBCXX_RESOLVE_LIB_DEFECTS
- // 402. wrong new expression in [some_] allocator::construct
- void construct(pointer __p, const _Tp& __val)
- { ::new((void *)__p) _Tp(__val); }
- void destroy(pointer __p) { __p->~_Tp(); }
- #endif
- };
在STL中(gcc 4.7.2),目前学习过的vector、list、deque都是用的new_allocator作为默认配置器。