C++内存管理变革 收藏
C++内存管理变革
许式伟
2005-7-16
关键字: 内存管理 垃圾回收器 内存配置器 引用计数
keyword: memory manage, memory recycler, allocator, reference count
引言
C/C++语言的内存管理经历了几次变革,但至今仍未能趋于成熟。这几次变革主要包括:
1. 从malloc/free到new/delete。这场变革是OOP技术兴起的产物。C++是强类型语言,new/delete的主要成果也就是加强了类型观念,减少了强制类型转换的需求。但是从内存管理角度看,这个变革并没有多少的突破性。
2. 从new/delete到内存配置器(allocator)。自从STL被纳入C++标准库后,C++世界产生了巨大的变化。而从内存管理角度来看,allocator的引入也是C++内存管理一个突破。留意一下你就可以发现,整个STL所有组件的内存均从allocator分配。也就是说,STL并不推荐使用new/delete进行内存管理,而是推荐使用allocator。
然而,STL的allocator并没有导致C++语言在内存管理上发生巨大的变化。除了STL本身外,并没有多少人使用allocator,甚至是意识到allocator的重要性。所以C++程序员在使用STL的同时,依旧在使用new/delete进行烦琐的内存分配/释放过程。
究其原因,主要有二。一是allocator的引入,STL设计者主要可能还是出于将内存管理从容器的实现独立出来的设计理念作用,让STL使用者在内存管理算法上有选择的余地。设计者本身都可能也没有意识到allocator的重要性。二是allocator本身也只是侧重于关注效率上,而没有侧重于C++语言使用者对内存管理观念的变革上。
然而,STL的allocator并没有导致C++语言在内存管理上发生巨大的变化。除了STL本身外,并没有多少人使用allocator,甚至是意识到allocator的重要性。所以C++程序员在使用STL的同时,依旧在使用new/delete进行烦琐的内存分配/释放过程。
究其原因,主要有二。一是allocator的引入,STL设计者主要可能还是出于将内存管理从容器的实现独立出来的设计理念作用,让STL使用者在内存管理算法上有选择的余地。设计者本身都可能也没有意识到allocator的重要性。二是allocator本身也只是侧重于关注效率上,而没有侧重于C++语言使用者对内存管理观念的变革上。
总之,在我看来,STL的引入allocator,是一件了不起的事情。但是这场变革被忽视了,没有得到贯彻。当然,这也与STL的allocator本身的缺陷有关。
本文要讨论的,正是如何贯彻STL的allocator思想,对其进行适当的改进,以期在C++内存管理观念上产生变革性的突破,彻底淘汰传统的new/delete内存管理方法[1]。
垃圾回收器
几乎所有目前流行的垃圾回收器,均倾向于将使用者当作一个傻瓜,期望能够让使用者在完全不理解内存管理的情况下,可以很好的使用它。应该说这它们基本上都也做到了(虽然使用者有时也有这样那样的烦恼,但总体来说情况确实得到了很大程度的改善)。然而这一设计理念我并不十分认同。
首先,可以在一个提供垃圾回收器的语言中自如的工作,没有被垃圾回收器所困扰,本身已经是很了不起的事情,他们绝对是非常聪明的人,而不是傻瓜。他们理解垃圾回收器的工作原理,选择它并且让它为他们工作,只是因为还有更重要的事情等着他们去做。必要的时候,他们需要有办法控制垃圾回收器,使它按照他们的意愿工作。因此,垃圾回收器的设计要点在于把使用者从烦琐的内存管理中解脱出来,使得他们可以将全部精力投入到本身的业务逻辑上,而不是让垃圾回收器看起来更傻瓜式。
其次,使用一个全自动的垃圾回收器,在内存回收的时机不明确的情况下,垃圾回收器的工作过程有很大的不确定性,这给使用者带来烦恼。例如C#在调用非管制代码(如调用Win32 api)时,这些问题变得突出。一个不小心,就有可能出现Win32 api还在使用一块内存,而垃圾回收器已经把它回收了的情形。在小心翼翼的避开这些陷阱时,这种感觉其实与C/C++程序员遗憾语言没有垃圾回收器的感觉有点类似。
因此,最理想的情况,是内存管理器提供垃圾回收的能力,但是它也只是提供这个能力而已,至于什么时候进行垃圾回收,完全可以由用户自己控制。另外,用户也可以强制释放一块内存,而不是完全被动的等待垃圾回收过程决策何时回收该内存。对于客户来说,他有权掌控一切,只是如果万一他确实疏忽了,垃圾回收器能够为他护航。
将垃圾回收器引入C++,有没有这种可能呢?我认为,如果我们试图提供一个全自动的垃圾回收器,这相当困难。我们看到以Microsoft之能,仍然无法把这件事做好[2]。或许,我们需要改变一下观念:一个半自动的垃圾回收器,也许就可能可以和C++融洽相处了呢?
初识allocator
allacator中文称为“内存配置器”,通常它是一个类,负责提供内存管理(可能包含内存分配、释放、自动回收等能力)相关的服务。例如,我们通过C提供的malloc/free即刻提供一个allocator实作出来:
class SimpleAlloc
{
public:
//注意这里提供的参数fnDestroy,它是为那些具备垃圾回收能力的allocator需要提供。
void* Alloc(size_t cb, FnDestructor fnDestroy = NULL)
{
return malloc(cb);
}
//注意这里有看似多余的参数cb,这完全是为了和后续提供的allocator规格一致的需要。
void Free(void* data, size_t cb)
{
free(data);
}
};
有了allocator,我们可以申请内存了,但是我们还不能用它创建一个C++对象。为了方便创建C++对象,我们提供了辅助的New操作,原型大体如下:
template <class Type, class AllocType>
Type* New(AllocType& alloc); // 类似于new Type
template <class Type, class ArgType1, class AllocType>
Type* New(ArgType1 arg1, AllocType& alloc); // 类似于new Type(arg1)
template <class Type, class AllocType>
Type* NewArray(size_t count, AllocType& alloc);// 类似于new Type[count]
有了这些辅助函数,我们就可以创建对象了。使用样例:
SimpleAlloc alloc;
int* intArray = NewArray<int>(count, alloc);
MyClass* obj = New<MyClass>(alloc);
MyClass* objWithArg = New<MyClass>(arg1, alloc);
MyClass* objArray = NewArray<MyClass>(count, alloc);
这里我们虽然使用SimpleAlloc创建对象,但是需要提醒的是,这些New操作对所有的allocator有效。如果你关心New函数的代码,先不急,下面我们马上就可以看到了。但是首先我们要继续讨论一下allocator。
allocator引起的观念变化
接触allocator,你可以体会到了它与C++传统的new/delete观念的不同。这主要有以下几点:
1. 每个类(或者算法)本身,均有最合适它的内存管理机制,并不是向C++传统的做法那样,使用一个全局的new/delete。也许你会说,C++不也允许一个类定义自己的new和delete吗?是的,C++的确支持类定义自己的new/delete,但注意,它的理念和allocator完全不同。我不认为它是C++的一个优秀之作,相反,它起到了误导作用。
因为,决定一个类对象怎么去new出来,并不是取决于该类本身,而相反是取决于使用该类的人。一个类不需要关心自身被如何创造出来,更不能假定。它需要关心的是它自己的类成员如何被创建出来,它的算法(你可以把类看做一个算法集合)涉及到的所有组件如何被创建出来。而这,才是allocator带来的观念。
让各种各样的allocator创建同一个类的不同实例,这些实例甚至可能在一起工作,相互协作。从STL的角度讲,这完全是最正常不过的事情了。
因为,决定一个类对象怎么去new出来,并不是取决于该类本身,而相反是取决于使用该类的人。一个类不需要关心自身被如何创造出来,更不能假定。它需要关心的是它自己的类成员如何被创建出来,它的算法(你可以把类看做一个算法集合)涉及到的所有组件如何被创建出来。而这,才是allocator带来的观念。
让各种各样的allocator创建同一个类的不同实例,这些实例甚至可能在一起工作,相互协作。从STL的角度讲,这完全是最正常不过的事情了。
2. 重要的是由allocator创建管理对象,避免在你的代码中使用new/delete。如果可能,你可以如STL那样,将allocator作为模板参数,不绑定具体的某个内存管理器。但是,如果你的算法依赖了某个allocator的实现特有的功能,这也并不要紧。你的目的不是要做到allocator的可替换,不是吗?重要的是使用了这个allocator了,它给你在内存管理上带来了益处。
但是,应该看到,STL实作的各种allocator,目前来看除了最简单使用malloc/free实现的外,主要就是基于mempool技术。而该技术的目标,不是让内存使用者更加方便有效地进行内存管理,而更多的是关注于内存分配的时间性能。为了让C++程序员从内存管理中解脱出来,我们需要实作新的alloctor,需要新的突破!
新视角:具垃圾回收能力的Allocator
对,我设想的一个做法是,贯彻STL的allocator观念,并且提供具备特定的内存管理能力(例如垃圾回收)的各种allocator。让C++社区广泛接受allocator观念,并且从中受益。C++程序员是时候抛弃传统的new/delete,让他们退出历史舞台了。
我接下来会实作两个具体的allocator(均属原创)。相信它们会让你耳目一新,让你不禁想到:哦,原来在C++中,我还可以这样进行内存管理。
当然,我最大的希望就是,这两个allocator能够起到抛砖引玉的作用,让大家也清楚地意识到allocator的重要性,可以出现更多的具备各种能力的allocator,解脱C++程序员一直以来的苦难(可能是最大苦难[3])。
这两个allocator均具备一定程度的垃圾回收能力。只是观念上各有各的侧重。我们接下来会分为两个专题专门对它们进行阐述。
辅助的New过程
我们终于可以开始讨论前文提到的New函数的实现上了。以不带参数的New为例,它的代码如下,可能并没有你想象的那么复杂:
#include <new>
template <class Type, class AllocType>
inline Type* New(AllocType& alloc)
{
void* obj = alloc.Alloc(sizeof(Type), DestructorTraits<Type>::Destruct);
return new(obj) Type;
}
其中DestructorTraits是一个根据类型Type萃取[4]析构函数的萃取器。它看起来是这样的:
template <class Type>
struct DestructorTraits
{
static void Destruct(void* pThis)
{
((Type*)pThis)->~Type();
}
};
这样,你就可以通过以下代码new出对象了:
MyClassA* obj = New<MyClassA>(alloc);
MyClassB* obj = New<MyClassB>(alloc);
特别提醒:这里New函数在VC++ 6.0下编译通过,但是产生的执行代码存在严重bug。如果你只New一类对象,没有问题,但在New了多种对象后,似乎VC++对MyClassA、MyClassB 两者混淆起来了。为了支持VC++ 6.0,你需要对这里的New做出调整(关于这一点,详细请参考:VC++ 6.0小技巧)。
COM技术[5]与内存管理
已经准备结束这篇短文的时候,忽然想到了长久以来使用COM技术形成的一些感想,这些想法恰恰与内存管理紧密相关。故此想就这个问题陈述一下。
从COM的IUnknown接口看,它主要关注两个问题:一个是QueryInterface,一个是引用计数(AddRef/Release)。COM组件很讲究信息的屏蔽,使用者对组件的认识有限,这就给组件升级、扩充功能提供了可能。QueryInterface是一个很好的概念,需要发扬光大。
COM的引用计数则关注的是组件的生命期维护问题。换句话说,就是组件如何销毁的问题。诚然,组件对象的销毁问题,是内存管理的关键。无论是COM的引用计数,还是垃圾回收技术,均是要解决对象的销毁问题。只是两者的侧重点不太一样,COM引用计数更关注“确保组件不会被提前销毁了,确保组件访问的安全性”,而垃圾回收器则关注“不管怎样确保组件最终被销毁,没有内存泄漏”。
在COM中,确保组件访问的安全性(避免非法访问),这个观点太重要了,以至于它甚至不惜加重程序员的内存管理负担。所以,在COM程序中,出现内存泄漏太正常了,而且一旦泄漏通常就是大片大片内存的漏。更加要命的是,你甚至不能有一个很简单有效的方法确认这个泄漏是由于哪段代码引起。因为组件所有的客户都是平等的,任何一个客户代码存在问题均将导致内存的泄漏。
刚开始接触COM技术的时候,我对引用计数持的是比较正面的态度。但是随着部门逐步加大COM技术的使用力度后,四五年下来,我渐渐开始迷惑起来。一切并不如想象的那样。这个引用计数的背后,需要我们付出多少额外的代价!
而这个迷惑、思索,可能就是本文以及后续相关内容的成因吧。
[2] Microsoft的财力实力是有目共睹的。然而它在VC++中进行管制代码的尝试,看起来并不怎么成功。而且就以Microsoft之能,也无法把经典的C++的非管制代码与管制代码融合的很好。我想,我们不应该再钻这个死胡同了。
[3] 当然,也许C++另一个致命伤是没有一个标准的图形/界面库。然而毕竟还是有那些形形色色的界面库支撑着。而内存管理,似乎是C++有生俱来的缺陷,没有得到应有的重视。
[4] 萃取,英文traits,是C++模板特化技术应用的一个典范,被stl、boost等库广泛使用。
[5] COM技术贯穿于Microsoft所有的产品之中并带来了巨大的成功。要了解COM技术,有不少好书,如《COM本质论》、《COM技术内幕》等等。
发表于 @ 2006年11月16日 21:41:00 | 评论( 42 ) | 举报| 收藏
std::allocator的作用也不是内存管理这么简单.
你能确定你的程序运行性能问题是由new/delete引起的?
allocator使用mempool的方式,效率要好得多
allocator使用mempool的方式,效率要好得多
“因此,最理想的情况,是内存管理器提供垃圾回收的能力,但是它也只是提供这个能力而已,至于什么时候进行垃圾回收,完全可以由用户自己控制。另外,用户也可以强制释放一块内存,而不是完全被动的等待垃圾回收过程决策何时回收该内存。对于客户来说,他有权掌控一切,只是如果万一他确实疏忽了,垃圾回收器能够为他护航。”
这个说法我觉得有些片面。作者仅考虑了语言,而没有考虑语言所要服务的目标系统。内存管理可能更是目标环境要绝对控制的方面,呵呵,作为一种灵活的语言,要考虑和各种目标系统融洽。
“因为,决定一个类对象怎么去new出来,并不是取决于该类本身,而相反是取决于使用该类的人。一个类不需要关心自身被如何创造出来,更不能假定。它需要关心的是它自己的类成员如何被创建出来,它的算法(你可以把类看做一个算法集合)涉及到的所有组件如何被创建出来。而这,才是allocator带来的观念。”
这一句有些武断,可能是另外一种“误导”呢,呵呵,如果按照作者说的方向去发展,C++就不会那么灵活了。其实,new/delete和alloc/free并不如此的不相容,前者只是稍多考虑了面向对象时的构造和析构,而且可以被重写。到底需要用什么方式去使用内存,需要C++程序员,在实际环境下作出决定。
另外,关于COM,我还是持正面态度的。它的引用计数规则,已经简单得不能再简单了。COM的不足不在这里,关于不足,DonBox的看法是比较深刻的。另外,推荐你看看这个小短文 http://blog.csdn.net/codesigner/posts/1261485.aspx
“因此,最理想的情况,是内存管理器提供垃圾回收的能力,但是它也只是提供这个能力而已,至于什么时候进行垃圾回收,完全可以由用户自己控制。另外,用户也可以强制释放一块内存,而不是完全被动的等待垃圾回收过程决策何时回收该内存。对于客户来说,他有权掌控一切,只是如果万一他确实疏忽了,垃圾回收器能够为他护航。”
这个说法我觉得有些片面。作者仅考虑了语言,而没有考虑语言所要服务的目标系统。内存管理可能更是目标环境要绝对控制的方面,呵呵,作为一种灵活的语言,要考虑和各种目标系统融洽。
“因为,决定一个类对象怎么去new出来,并不是取决于该类本身,而相反是取决于使用该类的人。一个类不需要关心自身被如何创造出来,更不能假定。它需要关心的是它自己的类成员如何被创建出来,它的算法(你可以把类看做一个算法集合)涉及到的所有组件如何被创建出来。而这,才是allocator带来的观念。”
这一句有些武断,可能是另外一种“误导”呢,呵呵,如果按照作者说的方向去发展,C++就不会那么灵活了。其实,new/delete和alloc/free并不如此的不相容,前者只是稍多考虑了面向对象时的构造和析构,而且可以被重写。到底需要用什么方式去使用内存,需要C++程序员,在实际环境下作出决定。
另外,关于COM,我还是持正面态度的。它的引用计数规则,已经简单得不能再简单了。COM的不足不在这里,关于不足,DonBox的看法是比较深刻的。另外,推荐你看看这个小短文 http://blog.csdn.net/codesigner/posts/1261485.aspx
有吗?
operator new ,operator delete 是可以重载的啊
com本来就不是为C++准备的
1、allocator的引入,STL设计者主要可能还是出于将内存管理从容器的实现独立出来的设计理念作用,让STL使用者在内存管理算法上有选择的余地。
2、“内存碎片”是技术细节,本文未讨论。
3、相对new/delete,allocator当然引起的观念变化。因为new/delete的观念,是“类”可以选择内存管理算法;allocator的观念,是“算法”可以选择内存管理算法。我认同allocator的观点。c++允许类重载new/delete,是个误导。
关于COM,我亦是持正面态度的。你介绍的的短文我看了。我很认同该文的观点。尤其是COM与.NET关系,我也有类似的提法,参考“ATL界面类——兼谈多态与泛型”。
COM引入引用计数,是非常可以理解的。本文并不想详细论述COM的方方面面,只是从内存管理角度讨论引用计数。无疑,这个引用计数的背后,需要我们付出“惨重”的代价。
引用计数则关注的是组件的生命期维护问题。换句话说,就是组件对象如何销毁的问题。对象的销毁问题,不是内存管理是什么?
不再回复了,没有意义.
: )
所谓的“傻瓜式**”,只不过是一种大众的、通俗的说法,显然并不是把用户当作傻瓜。把复杂的事情透明化,减少用户的关注点,大多数情况恰恰是对用户的尊重。当然,除了傻瓜相机,还有单反相机,用户可以在两者之间做出选择。
说起allocator,当然比类重载new/delete要高明许多。期待许先生的具垃圾回收能力的Allocator!
to fnzh: 是的,allocator比new/delete要高明多。但是,似乎我们中没有人用allocator去管理内存,这不是很值得思考吗?
同意
两者有本质的区别。
以service的思维来实现一个allocator会让事情变得复杂。
GC 式 allocator 我所能看到的最早的文档是1993 年的。因此,最迟这个技术也不会迟于 1993 即已经出现。
STL 显然是将 allocator 设计为 policy,而不是 element 的托管容器。
许先生,应该发现,加入 对象回收(不是指内存回收, 有很重要的差别) 功能的 分配器(allocator) 已经不再是一个 分配器 了。它有一个更正确的名字,叫 对象池(object pool)。
只是,许先生的实现,称为 仅分配无释放快速对象池,典型的 空间 换 时间 算法。
我觉得,还是应该将 allocator 名字回归到其应有的位置。它是一个内存分配的 policy,不应在其中实现 对象 的 所有权 语义。
Anyway, 我还是在用 许先生 的 快速对象池,因为,真的很好用。
已达N重境界,可喜可贺!
用C++也有些年头了,可以说已经到了非C++不用的执着
!
又苦于C++GUI过程的烦!因此时常想自己动手偿试一个
C++GUI RAD方案,而且也确有实作过demo.感觉如果暂
不提效率问题的话(因为效率问题我真不知如何计算和
评估).
应该可以kill掉现有的许多重量级GUI库.将许多因GUI
苦恼而弃C++的程序员重新拉回C++阵营.我确信如果没有
GUI的困扰的话,用C++写程序将会很愉快!刚好看到
lanzhengpeng2的提问,所以顺便提一下.我目前是在
MFC下实现的(有一种在人家的GUI上占空子的嫌疑),但
要在SDK下实现也应没有问题.希望看到本文的各路前
辈后进发表下看法,如果有此解决方案,你需要吗?如果
需要的人多了,我就考虑跳出来完成它(我还在为三餐
努力中^_^)
参与讨论的方法如下:
把你向往的GUI库或你需要实现的功能罗列出来,看我
的方案能否kill掉它.
联系:yxmmrwx@hotmail.com