本文已经迁移到:http://cpp.winxgui.com/cn:a-general-gc-allocator-scopealloc

C++内存管理变革(6):通用型垃圾回收器 - ScopeAlloc

许式伟
2008-1-22

引言

在前文,我们引入了GC Allocator(具备垃圾回收能力的Allocator),并提供了一个实作:AutoFreeAlloc(具体内容參见《C++内存管理变革(2):最袖珍的垃圾回收器 - AutoFreeAlloc》)。

可是,如前所述,AutoFreeAlloc是有其特定的适用环境的(它对内存管理的环境进行了简化,这样的简化环境是常见的。具体參阅《C++内存管理变革(3):另类内存管理 - AutoFreeAlloc典型应用》)。那么,在AutoFreeAlloc不能适用的情形下,我们能够有什么选择?

本文要讨论的,正是这样一个GC Allocator实作。它所抽象的内存管理的环境比之AutoFreeAlloc复杂很多,适用范围也广泛非常多。这个GC Allocator我们称之为ScopeAlloc

思路

在AutoFreeAlloc假象的模型里,一个算法的全部步骤都统一使用同一个GC Allocator,最后的内存由该Allocator统一回收。这个模型非常简洁,非常easy理解和掌握。

理解ScopeAlloc的关键,在于理解我们对AutoFreeAlloc的模型所作的修正。我们设想一个算法的第i步骤比較复杂,其内存开销也 颇为可观,希望为步骤i引入一个私有存储(Private GC Allocator),以便哪些步骤i内部计算用的暂时内存在该步骤结束时释放。示意图例如以下:

图1

因为引入私有存储(Private GC Allocator),模型看起来就变得非常复杂。上面这个图或许让你看晕了。只是没有关系,我们把上图中与步骤i相关的内容独立出来看,得到下图:

图2

如图2显示,一个算法会有自己的私有存储(Private GC Allocator),也会使用外部公有的存储(Share GC Allocator)。之所以是这样,是由于算法的结果集(Result DOM)不能在算法结束时销毁,而应该返回出去。这我们大致能够用下面伪代码表示:

  1. ResultDOM* algorithm(InputArgs args, ScopeAlloc& shareAlloc)
    {
    ScopeAlloc privateAlloc(shareAlloc);
    ...
    ResultDOM* result = STD_NEW(shareAlloc, ResultDOM);
    ResultNode* node = STD_NEW(shareAlloc, ResultNode);
    result->addNode(node);
    ...
    TempVariable* temp = STD_NEW(privateAlloc, TempVariable);
    ...
    return result;
    }

在这段伪代码中,ScopeAlloc是今天的主角。STD_NEWStdExt库中用于生成对象实例的宏,STD_NEW(alloc, Type)其功用等价于《C++内存管理变革(1): GC Allocator》中的New<Type>(alloc)。仅仅是New<Type>模板函数比較“C++”,比較正统,也比較偏于理论1。而STD_NEW则是实际project中的使用方式。

挑战

你可能说,要引入私有存储(Private GC Allocator),为什么非要提供一个新的Allocator?为什么不能是AutoFreeAlloc?为什么不能像以下这样:

  1. ResultDOM* algorithm(InputArgs args, AutoFreeAlloc& shareAlloc)
    {
    AutoFreeAlloc privateAlloc;
    ...
    ResultDOM* result = STD_NEW(shareAlloc, ResultDOM);
    ResultNode* node = STD_NEW(shareAlloc, ResultNode);
    result->addNode(node);
    ...
    TempVariable* temp = STD_NEW(privateAlloc, TempVariable);
    ...
    return result;
    }

答案是,性能问题。我们这里对AutoFreeAlloc和ScopeAlloc这两个GC Allocator的性能进行了对照,结论例如以下:

生成一个新的AutoFreeAlloc实例是一个比較费时的操作,其用户应注意做好内存管理的规划。而生成一个ScopeAlloc实例的开销非常小,你甚至能够哪怕为生成每个对象都去生产一个ScopeAlloc都没有关系(当然我们并不建议你这样做)。

对于多数的算法而言,我们不能确定它所须要的私有存储(Private GC Allocator)的内存空间是多大。或者说,通常它们或许并不大。而在只申请少量内存的情形下,使用AutoFreeAlloc是不太经济的做法。 而相对的,不管算法所需的内存多少,使用ScopeAlloc都能够获得很平稳的性能。

故此,我们的第二个结论是:

AutoFreeAlloc有较强的局限性,只适用于有限的场合(局部的复杂算法);而ScopeAlloc是通用型的Allocator,基本在不论什么情况下,你都可通过使用ScopeAlloc来进行内存管理,以获得良好的性能回报。

实现

看到这里,你的兴趣相信来了,非常想知道ScopeAlloc是长什么样。事实上,ScopeAlloc仅仅是还有一个“AutoFreeAlloc”。我们来看看它的定义:

  1. typedef AutoFreeAllocT<ProxyBlockPool> ScopeAlloc;

而我们的AutoFreeAlloc它的定义是:

  1. typedef AutoFreeAllocT<DefaultStaticAlloc> AutoFreeAlloc;

具体的代码,參考下面链接:

能够看出,ScopeAlloc和AutoFreeAlloc唯一的差别,在于AutoFreeAlloc向系统申请内存(调用的是 malloc/free),而ScopeAlloc向一个内存池(即BlockPool,调用的是BlockPool:: allocate/deallocate)。

BlockPool

BlockPool 就是通常我们所说的内存池(Memory Pool)。可是它比一般的内存池要简单非常多,由于它仅仅是管理MemBlock,而不负责对MemBlock进行结点(Node)2的划分(这个工作实际上由AutoFreeAllocT完毕了)。

BlockPool的规格例如以下:

  1. class BlockPool
    {
    BlockPool(int cbFreeLimit, int cbBlock);
    void* allocate(size_t cb); // 申请一个MemBlock
    void deallocate(void* p); // 释放一个MemBlock
    void clear(); // 清空全部申请的内存
    };

关于该类的实现细节,我并不多解释,大家能够參考内存池(MemPool)技术具体解释。我解释下构造函数的两个參数:cbFreeLimit、cbBlock是什么。

cbBlock

这个量比較好解释,是指单个MemBlock的字节数。

cbFreeLimit

大家都知道,内存池技术在释放内存时,它并非将内存真的释放(还给系统),而是记录到一个FreeList中,以加快内存申请的速度。可是这带来 的一个问题是,内存池随着时间的推移,其占有的内存会不断 地增长,从而不断地吃掉系统的内存。cbFreeLimit的引入是为了限制了FreeList中的内存总量,从而抑制这样的情况的发生。在 BlockPool中的FreeList内存达到cbFreeLimit时,deallocate操作直接释放MemBlock。代码例如以下:

  1. class BlockPool
    {
    public:
    void deallocate(void* p) // 提醒:m_nFreeLimit = cbFreeLimit / cbBlock + 1
    {
    if (m_nFree >= m_nFreeLimit) {
    free(p);
    }
    else {
    _Block* blk = (_Block*)p;
    blk->next = m_freeList;
    m_freeList = blk;
    ++m_nFree;
    }
    }
    }

ProxyBlockPool

它仅仅是BlockPool的代理。定义例如以下:

  1. typedef ProxyAlloc<BlockPool> ProxyBlockPool;

而Proxy是什么?简单地不能再简单:

  1. template <class AllocT>
    class ProxyAlloc
    {
    private:
    AllocT* m_alloc;
  2.  
  3. public:
    ProxyAlloc(AllocT& alloc) : m_alloc(&alloc) {}
  4.  
  5. public:
    void* allocate(size_t cb) { return m_alloc->allocate(cb); }
    void deallocate(void* p) { m_alloc->deallocate(p); }
    void swap(ProxyAlloc& o) { std::swap(m_alloc, o.m_alloc); }
    };

ScopeAlloc

如上所述,ScopeAlloc仅仅是一个typedef:

  1. typedef AutoFreeAllocT<ProxyBlockPool> ScopeAlloc;

而关于AutoFreeAlloc的细节,前面《C++内存管理变革(2):最袖珍的垃圾回收器 - AutoFreeAlloc》中我们已经做了具体介绍。

ThreadModel

关于线程模型(ThreadModel),从上面给出的代码(ScopeAlloc.h)中你能够看到相关的代码。可是具体的解释超出了本文的范畴,我们会另外一篇专门解释GC Allocator与线程模型(ThreadModel)之间的关系3

时间性能分析

关于性能问题,我们前面已经作了AutoFreeAlloc和ScopeAlloc的性能对照。这里简单再做一下分析。

内存申请/释放过程

这两个过程ScopeAlloc与AutoFreeAlloc基本差点儿相同。考虑到ScopeAlloc使用了MemPool技术,从统计意义上来讲,假设系统存在频繁的内存申请和释放,则ScopeAlloc性能略好于AutoFreeAlloc。

构造过程

基本上都仅仅是指针赋值,可忽略不计。

析构过程

因为ScopeAlloc析构时将内存归还给内存池,而不是还给系统,ScopeAlloc的时间性能要好过AutoFreeAlloc很多。更确 切地讲,两者的时间复杂度都是O(N),当中N为MemBlock的个数(也就是Allocator所占的内存总量),可是因为释放MemBlock操作 的单位时间不同(BlockPool::deallocate比free快很多),导致两者的性能有异。

使用例子

AutoFreeAlloc和ScopeAlloc的性能对照中当然不是ScopeAlloc的典型用例。这里我们举一个:

  1. class Obj
    {
    private:
    int m_val;
    public:
    Obj(int arg = 0) {
    m_val = arg;
    printf("construct Obj: %d/n", m_val);
    }
    ~Obj() {
    printf("destruct Obj: %d/n", m_val);
    }
    };
  2.  
  3. void testScope()
    {
    std::BlockPool recycle;
    std::ScopeAlloc alloc(recycle);
    printf("/n------------------- global: have 3 objs ----------------/n");
    {
    Obj* a1 = STD_NEW(alloc, Obj)(0);
    Obj* a2 = STD_NEW_ARRAY(alloc, Obj, 2);
    printf("------------------- child 1: have 4 objs ----------------/n");
    {
    std::ScopeAlloc child1(alloc);
    Obj* o1 = STD_NEW(child1, Obj)(1);
    Obj* o2 = STD_NEW_ARRAY(child1, Obj, 3);
    printf("------------------- child 11: have 3 objs ----------------/n");
    {
    std::ScopeAlloc* child11 = STD_NEW(child1, std::ScopeAlloc)(child1);
    Obj* o11 = STD_NEW(*child11, Obj)(11);
    Obj* o12 = STD_NEW_ARRAY(*child11, Obj, 2);
    }
    printf("------------------- leave child 11 ----------------/n");
    printf("------------------- child 12: have 3 objs ----------------/n");
    {
    std::ScopeAlloc child12(child1);
    Obj* o11 = STD_NEW(child12, Obj)(12);
    Obj* o12 = STD_NEW_ARRAY(child12, Obj, 2);
    }
    printf("------------------- leave child 12 ----------------/n");
    }
    printf("------------------- leave child 1 ----------------/n");
    printf("------------------- child 2: have 4 objs ----------------/n");
    {
    std::ScopeAlloc child2(alloc);
    Obj* o1 = STD_NEW(child2, Obj)(2);
    Obj* o2 = STD_NEW_ARRAY(child2, Obj, 3);
    }
    printf("------------------- leave child 2 ----------------/n");
    }
    }

这个例子中,child11是特别要注意的。请注意child11它是new出来的,我们忘记释放它4。可是不要紧,在child1析构时,child11将会被删除。

我们看到,有了ScopeAlloc,内存管理就能够层层规划,成为一个内存管理树(逻辑ScopeAlloc树5)。你能够忘记释放内存(其实你不能释放,仅仅能clear),ScopeAlloc会记得为你做这种琐事。这正是GC Allocator的精髓。

ScopeAlloc的名字来由,看这个例子就能够体会一二了。在《C++内存管理变革(1): GC Allocator》我们特别提到,内存管理有非常强的区域性。在不同的区域(Scope),因为算法不同,而导致对Allocator需求亦不同。从整体上来讲,ScopeAlloc有更好的适应性,适合更为广泛的问题域。

Footnotes
1. StdExt库最初提供了New<Type>函数函数,可是后来终于还是决定废弃之。
2. 在 boost 的 pool/object_pool 类中,结点(Node)被称为块(Chunk)。
3. 你已经看到,AutoFreeAlloc并不考虑ThreadModel,而ScopeAlloc也并不全然支持。
4. 实际上也没有办法释放,由于根本没有STD_DELETE这种东西,只是能够调用child11->clear()来释放由child11负责的内存。
5. ScopeAlloc的层次是逻辑上的。实际上ScopeAlloc并无层次。例如以下:

  1. std::BlockPool recycle;
    std::ScopeAlloc alloc(recycle);
    {
    std::ScopeAlloc child(alloc);
    ...
    }

并非说child就是alloc的子存储(Allocator)。仅仅是通常child生命周期比alloc短,从而有逻辑上的父子关系。
以上代码等价于:

  1. std::BlockPool recycle;
    std::ScopeAlloc alloc(recycle);
    {
    std::ScopeAlloc child(recycle);
    ...
    }

这里就非常easy看出,事实上alloc、child是平等的。

C++内存管理变革(6):通用型垃圾回收器 - ScopeAlloc的更多相关文章

  1. 你必须了解的java内存管理机制(三)-垃圾标记

    本文在个人技术博客不同步发布,详情可用力戳 亦可扫描屏幕右侧二维码关注个人公众号,公众号内有个人联系方式,等你来撩... 相关链接(注:文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8) ...

  2. 你必须了解的java内存管理机制(四)-垃圾回收

    本文在个人技术博客不同步发布,详情可用力戳 亦可扫描屏幕右侧二维码关注个人公众号,公众号内有个人联系方式,等你来撩... 相关链接(注:文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8) ...

  3. 【python测试开发栈】—python内存管理机制(二)—垃圾回收

    在上一篇文章中(python 内存管理机制-引用计数)中,我们介绍了python内存管理机制中的引用计数,python正是通过它来有效的管理内存.今天来介绍python的垃圾回收,其主要策略是引用计数 ...

  4. iOS非ARC内存管理摘要 - 实践型

    关于ios内存管理.在开发过程中,内存管理很重要,我简单说明一下. 1.正确用法 UIView *v = [[UIView alloc] init]; //分配后引用计数为1 [self.view a ...

  5. android 内存管理机制、异常、垃圾回收

    当 Android 应用程序退出时,并不清理其所占用的内存,Linux 内核进程也相应的继续存在,所谓“退出但不关闭”.从而使得用户调用程序时能够在第一时间得到响应. 当系统内存不足时,系统将激活内存 ...

  6. cocos2dx 内存管理

    转载自 ocos2dx 内存管理 - 小花原创博客 - 博客频道 - CSDN.NET http://blog.csdn.net/ring0hx/article/details/7946397 coc ...

  7. (转)cocos2dx 内存管理

    原文地址:http://blog.csdn.net/ring0hx/article/details/7946397 cocos2dx的内存管理移植自Objective-C, 对于没有接触过OC的C++ ...

  8. Android内存优化5 了解java GC 垃圾回收机制3

    引言 接App优化之内存优化(序), 作为App优化系列中内存优化的一个小部分. 由于内存相关知识比较生涩, 内存优化中使用到的相关工具, 也有很多专有名词. 对Java内存管理, GC, Andro ...

  9. C++中的垃圾回收和内存管理

    最开始的时候看到了许式伟的内存管理变革系列,看到性能测试结果的时候,觉得这个实现很不错,没有深入研究其实现.现在想把这个用到自己的一个项目中来,在linux下编译存在一些问题,所以打算深入研究一下. ...

随机推荐

  1. ArcGIS10.3+Oracle12C+ArcGIS Server10.3安装布署(之三)

    1.将Oracle的客户端切换到64位 (1)将C:\下的instantclient_12_1目录重命名为instantclient_12_1X86 (2)从Oracle的官方网站下载   insta ...

  2. Android 仿美团网,探索使用ViewPager+GridView实现左右滑动查看更多分类的功能

    看下效果图,自己考虑下自己会如何实现,然后再继续看看作者的实现~ 不记得什么时候,我留意到到美团网首页有使用ViewPager+GridView实现左右滑动查看更多分类的一个功能,感觉它很有趣,于是想 ...

  3. JSON学习笔记-2

    JSON的语法 1.JSON 数据的书写格式是:名称/值对. "name" : "我是一个菜鸟" 等价于这条 JavaScript 语句: name = &qu ...

  4. Storm并行度

    1.Storm并行度相关的概念 Storm集群有很多节点,按照类型分为nimbus(主节点).supervisor(从节点),在conf/storm.yaml中配置了一个supervisor有多个槽( ...

  5. idea 版本控制忽略文件、文件夹设置

    setting 或者底部的 设置 忽略某个文件 后面选择框可以去选择 忽略某个文件夹 后面选择框可以去选择 忽略某种文件 后面输入填写如: *.txt

  6. 17 汽车服务工程 李腾飞 MP4

  7. IP地址分类和网段区分的知识

    IP地址分类/IP地址10开头和172开头和192开头的区别/判断是否同一网段 简单来说在公司或企业内部看到的就基本都是内网IP,ABC三类IP地址里的常见IP段. 每个IP地址都包含两部分,即网络号 ...

  8. U盘安装win7系统

    windows 7安装 准备 1.iso系统镜像文件(我一般选用纯净版) 2.Windows7-USB-DVD-Download-Tool-Installer-en-US(启动盘制作) 3.准备4G以 ...

  9. for 与forEach的区别

    for循环 for循环,通过下标,对循环中的代码反复执行,功能强大,可以通过index取得元素.在处理比较复杂的处理的时候较为方便. foreach循环 foreach,从头到尾,对于集合中的对象遍历 ...

  10. 个人作业2:APP案例分析--腾讯动漫

    第一部分 调研,评测 个人第一次上手体验 以往看漫画就是在浏览器直接搜索在网页上看,直到用了腾讯动漫APP,我才摒弃这个很low的方法.腾讯动漫直接用qq就可以登陆,有更齐全的漫画分类,更清晰的画质, ...