zoukankan      html  css  js  c++  java
  • STL容器的内存分配

    这篇文章参考的是侯捷的《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) {}
    Vectorbase的默认构造函数:

    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()); }
    不看实现,我们先设想一下,容器一旦new出来,必定会有若干的容器内元素,如果不采用特别的应对策略,那么在一般情况下,除了分配内存,还会有一系列的构造操作。比如,如果我们一开始就定义一个vector v(n),每一个BigObject带一个需要执行很久的默认构造函数(假设),然而,我们却只用到了其中n/2个单位的空间,那么除了n/2个单位的空间闲置之外(这个是没办法的,所以不作缺点考虑吧。),这n/2个单位空间的构造操作也没有意义了。

    于是,设计者利用了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); }
    但是,你能想到这些,不一定能想到destroy内部就是运行析构函数而已。。。好吧,其实你是对的:

    void destroy(pointer p) { _p->~Tp(); }
    (喂,你前面有个代码片段已经暴露了吧~)世界有时候就是充满了恶意。

    知道这几个东西之后其实stl对内存分配所做的事情就没有那么神秘了。

    那么,之前既然已经知道了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)); }
    };
    可以看到,它内部使用的是一个称为simple_alloc的包装类。

    到这里为止,已经没法继续追下去了,因为这里会涉及到根据不同的需求,进行不同的内存分配策略。事实上,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呢?因为其内部还是alloc。在stl_alloc中,我们看到

    # ifdef __USE_MALLOC
    
    typedef malloc_alloc alloc;
    typedef malloc_alloc single_client_alloc;
    
    # else

    噢噢噢,mallocalloc,就是你了!嗯,写到这里脑子都散黄了,思绪已经拉不回来了,也不知道我究竟是在写什么了,如果您发现了我文章组织有些问题,没关系,可以提意见,反正我又不听。(/#=皿=)/|_|

    PS:对了,为什么只讲了vector?因为其内部实现是固定分配的,比较有代表性,而链表实现的(如List等)虽然也是分配和构造分开的,但是逻辑上看还是一起的,所以可能没有讲的必要,知道allocator差不多就知道容器的内存分配大概状况了,所以主要内容其实并不是在容器本身了。

  • 相关阅读:
    企业使用数据库的12种姿势
    回归架构本质,重新理解微服务
    Java中随机数的产生方式与原理
    自动类型转换、强制类型转换、作用域、整型表数范围
    创建自定义类的对象数组
    CentOS上安装比较习惯的代码编辑器
    ubuntu 15.04 的安装遇到的问题及其解决方法
    算法思想篇(1)————枚举算法
    初来乍到
    Eclipse中获取html jsp 标签的属性提示信息方法
  • 原文地址:https://www.cnblogs.com/imjch/p/3902952.html
Copyright © 2011-2022 走看看