时间: 2006-11-24 来自:天极yesky
| |||||||
作者:谢启东编译 应用程序分配内存的方法,对程序的执行性能有着深刻的影响。目前,通用的内存分配方法本质上已非常高效,但仍有改进的空间。
在每一层的实现中,都有可能向它的基类请求内存,一般来说,一个不依赖于外界的内存分配算符,都会处在层次的顶层--直接向前请求系统的new和delete操作符、malloc和free函数。在HeapLayers的术语中,没有顶层堆,以下是示例:
为获取内存,顶层堆也能通过系统调用来实现,如Unix的sbrk或mmap。getSize函数的情况就比较特殊,不是每个人都需要它,定义它只是一个可选项。但如果定义了它,你所需做的只是插入一个存储内存块大小的层,并提供getSize函数,见例1: 例1:
SizeHeap是怎样实现一个实用的层,并挂钩于它基类的malloc与free函数的最好示例,它在完成一些额外的工作之后,把修改好的结果返回给使用者。SizeHeap为存储内存块大小,分配了额外的内存,再加上适当的小心调整(指union),尽可能地避免了内存数据对齐问题。不难想像,我们可构建一个debug堆,其通过特定模式在内存块之前或之后填充了一些字节,通过检查是否模式已被保留,来确认内存的溢出。事实上,这正是HeapLayers的DebugHeap层所做的,非常的简洁。 让我们再来看看,以上还不是最理想的状态,某些系统已经提供了计算已分配内存块大小的原语(此处指操作符,即前述的分配算符),在这些系统上,SizeHeap实际上只会浪费空间。在这种情况下(如Microsoft Visual C++),你将不需要SizeHeap与MallocHeap的衔接,因为MallcoHeap将会实现getSize:
但似乎还有一些不足之处。想一想,我们是在统计时钟周期,如果一个系统的malloc声明了内存的块大小将存储在实际块之前的一个字中,那将会怎样呢?在这种情况下,SizeHeap还是会浪费空间,因为它仍会在紧接着系统已植入的块后存储一个字。此处所需的,只是一个用SizeHeap的方法实现了getSize的层,但未挂钩malloc与free。这就是为什么HeapLayers把前面的SizeHeap分成了两个,见例2: 例2:
现在,SizeHeap就会正确地添加UseSizeHeap层,并利用它的getSize实现了,而UseSizeHeap也能通过其他配置来使用--这是一个非常优雅的设计。 一个实用的示例:FreelistHeap 到目前为止,我们还处于一个准备的阶段,只有架构,还不知怎样利用这些层来编写一个高效专用的内存分配算符,也许一个比较合适的开发步骤可如下所示: ·收集有关程序为每种内存块大小进行分配次数的信息。 ·为最经常请求的大小(在此称为S),维持一个私有、逐一链接的列表。 ·对S的内存分配尽可能地从列表中返回内存,或者从默认分配算符中返回(在分层架构中,从上级层中)。 ·对S大小内存块的释放,把内存块放回至列表中。 ·一个精心设计的分配策略,应可对范围大小从S1至S2,使用相同的释放列表,并消耗同等的内存。而所需链接列表的操作开销为O(1),实际上只有几条指令。另外,指向下一条目的指针,能存储在实际的块中(块中存储了无用的数据--总为一个释放了的块),因此,对每个块就不需要额外的内存了。正因为大多数应用程序分配内存的大小都是不同的,所以,对任何分配算符的实现来说,释放列表就必不可少了。 下面让我们来实现一个层,由其对已知静态范围大小从S1至S2,实现了一个释放列表,见例3: 例3:
现在,你像如下所示可定义一个自定义的堆:
SmartoHeapo在分配的大小在24至32之间时,速度相当快,对其它大小来说,也基本上一样。 |
原地重新分配(Inplace Resizing)
许多的C++程序员都梦寐以求有一种标准的原语(也即操作符),用于原地重新分配内存。众所周知,C语言中有realloc,其尽可能的原地重新分配内存,并在涉及到复制数据时使用memcpy,但memcpy并不适合于C++对象,所以,realloc也不适用于C++的对象。因此,任何一种renew原语都不能用标准C分配符来实现,这就是为什么C++中没有renew的原因。
以下演示了一种改进后的方法,可应用于C++代码中的原地重新分配,请看:
const int n = 10000; Vec v; for (int i = 0; i < n; ++i) v.push_back(0); |
Metrowerks的Howard Hinnant一直在为实现应用于CodeWarrior标准库的原地扩展而努力,用他自己的话来说:
现在有一个可进行原地重新分配的vector<T, malloc_allocator<T>>,当Vec为一个不带原地扩展的vector<int>时,耗时为0.00095674秒;当Vec为一个带有原地扩展的vector<int>时,耗时为0.000416943。由此可看出,内存的原地重新分配,所带来的性能提升,非常之明显。
既然有了原地重新分配所带来的好处,而堆中的每个层都能控制其自己的分配算法和数据结构,请看下面的堆层接口:
template <class T> struct Allocator : public T { void * malloc(size_t sz); void free(void* p); size_t expand(void* p, size_t min, size_t max); }; |
扩展在语义上的意思是,尝试通过p扩展指向在两者之间最大尺寸的块,并返回期望扩展的任意大小内存块。幸运的是,一个层不必关心用于扩展的子程序,如果所有顶层的分配方法都继承自以下的类,那么一切都将工作正常:
struct TopHeap { size_t expand(void*, size_t, size_t) { return 0; } protected: ~TopHeap() {} }; |
结论
可配置的内存分配算符,是一种实用的、一体化的解决方案,可取代专门或通用的内存分配操作符。此外,HeapLayers的分层架构支持更简单的调试,并且具有非并行的可扩展性。表1演示了一个在HeapLayers中,层实现的相关子集,其中有许多值得讨论的地方,如多线程操作中的闭锁堆、STL适配程序、各种不同的工具堆、还有怎样结合多个层来创建一个通用的内存分配算符,另外,千万记住不要忘了在析构函数中释放内存,祝大家编程愉快!
表1:部分HeapLayers库
顶层堆 | |
mallocHeap | 取代malloc的层 |
mmapHeap | 取代虚拟内存管理的层 |
sbrkHeap | 取代sbrk(连续内存)构建块堆的层 |
AdaptHeap | 使数据结构可作为堆使用 |
BoundedFreelistHeap | 有长度限制的释放列表 |
ChunkHeap | 以给定大小的块来管理内存 |
CoalesceHeap | 执行拼接与拆分 |
FreelistHeap | 一个释放列表(用于捕捉释放的对象) |
组合堆 | |
HybridHeap | 对小对象使用一个堆,而对大对象使用另一个堆 |
SegHeap | 用于分配方法的一般分割 |
StrictSegHeap | 用于分配方法的严格分割 |
工具层 | |
ANSIWrapper | 提供与ANSI-malloc的兼容性 |
DebugHeap | 检查多种分配错误 |
LockedHeap | 为保证线程安全的闭锁堆 |
PerClassHeap | 使用一个堆作为每个类的分配算符 |
PHOThreadHeap | 带有自有分配算符私有堆 |
ProfileHeap | 收集并输出碎片统计 |
ThreadHeap | 一个纯私有堆分配算符 |
ExceptionHeap | 当父类堆超出内存时,抛出一个异常 |
TraceHeap | 输出有关内存分配的跟踪信息 |
UniqueHeap | 引用一个堆对象的堆类型 |
对象表示 | |
CoalesceableHeap | 为拼接提供支持 |
SizeHeap | 在头部中记录对象大小 |
特殊用途的堆 | |
ObstackHeap | 专门优化用于类似堆栈行为或快速大小调整的堆 |
ZoneHeap | 一个区域分配算符 |
XallocHeap | 优化用于类似堆栈行为的堆 |
通用堆 | |
KingsleyHeap | 快速但多碎片的堆 |
LeaHeap | 速度不快,但碎片很少的堆 |