说实话,以前在程序优化方面,我更多关注的是汇编和算法这两方面的优化,但是有时候在这两块已经难以下手的时候,却依然感到还不够,速度还是不够。
去年在家仔细阅读了一些有关内存优化的书籍,不过那都是讲低层次的优化,比如缓存命中,循环展开。这些技术过于注重细节,并不适合从大局来设计一个产品。前一段时间看到IBM的一篇文章:内存池,突然发现自己原来忽视了这么重要的一块战略要地。于是毫不犹豫地把《C++应用程序性能优化》买来仔细研读。
很多人对C++有偏见,其中最大的诟病就是对象实例构造带来的隐含性能损失,以及程序中超大量细小对象的频繁构造,这两个问题在书中都有了一定程度的解释和解决办法。
对于构造的隐含性能损失,建议就是:尽量把成员变量的初始化放在构造函数的初始化列表中:
class test
{
public:
int a;
test(int i)
:a(i)
{
}
}
如果把显式初始化放在构造函数体中,那么相当于初始花了两次,这可是一个不能接受的性能损失。
对于程序中超大量细小对象的频繁构造,建议就是内存池。这是一种什么样的情况下会遇到的问题呢?举例来说,3D CAD这类程序中,假设我们用一个CVertex类来表示一个顶点,里面保存x,y,z坐标,那么当我们操作一个3D实体的时候,可能会遇到上百万的CVertex实例。当然有人会说,为什么非要用面向对象的封装?C++不适合。但是如果我们深入了解一下C++的对象模型,会发现在空间占用上,C++封装并不一定会有额外的开销,而面向对象的设计所带来的程序清晰性和可维护性是我们不得不考虑的。我们唯一要面对的就是new和delete的性能打击,如果注意到前面的初始化列表的问题,那么剩下的就是内存管理了。
CRT中内存管理器是一种通用的设计,如果用这个管理器直接操作上百万的细粒度内存分配释放,短期内也许不会有什么感觉,但是长时间运行后,内存碎片之类的问题会严重降低应用程序的性能。针对以上种种,用自定义内存池是上上之选。
下面是我做的一个简单测试:
测试是针对1000000个对象的new和delete进行的,从测试结果来看,内存池对new有一个数量级的性能提升,但是对delete性能提升很小,这其实和内存池的实现有关,并不是这种技术理论上的问题。事实上,如果我在内存分配中额外在每一块分配出去的小内存里面保存pool block的pointer,那么delete速度也会有一个数量级地提高,但是这就属于用空间换时间另一个优化范畴了(上百万的pointer,本身就是一个需要考虑的内存开销)。
后记:我又跟踪了一下测试程序的内存使用,用CRT的话,程序峰值内存使用大约是44MB,用内存池的话,程序峰值内存使用大约是24MB。其实想想原因也比较简单,CRT的内存管理器要维护比较复杂的数据结构来跟踪不定长内存的分配释放,这自然就是一个沉重的负担了,内存池是轻量级的管理机制,自然资源消耗少,这是内存池技术带来的“免费”优化。