有人会想要替换掉编译器提供的operator new或operator delete,因为
- 用来检测运用上的错误。如果delete new的内存失败,会导致内存泄漏。如果在new所得内存多次delete会导致不确定行为。使用编译器提供的operator new和operator delete不能检测上述行为。如果operator new持有一个链表,其存储动态分配所得内存,operator delete则将内存从链表删除,这样就能呢检测上述错误用法。如果编程错误,可能在分配内存的之前区域或之后区域写入数据;这时可以自己定义operator new分配超额内存,在多出部分写上特定byte patterns(即签名,signature),自己定义operator delete检测签名是否更改。
- 为了强化效能。operator new和operator delete如果开辟大内存、小内存,持续这样做会造成内存碎片,这在服务器的后台程序上,可能会导致无法满足大区快内存需求,即使有足够但分散的小区块自由内存。使用自己定制的operator new和operator delete可以避免这样的问题。针对特定的需求,有时还可以提升性能。
- 为收集使用上的统计数据。在定制operator new和operator delete之前,应该首先了解软件如何使用动态内存。分配区块如何分布?寿命如何?它们是FIFO先进先出还是LIFO后进先出,或随机分配和归还?软件在不同执行阶段有不同的分配归还形态吗?任何时刻使用的最大动态分配量是多少?自己定义的operator new和operator delete可以轻松收集到这些信息。
写个定制的operator new和operator delete并不难。例如,写个global operator new,用于检测在分配区块的后面或前面写入数据。下面是个初步版本,有小错误,后面在完善。
static const int signature=0xDEADBEEF;
typedef unsigned char Byte;
//下面代码有些小错误
void* operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
size_t realSize=size+2*sizeof(int);//增加大小,塞入两个sinature
void* pMem=malloc(realSize);
if(!pMem) throw bad_alloc();
//将signarure写入内存最前后最后
*(static_cast<int*>(pMem))=signarure;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int)))=signature;
return static_cast<Byte*>(pMem)+sizeof(int);
}
暂且忽略之前所说的operator new内应该有个循环,反复调用new-handling。来说一下另外一个主题:对齐(alignment)。
许多计算机体系结构要求特定的类型必须放在特定的内存地址上。例如可能是指针的地址必须是4的倍数(four-byte aligned)或double的地址是8的倍数(eight-byte aligned)。没有这些约束可能会导致运行期硬件异常。有些体系结构要求没这么严格,没有字节对齐不会导运行效率低下。
C++要求所有operator new返回的指针都有适当的对齐(取决于数据类型)。malloc就是在这样的要求下工作。所以令operator new返回一个得自malloc的指针是安全的。但是上面实现中,我们偏移了一个int的大小,就不能保证其安全了。例如,如果返回double指针,就不是8字节对齐了。
像对齐这类技术细节,可以区分内存管理器的质量。写一个能够运行的内存管理器并不难,难的是让它总是能够高效优良的运作。一般来说,若非必要,不要去写内存管理器。
很多时候也是非必要的。有些编译器已经在它们的内存管理函数中切换至调试状态(enable debugging)和志记状态(logging)。许多平台上有商业产品可以代替编译器自带的内存管理器,可以用它们来提高机能和改善效率。
另外一个选择是开源领域中的内存管理器。它们对许多平台都可以用。Boost程序库(条款 55)的Pool就是这样的一个分配器,它对常见的分配大量小内存很有帮助。一些小型开源内存分配器大多都不完整,缺少移植、线程安全、对齐等考虑。
本条款是在探讨何时需要在全局性的活class专属的基础上合理替换掉缺省的new和delete,前面说到了3点。这里继续。
- 为了增加分配和归还的速度。使用定制的针对特定类型对象的分配器,可以提高效率。例如,Boost提供的Pool程序库便是。如果在单线程程序中,你的编译器所带的内存管理具备线程安全,你可以写个不具备线程安全的分配器而大幅度改善速度。
- 为了降低缺省内存管理器带来的空间额外开销。泛用型分配器往往(虽然并非总是)不只比定制型慢,还使用更多空间,因为它们常常在每一个分配区块上招引某些额外开销。针对小型对象开放的分配器,例如Boost库的Pool,本质上消除了这样的额外开销。
- 为了弥补缺省分配器的非最佳对齐(suboptimal alignment)。X86体系结构上的double访问最快–如果它们是8-byte对齐。但是编译器自带的operator new并不保证分配double是8-byte对齐。
- 为了将相关对象成簇集中。如果特定的某个数据结构往往被一起使用,我们希望在处理这些数据时将“内存页错误”(page faults)的频率降至最低,那么为此数据结构创建另一个heap就有意义,这样就可以将它们成簇集中到尽可能少的内存也上。
- 为了获得非传统的行为。有时候我们需要做operator new和delete没做的事。例如,在归还内存时将其数据覆盖为0,以此增加应用程序的数据安全。
总结
- 有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。