STL 源码剖析 空间配置器

53 篇文章 1 订阅
  • 以STL的运用角度而言,空间配置器是最不需要介绍的东西,它总是隐藏在一切组件(更具体地说是指容器,container) 的背后
  • 但是STL的操作对象都存放在容器的内部,容器离不开内存空间的分配
  • 为什么不说allocator是内存配置器而说它是空间配置器呢?因为空间不一定 是内存,空间也可以是磁盘或其它辅助存储介质。是的,你可以写一个allocator, 直接向硬盘取空间。以下介绍的是SGI STL提供的配置器,配置的对象,这里分配的空间是指分配内存

 

namespace JJ
{
    template <class T>
    inline T* _allocate(std::ptrdiff_t size,T*){
        std::set_new_handler(0);
        T* tmp = (T*)(::operator new ((std::size_t)(size * sizeof(T))));
        if (tmp == 0){
            std::cerr << "out of memory " << std::endl;
            exit(1);
        }
        return tmp;
    }

    template <class T>
    inline void _deallocate(T* buffer){
        ::operator delete (buffer);
    }

    template <class T1,class T2>
    inline void _construct(T1* p,const T2& value){
        new(p) T1(value);
    }

    template <class T>
    inline void _destroy(T* ptr){
        ptr->~T();
    }

    template <class T>
    class allocator{
        public:
            typedef T           value_type;
            typedef T*          pointer;
            typedef const T*    const_pointer;
            typedef T&          reference;
            typedef const T&    const_reference;
            typedef size_t      size_type;
            typedef ptrdiff_t   difference_type;

            //rebind allocator of type U
            template <class U>
            struct rebind{
                typedef allocator<U> other;
            };

            //hint used for locality. ref.[Austern],pl89
            pointer allocate(size_type n,const void* hint = 0){
                return _allocate((difference_type)n,(pointer)0);
            }

            void deallocate(pointer p,size_type n){
                _deallocate(p);
            }

            void construct(pointer p,const T& value){
                _construct(p,value);
            }

            void destroy(pointer p){
                _destroy(p);
            }

            pointer address(reference x){
                return (pointer)&x;
            }

            const_pointer const_address(const_reference x){
                return (const_pointer)&x;
            }

            size_type max_size()const{
                return size_type (UINT_MAX / sizeof(T));
            }

        };
}
  • SGI STL在这个项目上根本就逸脱了 STL标准规格,使用一个专属的、拥有次层配置(sub-allocation)能力的、效率优越的特殊配置器,稍后有详细介绍
  •  备 次 配 置 力 (sub-allocation)的 S G I 空间配置器
  • SGI STL的配置器与众不同,也与标准规范不同,其名称是a llo c 而非 allo ca to r,而且不接受任何参数。换句话说,如果你要在程序中明白采用SGI配 置器,则不能采用标准写法:

  •  SGI STL allocator未能符合标准规格,这个事实通常不会给我们带来困扰,因 为通常我们使用缺省的空间配置器,很少需要自行指定配置器名称,而SGI STL的每一个容器都已经指定其缺省的空间配置器为a llo c .例如下面的vector声明:

  •  S G I标 准 的 空 间 配 量 器 ,std::allocator
  • SG I特 殊 的 空 间 配 置 器 ,std::alloc
  • 上一节所说的a llo c a to r 只是基层内存配置/释放行为(也就是 ::operator new和 : :operator delete)的一层薄薄的包装,并没有考虑到任何效率上的强 化. S G I另有法宝供其本身内部使用。

  •  这其中的new算式内含两阶段操作3: (1 )调 用 ::operator new配置内存; ⑵ 调 用Foo::Foo()构造对象内容。delete算式也内含两阶段操作:(1)调用Foo: :-Foo ()将对象析构;(2 ) 调 用 ::operator delete释放内存。
  • 为了精密分工,STL a llo c a to r决定将这两阶段操作区分开来。内存配置操作 由 alloc: al locate ()负责,内存释放操作由alloc : : deallocate ()负责;对象构造操作由::construct:()负责,对象析构操作由:;destroy负责

  •  内存空间的配置/释放与对象内容的构造/析构,分别着落在这两个文件身上。其 中 <stl_construct .h>定义有两个基本函数:构造用的 construct() 和析构用的destroy。。在一头栽进复杂的内存动态配置与释放之前,让我们先看清楚这 两个函数如何完成对象的构造和析构。

  •  构造和析构基本工具:co n stru ct()和 destroy()

  •  这两个作为构造、析构之用的函数被设计为全局函数,符合STL的规范.此外,STL还规定配置器必须拥有名为construct ()和 destroy ()的两个成员函数(见2.1节 ),然而真正在SGI STL中大显身手的那个名为std::alloc的配 置器并未遵守这一规则(稍后可见
  • 上 述 construct ()接受一个指针p 和一个初值value,该函数的用途就是 将初值设定到指针所指的空间上。C++的 placement new 运算子5 可用来完成这一任务。
  • destroy() 有两个版本,第一版本接受一个指针,准备将该指针所指之物析 构掉。这很简单,直接调用该对象的析构函数即可。第二版本接受first和 last 两个迭代器(所谓迭代器,第3 章有详细介绍),准备将 [first, last)范围内的所有对象析构掉。我们不知道这个范围有多大,万一很大,而每个对象的析构函数都无关痛痒(所谓rrzvia/destructor), 那么一次次调用这些无关痛痒的析构函数, 对效率是一种伤害。
  • 因此,这里首先利用value_type()获得迭代器所指对象的 型别,再利用 _type_traits< T> 判断该型别的析构函数是否无关痛痒.若是 (一true_type), 则什么也不做就结束;若 否 (— false_type), 这才以循环 方式巡访整个范围, 并在循环中每经历一个对象就调用第一个版本的destroy ()

空 间 的 配 置 与 释 放 , std::alloc

  • 对象构造前的空间配置和对象析构后的空间释放,由 <stl_alloc.h>负责, S G I 对此的设计哲学如下:

  •  C + + 的 内 存 配 置 基 本 操 作 是 ::operator new ( ) , 内存释放基本操作 是 : operator delete)). 这两个全局函数相当于C 的malloc ( ) 和 f r e e O 函 数。是的,正是如此,S G I 正是以malloc ()和 f r e e O 完成内存的配置与释放。 
  • 考虑到小型区块所可能造成的内存破碎问题,S G I 设计了双层级配置器,第一级配置器直接使用 malloc() 和 fme(),第二级配置器则视情况采用不同的策略:当配置区块超过128 bytes时,视 之 为 “足够大”,便调用第一级配置器;当配 置区块小于128 bytes时,视 之 为 “过小”,为了降低额外负担(overhead,见 2.2.6 节 ),便采用复杂的memory pool整理方式,而不再求助于第一级配置器, 整个设 计究竟只开放第一级配置器,或是同时开放第二级配置器,取决于_ use_malloc是否被定义(唔,我们可以轻易测试出来,SGI STL并未定义一_USE_MALLOC)

  •  无论alloc被定义为第一级或第二级配置器,SGI还为它再包装一个接口如 下,使配置器的接口能够符合STL规格:

  •  其内部四个成员函数其实都是单纯的转调用,调用传递给配置器(可能是第一级也可能是第二级)的成员函数.这个接口使配置器的配置单位从bytes转为个别元素的大小(sizeof (T) ) . SGI STL容器全都使用这个simple_alloc接口,例如:

 第 一 级 配 置 器 ―malloc_alloc_template 剖析

  •  第一级配置器以malloc () , free () , realloc ()等 C 函数执行实际的内存配置、释放、重配置操作,并实现出类似C + + new-hand宜了的机制。是的,它不能 直 接 运 用 C++ new-handier机制,因为它并非使用::operator n e w 来配置内存。 
  • 所 谓 C++ new handler机制是,你可以要求系统在内存配置需求无法被满足 时 ,调用一个你所指定的函数。换句话说,一 旦 ::operator new无法完成任务, 在 丢 出 std::bad_alloc异常状态之前,会先调用由客端指定的处理例程。该处理例程通常即被称为new-handiero new-handier解决内存不足的做法有特定的模式,请 参 考 《婀 伽 e C++》2 e 条款7 
  • handler

  •  程通常即被称为new-handiero new-handier解决内存不足的做法有特定的模式
  • 注意,S G I 以 malloc而 非 ::operator new来配置内存(我所能够想象的 一个原因是历史因素,另一个原因是C + + 并未提供相应于 realloc () 的内存配 置 操 作 ),因此,S G I 不能直接使用C + + 的 set_new_handler (),必须仿真一个类似的 set_malloc_handler () 
  • 请 注 意 ,S G I 第 一 级 配 置 器 的 allocate ()和 realloc ( ) 都是在调用 malloc ()和 realloc ()不成功后,改调用 oom_malloc ()和 oom_realloc ()=后两者都有内循环,不 断 调 用 “内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但 如 果 “内存不足处理例程”并未被客端设定,oom_malloc() 和 oom_realloc() 便老实不客气地调用 — THROW_BAD_ALLOC,丢 出 bad_alloc异常信息,或 利 用 exit(l)硬生生中止程序。 
  • 记住,设 计 “内存不足处理例程”是客端的责任,设 定 “内存不足处理例程”也是客端的责任.再一次提醒你,“内存不足处理例程”解决问题的做法有着特定的模式,请 参 考 [Meyers98]条款7

第二级配置器 __default_alloc_template 剖析

  • 第二级配置器多了一些机制,避免太多小额区块造成内存的碎片。小额区块带来的其实不仅是内存碎片,配置时的额外负担(overhead)也是一个大问题% 额外 负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存,如图2-3所示。但是区块愈小,额外负担所占的比例就愈大,愈显得浪费。

  • SGI第二级配置器的做法是,如果区块够大,超过128 bytes时,就移交第一 级配置器处理.当区块小于128 bytes时,则以内存池(memory pool)管理,此法 又称为次层配置(sub-allocation):
  • 每次配置一大块内存,并维护对应之自由链表 {free-list). 下次若再有相同大小的内存需求,就直接从free-lists中拨出。如果客户端释还小额区块,就由配置器回收到free-lists中—— 是的,别忘了,配置器除了负 责配置,也负责回收。
  • 为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8 的倍数(例如客端要求30 bytes,就自动调整为32 bytes),并维护 16 个free-lists,各自管理大小分别为 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 bytes的小额区块。free-lists的节点结构如下:

  • 诸君或许会想,为了维护链表(lists), 每个节点需要额外的指针(指向下一 个节点),这不又造成另一种额外负担吗?你的顾虑是对的,但早已有好的解决办法。
  • 注意’上 述 obj所用的是union,由于union之故,从其第一字段观之, obj可被视为一个指针,指向相同形式的另一个。切从其第二字段观之。obj可 被视为一个指针,指向实际区块,如图2-4所示。一物二用的结果是,不会为了维护链表所必须的指针而造成内存的另一种浪费(我们正在努力节省内存的开销呢)。这种技巧在强型(strongly typed)语言如Java中行不通,但是在非强型语言如C++中十分普遍

 空 间 配 置 函 数 allocate()

  • 此函数首先判断区块大小,大 于 128 bytes就调用第一级配置器,小 于 128 bytes就检查对应的free listo 如 果 free list之内有可用的区块,就直接拿来 用,如果没有可用区块,就将区块大小上调至8 倍数边界,然后调用refilio, 准备为free list重新填充空间。refill ()将于稍后介绍。

 

  •  free_list维护8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 bytes的小额区块,需要分配内存的时候,如上图所示,需要分配的大小是96,my_free_list先找到96对应的链条,result指向想要的96区块,my_free_list移动到下一个区块,将result需要的区块排除到链外,表示为其分配了区间

空 间 释 放 函 数 deallocate。

  • 身为一个配置器, _ default_alloc_template 拥有配置器标准接口函数deallocated o 该函数首先判断区块大小,大于128 bytes就调用第一级配置器, 小于128 bytes就找出对应的free list,将区块回收。

  •  使用q指向需要回收的空间,使用my_free_list找到与之大小相匹配的存储区块的链条,使用q衔接对应的 存储区块的链条位置,移动初始位置

重 新 填 充 free lists

  • 回头讨论先前说过的allocate (). 当它发现free list中没有可用区块了时, 就 调 用 refillO,准备为free list重新填充空间.新的空间将取自内存池(经由 chunk_alloC ()完 成 )。缺省取得2 0 个新节点(新区块),但万一内存池空间不 足,获得的节点数(区块数)可能小于20:

 内 存 池 ( m e m o r y pool )

  • 从内存池中取空间给斤e。 使用,是 chunk_alloc()的工作: 

  • 上图存在一个错误,size是需要的大小,nobjs是需要的数量

 

  •  上述的chunk_alloc ( ) 函数以end_free - s t a r t _ f r e e 来判断内存池的水量。如果水量充足,就直接调出20个区块返回给free list。如果水量不足以提供20 个区块,但还足够供应一个以上的区块,就拨出这不足20个区块的空间出去。这时候其pass by reference的n o b js参数将被修改为实际能够供应的区块数。如果 内存池连一个区块空间都无法供应,对客端显然无法交待,此时便需利用m alloc 从 heap中配置内存,为内存池注入活水源头以应付需求。新水量的大小为需求量 的两倍,再加上一个随着配置次数增加而愈来愈大的附加量。

  •  如果一开始free_list 和 内存池都是空的,当用户需求数据的时候,发现没有内存,申请的空间是20的两倍,也就是40,一半内存交给free_list用于数据的维护,一半数据给 内存池;当再次申请内存时,所对应的free_list不存在数据,就会先向内存池索要内存,满足一部分之后,将剩余的内存交给 free_list;

 内存基本处理工具

  • STL定义有五个全局函数,作用于未初始化空间上• 这样的功能对于容器的实现很有帮助,我们会在第4章容器实现代码中,看到它们肩负的重任 前两个函数是2.2.3节说过的、用于构造的construct ()和用于析构的destroy (),另三个函数uninitialized_copy(),uninitialized_fill(),uninitialized_fill_n(), 分别对应于高层次函数copy () fill () fill_n() 这些都是STL算法,将在第6 章介绍。如果你要使用本节的三个低层次函数,应该包含 <memory>, 不过SG I把它们实际定义于 <stl_uninitialized>。

uninitialized_copy

  •  uninitialized_copy ()使我们能够将内存的配置与对象的构造行为分离开来。如果作为输出目的地的[result/ result+(last-first))范围内的每一个迭代器都指向未初始化区域,则 uninitialized_copy ()会使用copy constructor,给身为输入来源之[first, last)范围内的每一个对象产生一份复制品’放进输出 范围中。换句话说,针对输入范围内的每一个迭代器该函数会调用
    construct (&* (result+ (i-f irst) ) , *i), 产 生 * i 的复制品,放置于输出范围的相对位置上。式中的construct ()已于2.2.3节讨论过。
  • result 指向内存拷贝的目的地址,i和first指向的是同一个内存区间,使用i和first之间的差值,将*i(元素的数值)拷贝到指定的位置
  • 果你需要实现一个容器,uninitialized.copy() 这样的函数会为你带来 很大的帮助,因为容器的全区间构造函数(range constructor) 通常以两个步骤完 成:

  •  C++ 标准规格书要求 uninitialized_copy () 具 有 'commit or rollback 语 意,意思是要么“构造出所有必要元素”,要么(当有任何一个copy constructor失 败时)“不构造任何东西””

2.3.2 u n in itia liz e d _fill

  •  &*i   其实就是元素的位置,*i代表元素的数值,加上&,就是取地址,后面接入x,construct就是在指定的位置上填写x
  • 与 uninitialized_copy() 一样,uninitialized_f ill ()必须具备 acommit or rollback语意,换句话说,它要么产生出所有必要元素,要么不产生任何元素。如果有任何一个 copy constructor 丢出异常(exception) ,uninitialized_f ill (),必须能够将已产生的所有元素析构掉。

u n in itia liz e d _ fill_ n

  •  uninitialized_fill_n ()能够使我们将内存配置与对象构造行为分离开来。它会为指定范围内的所有元素设定相同的初值。

  • fill_n
  • 如果是POD类型,使用_true_type,交由高阶函数执行,使用fill_n对每个元素进行赋值
  • 如果不是POD类型,使用_flase_type,调用construct函数对每个元素进行赋值

 uninitialized_copy

  •  这个函数的进行逻辑是,首先萃取出迭代器result的 value type (详见第3 章 ) , 然后判断该型别是否为PO D 型别:

  •  如果是POD类型,采用最有效率的办法就是复制的方式,内部调用fill_n对元素进行复制
  • 如果不是POD类型,只能使用最保险安全的construct的函数构造的方式

  • char是一个字节
  • wchar_t  使用sizeof判断不同环境下的占据的单个元素的空间大小
  • 使用memmove 直接对指定的内存区间进行数据的移动 

u n in itia liz e d _ fill

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值