C++内存管理变革(3):另类内存管理 - AutoFreeAlloc典型应用

最简单的C++/Java程序

最简单的Java程序:

class Program
{
   public static void main()
   {
       new int;
   }
}

对应的C++程序:

void main()
{
   new int;
}

我想没有一个Java程序员会认为上面的Java代码存在问题。但是所有严谨的C++程序员则马上指出:上面这个C++程序有问题,它存在内存泄漏。但是我今天想和大家交流的一个观念是:这个C++程序没有什么问题。

DocX程序的内存管理

DocX是我开发的一个文档撰写工具。这里有关于它的一些介绍。在这一小节里,我要谈谈我在DocX中尝试的另类内存管理方法。

DocX的总体流程是:

  1. 读入一个C++源代码(或头)文件(.h/.c/.hpp/.cpp等),分析其中的注释,提取并生成xml文档。
  2. 通过xslt变换,将xml文档转换为htm。
  3. 分析源代码中的所有include指令,取得相应的头文件路径,如果某个头文件没有分析过,跳到1反复这些步骤。
  4. 最后所有生成的htm打包生成chm文件。

一开始,我象Java/C#程序员做的那样,我的代码中所有的new均不考虑delete。当然,它一直运作得很好,直到有一天我的文档累计到了一定程度后。正如我们预见的那样,DocX程序运行崩溃了。

那么,怎么办呢?找到所有需要delete的地方,补上delete?

这其实并不需要。在前面,我给大家介绍了AutoFreeAlloc(参见《C++内存管理变革(2):最袖珍的垃圾回收器》),也许有人在嘀咕,这样一个内存分配器到底有何作用。——那么,现在你马上可以看到它的典型用法之一了:

对于我们的DocX崩溃后,我只是做了以下改动:

  1. 加一个全局变量:std::AutoFreeAlloc alloc;
  2. 所有的new Type(arg1, arg2, …, argn),改为STD_NEW(alloc, Type)(arg1, arg2, …, argn);
  3. 所有的new Type[n],改为STD_NEW_ARRAY(alloc, Type, n);
  4. 每处理完一个源代码文件时,调用一次alloc.clear();

搞定,自此之后,DocX再也没有内存泄漏,也不再有遇到内存不足而崩溃的情形。

只读DOM模型(或允许少量修改)的建立

在《文本分析的三种典型设计模式》一文中我推荐大家使用DOM模型去进行文件操作。并且通常情况下,这个DOM模型是只读DOM模型(或允许少量修改)。

对于只读DOM模型,使用AutoFreeAlloc是极其方便的。整个DOM树涉及的内存统一由同一个AutoFreeAlloc实例进行分配。大体如下:

class Document;
class ObjectA
{
private:
    Document* m_doc;
    SubObject* m_c;
 
public:
    ObjectA(Document* doc) : m_doc(doc) {
        m_c = STD_NEW(doc->alloc, SubObject);
    }
 
    SubObject* getC() {
        return m_c;
    }
};
 
class Document
{
public:
    AutoFreeAlloc alloc;
 
private:
    ObjectA* m_a;
    ObjectB* m_b;
 
public:
    ObjectA* getA() {
        if (m_a == NULL)
            m_a = STD_NEW(alloc, ObjectA)(this);
        return m_a;
    }
};

通过这种方式创建的DOM模型,只要你删除了Document对象,整个DOM树自然就被删除了。你根本不需要担心其中有任何内存泄漏的可能。

另类内存管理的观念

通过以上内容,我试图向大家阐述的一个观点是:

  • 有了AutoFreeAlloc后,C++程序员也可以象GC语言的程序员一样大胆new而不需要顾忌什么时候delete。

展开来讲,可以有以下结论:

  • 如果你程序的空间复杂度为O(1),那么只new不delete是没有问题的。
  • 如果你程序的空间复杂度为O(n),并且是简单的n*O(1),那么可以用AutoFreeAlloc简化内存管理。
  • 如果你程序的空间复杂度为O(t),其中t是程序运行时间,并且你不能确定程序执行的总时间,那么AutoFreeAlloc并不直接适合你。比较典型的例子是Word、Excel等文档编辑类的程序。

用AutoFreeAlloc实现通用型的GC

本小节的内容纯属YY,只是基于探讨和思路拓展的目的,实际上本人并不推荐你真的去怎么做。

AutoFreeAlloc对内存管理的环境进行了简化,这种简化环境是常见的。在此环境下,C++程序员获得了无可比拟的性能优势。当然,在一般情形下,AutoFreeAlloc并不适用。

那么,一个通用的半自动GC环境在C++是否可能?《C++内存管理变革系列》的核心就是要告诉你:当然可以。并且,我们推荐C++程序员使用半自动的GC,而不是Java/C# 中的那种GC。

通用的半自动GC环境可以有很多种建立方式。这里我们简单聊一下如何使用AutoFreeAlloc去建立。

我们知道,使用AutoFreeAlloc,将导致程序随着时间推移,逐步地吃掉可用的内存。假设现在已经到达我们设置的临界点,我们需要开始 gc。整个过程和Java等语言的gc其实完全类似:通过一个根对象(Object* root),获得所有活动着的对象(Active Objects),将它们复制到一个新的AutoFreeAlloc中:

Object* gc(AutoFreeAlloc& oldAlloc, Object* root, AutoFreeAlloc& newAlloc)
{
    Object* root2 = root->clone(newAlloc);
    oldAlloc.clear();
    return root2;
}

如果C++象Java/C#那样有足够丰富的元信息,那么Object::clone过程就可以象Java/C# 等语言那样自动完成。这些元信息对于GC过程的用处无非在于,我们可以遍历整个活动对象的集合,然后把这些活动对象复制一份。没有复制过来的对象自然而然就被丢弃了。

GC的原理就是这么简单。没有元信息也没关系,只要我们要求每个由GC托管的对象支持clone函数,一切就ok了。对于一个复杂程序,要求每个对象提供clone函数不见得是什么过分的要求,clone函数也不只有gc过程才需要,很多对象在设计上天然就需要clone。

补充说明

关于全局AutoFreeAlloc变量

我个人非常不推荐使用全局变量(除非是常量:不一定用const修饰,指的是经过一定初始化步骤后就不在修改的变量)。上面只是对于小型的单线程程序偷懒才这样做。

关于用AutoFreeAlloc实现通用型的GC

本小节的内容纯属YY,只是基于探讨和思路拓展的目的,实际上本人并不推荐你真的去怎么做。如果你决定选择这种做法,请仔细推敲细节。可以预见的一些细节有:

  • AutoFreeAlloc与线程模型(ThreadModel)。AutoFreeAlloc关注点在于快,它通常不涉及跨线程问题。但是如果要作为通用型的GC,这一点不能不考虑。为了性能,推荐每个线程独立管理内存,而不要使用互斥体。
  • 性能优化。可以考虑象Java的GC那样,使用两个AutoFreeAlloc,把对象划分为年轻代和年老代。

推荐的相关文章

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License

Subscription expired — please renew

Pro account upgrade has expired for this site and the site is now locked. If you are the master administrator for this site, please renew your subscription or delete your outstanding sites or stored files, so that your account fits in the free plan.