STL容器的内存分配
2014-08-10 16:01 大脑溢出 阅读(1718) 评论(0) 编辑 收藏 举报这篇文章参考的是侯捷的《STL源码剖析》,所以主要介绍的是SGI STL实现版本,这个版本也是g++自带的版本,另外有J.Plauger实现版本对应的是cl自带的版本,他们都是基于HP实现的版本,有兴趣可以翻翻最新的源码头文件开始处有声明。
/* * * Copyright (c) 1994 * Hewlett-Packard Company(这里) * * Permission to use, copy, modify, distribute and sell this software * and its documentation for any purpose is hereby granted without fee, * provided that the above copyright notice appear in all copies and * that both that copyright notice and this permission notice appear * in supporting documentation. Hewlett-Packard Company makes no * representations about the suitability of this software for any * purpose. It is provided "as is" without express or implied warranty. * * * Copyright (c) 1996 * Silicon Graphics Computer Systems, Inc.(和这里) * * Permission to use, copy, modify, distribute and sell this software * and its documentation for any purpose is hereby granted without fee, * provided that the above copyright notice appear in all copies and * that both that copyright notice and this permission notice appear * in supporting documentation. Silicon Graphics makes no * representations about the suitability of this software for any * purpose. It is provided "as is" without express or implied warranty. */
说到STL容器的内存分配就得先从new说起。
咱们知道,new一个对象出来,编译器会做两步工作,咱称之为“new操作”:
为该类型分配内存 在新分配的内存中构造该类型的一个对象虽然new关键字很方便,但是可以预想到,在某些应用场景中,运行时开销是不太理想的,或者说,有更好的策略可以节省开销,推迟到必要的时候再付出这部分开销。
比如vector容器,对应于c#或者java中的ArrayList,这种容器的特点是一旦new出他们来,就已经分配固定大小的内存,当若干插入元素操作之后如果空间不够,则新申请一块内存,依据具体的策略确定新申请的内存的大小,一般来说2倍于原空间大小。当然了,这是基于vector的接受一个size类型的参数的构造函数来说的。咱们先可以看看vector的默认构造函数的实现的片段:
const size_type __old_size = size(); const size_type __len = __old_size != 0 ? 2 * __old_size : 1; iterator __new_start = _M_allocate(__len);
其中_len即为新空间大小,可以看到确实是2倍于原空间,同时也可以看到,如果size大小为0个单位,则先分配1个单位的空间,由M_allocate函数进行分配,然后复制原来的元素到新分配的内存,最后归还原来的空间:
const size_type __old_size = size(); const size_type __len = __old_size != 0 ? 2 * __old_size : 1; iterator __new_start = _M_allocate(__len); iterator __new_finish = __new_start; __STL_TRY { __new_finish = uninitialized_copy(_M_start, __position, __new_start); construct(__new_finish, __x); ++__new_finish; __new_finish = uninitialized_copy(__position, _M_finish, __new_finish); } __STL_UNWIND((destroy(__new_start,__new_finish), _M_deallocate(__new_start,__len))); //destroy操作负责调用对象的析构函数 destroy(begin(), end());//begin()内部为_M_start,end()内部为_M_finish //_M_deallocate负责收回空间 _M_deallocate(_M_start, _M_end_of_storage - _M_start); _M_start = __new_start; _M_finish = __new_finish; _M_end_of_storage = __new_start + __len;
再瞧瞧new的时候vector分配多少单位的内存吧
vector的默认构造函数:
typedef _Vector_base<_Tp, _Alloc> _Base; explicit vector(const allocator_type& __a = allocator_type()) : _Base(__a) {}
typedef _Alloc allocator_type; _Vector_base(const _Alloc&) : _M_start(0), _M_finish(0), _M_end_of_storage(0) {}
可以看到,vector在new出来的时候是没有任何空间的,其中Mstart,Mfinish等等变量名作何意义可以去找参考相关书籍,或者请教搜索引擎。
好了,下面是接受一个size类型参数的构造函数的源码:
explicit vector(size_type __n) : _Base(__n, allocator_type()) { _M_finish = uninitialized_fill_n(_M_start, __n, _Tp()); }
于是,设计者利用了c++提供的一些特性将此情况比较理想地进行了解决,即将空间分配与对象构造分开进行。
接着讲述之前我们先了解一下
construct(T* p,var t):对p所指的内存中构造一个新元素,其中,用t进行复制初始化。
destroy(T* p):与construct相反,运行p所指对象的析构函数。
注意,两者都要求p必须指向了一块儿内存。
placement new(定位new): 语法形式: new(p) type(val)。与传统的new不同的是,定位new,只在已分配的内存中构造对象,并不负责分配内存。
可以猜想到construct内部实现也许就是定位new,但是有时候想象的不一定就是正确的。。。不过。。。这次其实就是正确的:
void construct(pointer p, const Tp& val) { new(p) Tp(val); }
void destroy(pointer p) { _p->~Tp(); }
那么,之前既然已经知道了vector是怎么节省开销的,也知道了构造的时候其实就是用了定位new,那么,是不是内存的分配用的就是operator new呢?想想可能是这样,因为这东西其实就是用来进行内存的分配,但是不会进行构造,所以可能vector中产生一个元素其实就是operator new+placement new!嗯,但是,源码给了我们一个无情的打击,前面我们已经看到,vector的实现里,内存的分配是靠Mallocate实现,那么,Mallocate_里面是怎么回子事呢?其实:
_Tp* _M_allocate(size_t __n) { return _M_data_allocator.allocate(__n); } /////////////////////////////////////////////// typedef simple_alloc<_Tp, _Alloc> _M_data_allocator; ////////////////////////////////////////////// template<class _Tp, class _Alloc> class simple_alloc { public: static _Tp* allocate(size_t __n) { return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); } static _Tp* allocate(void) { return (_Tp*) _Alloc::allocate(sizeof (_Tp)); } static void deallocate(_Tp* __p, size_t __n) { if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); } static void deallocate(_Tp* __p) { _Alloc::deallocate(__p, sizeof (_Tp)); } };
到这里为止,已经没法继续追下去了,因为这里会涉及到根据不同的需求,进行不同的内存分配策略。事实上,slt中的分配策略一般来说有两种。
第一级配置器:默认策略,一会儿讲述 第二级配置器:产生原因是为了避免太多小额区块造成内存碎片(其实这里还有待深入研究,因为目前为止最新版的STL源码有这么一句话——With a reasonable compiler, this(指的就是第一级配置器) should be roughly as fast as the original STL class-specific allocators, but with less fragmentation.),以内存池进行管理,不深入讨论。第一,我本身也没弄明白这个策略的内部实现,怕hold不住。第二是因为:
Defaultalloctemplate parameters are experimental and MAY DISAPPEAR in the future. Clients should just use alloc for now.
嗯,作者都这么说了。
然而,第一级配置器是什么呢?它叫_mallocalloc_template,它是怎么进行内存分配的呢?见下:
static void* allocate(size_t __n) { void* __result = malloc(__n); if (0 == __result) __result = _S_oom_malloc(__n); return __result; }
上段代码来自mallocalloctemplate的内部实现,可以看到,其实它就是用的C语言中用来进行堆内存分配的malloc函数(第二行的Soom_malloc用于处理内存分配失败时的情况,不做讲解),
至于为什么选它,我猜是因为效率考虑吧,没有查证过。至此还不能认为vector就是内部是按照这样分配的,还需要接着考证,我们回到vector,看看究竟是不是用的这个策略。
template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) > class vector : protected _Vector_base<_Tp, _Alloc> { // requirements:
可以看到,如果没有为vector指定“分配策略”,那么默认的就是STLDEFAULTALLOCATOR分配策略,这里就已经差不多得到答案了,经过一个遍历所有文件查找字符串的小脚本,终于找到组织了,位于stlconfig.h
# ifndef __STL_DEFAULT_ALLOCATOR # ifdef __STL_USE_STD_ALLOCATORS # define __STL_DEFAULT_ALLOCATOR(T) allocator< T > # else # define __STL_DEFAULT_ALLOCATOR(T) alloc # endif # endif
_STLUSESGIALLOCATORS is a hook so that users can disable new-style allocators, and continue to use the same kind of allocators as before, without having to edit library headers.
并且在stlvector中,也针对性地根据是否定义STLUSESTDALLOCATORS给出了两个版本的Vectorbase,其在分配内存上的不同仅仅是一个用的是标准接口,一个是自己定的接口(但是追溯其源码,还是用的标准接口,有兴趣的同学可以瞧瞧去),但是咱们不必关心这之间的区别。
现在我们就需要弄清楚,alloc到底是啥,为什么不讨论allocator
# ifdef __USE_MALLOC
typedef malloc_alloc alloc;
typedef malloc_alloc single_client_alloc;
# else
# ifdef __USE_MALLOC typedef malloc_alloc alloc; typedef malloc_alloc single_client_alloc; # else
噢噢噢,mallocalloc,就是你了!嗯,写到这里脑子都散黄了,思绪已经拉不回来了,也不知道我究竟是在写什么了,如果您发现了我文章组织有些问题,没关系,可以提意见,反正我又不听。(/#=皿=)/|_|
PS:对了,为什么只讲了vector?因为其内部实现是固定分配的,比较有代表性,而链表实现的(如List等)虽然也是分配和构造分开的,但是逻辑上看还是一起的,所以可能没有讲的必要,知道allocator差不多就知道容器的内存分配大概状况了,所以主要内容其实并不是在容器本身了。