让我们暂时回到根本原理。首先怎么会有人想要替换编译器提供的operator new或operator delete呢?下面是三个最常见的理由:
- 用来检测运用上的错误。如果将“new所得内存”delete掉却不幸失败,会导致内存泄漏(memory leaks)。如果在“new所得内存”身上多次delete则会导致不确定行为。如果operator new持有一串动态分配所得地址,而operator delete将地址从中移走,倒是很容易检测出上述错误用法。
- 为了强化效能。编译器所带的operator new和operator delete主要用于一般目的,它们不但可被长时间执行的程序(例如网页服务器,web servers)接受,也可被执行时间少于一秒的程序接受。它们必须处理一系列需求,包括大块内存、小块内存、大小混合型内存。它们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配和归还。它们必须考虑破碎问题,这最终会导致程序无法满足大区块内存要求,即使彼时有总量足够但分散为许多小区块的自由内存。
现实存在这么些个对内存管理器的要求,因此编译器所带的operator news和operator deletes采取中庸之道也就不令人惊讶了。它们的工作对每个人都是适度地好,但不对特定任何人有最佳表现。如果你对你的程序的动态内存运用型态有深刻的了解,通常可以发现,定制版之operator new和operator delete性能胜过缺省版本。说到胜过,我的意思是它们比较快,有时甚至快很多,而且它们需要的内存比较少,最高可省50%。对某些(虽然不是所有)应用程序而言,将旧有的(编译器自带的)new和delete替换为定制版本,是获得重大效能提升的办法之一。
- 为了收集使用上的统计数据。在一头栽进定制型news和定制型deletes之前,理当收集你的软件如何使用其动态内存。分配区块的大小分布如何?寿命分布如何?它们倾向于以FIFO(先进先出)次序或LIFO(后进先出)次序或随机次序来分配和归还?它们的运用形态是否随时间改变,也就是说你的软件在不同的执行阶段有不同的分配/归还形态吗?任何时刻所使用的最大动态分配量(高水位)是多少?自行定义operator new和operator delete使我们得以轻松收集到这些信息。
观念上,写一个定制型operator new十分简单。举个例子,下面是个快速发展得出的初阶段global operator new,促进并协助检测“overruns”(写入点在分配区块尾端之后)或“underruns”(写入点在分配区块起点之前)。其中还存在不少小错误,稍后我会完善它。
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); // 增加大小,使能够塞入两个signatures
void* pMem = malloc(realSize);
if (!pMem) throw bad_alloc();
// 将signature写入内存的最前段和最后段落.
*(static_cast<int*>(pMem)) = signature;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) = signature;
// 返回指针,指向恰位于第一个signature之后的内存位置.
return static_cast<Byte*>(pMem) + sizeof(int);
}
这个operator new的缺点主要在于它疏忽了身为这个特殊函数所应该具备的“坚持C++规矩”的态度。举个例子,条款51说所有operator news都应该内含一个循环,反复调用某个new-handling函数,这里却没有。由于条款51就是专门为此协议而写,所有这儿我暂且忽略之。我现在只想专注于一个比较微妙的主题:齐位。
许多计算机体系结构要求特定的类型必须放在特定的内存地址上。例如它可能会要求指针的地址必须是4的倍数或doubles的地址必须是8的倍数。如果没有奉行这个约束条件,可能导致运行期硬件异常。有些体系结构比较慈悲,没有那么霹雳,而是宣称如果齐位条件获得满足,便提供较佳效率。例如Intel x86体系结构上的doubles可被对齐于任何byte边界,但如果它是8-byte齐位,其访问速度会快许多。
在我们目前这个主题中,齐位意义重大,因为C++要求所有operator news返回的指针都有适当的对齐。malloc就是这样的要求下工作,所以令operator new返回一个得自malloc的指针是安全的。然而上述operator new中我并未返回一个得自malloc的指针,而是返回一个得自malloc且偏移一个int大小的指针。没人能够保证它的安全!如果客户端调用operator new企图获取足够给一个double所用的内存(或如果我们写个operator new[],元素类型是doubles),而我们在一部“ints为4bytes且double必须8-byte齐位”的机器上跑,我们可能会获得一个未有适当齐位的指针。那可能会造成程序崩溃或执行速度变慢。不论哪种情况都非我们所乐见。
本条款的主题是,了解何时在“全局性的”或“class专属的”基础上合理替换缺省的new和delete。挖掘更多细节之前,让我先对答案做一些摘要。
- 为了检测运用错误(如前所述)。
- 为了收集动态内存分配之使用统计信息(如前所述)。
- 为了增加分配和归还的速度。泛用型分配器往往(虽然并不总是)比定制型分配器慢,特别是当定制型分配器专门针对某特定类型之对象而设计时。
- 为了降低缺省内存管理器带来的空间额外开销。泛用型内存管理器往往(虽然并非总是)不只比定制型慢,它们往往还使用更多内存,那是因为它们常常在每一个分配区块身上招引某些额外开销。针对小型对象而开发的分配器本质上消除了这样的额外开销。
- 为了弥补缺省分配器中的非最佳齐位。一如先前所说,在x86体系结构上doubles的访问最是快速——如果它们都是8-byte齐位。但是编译器自带的operator news并不保证对动态分配而得的doubles采取8-byte齐位。这种情况下,将缺省的operator new替换为一个8-byte齐位保证版,可导致程序效率大幅提升。
- 为了将相关对象成簇集中。如果你知道特定之某个数据结构往往被一起使用,而你又希望在处理这些数据时将“内存页错误”的频率降至最低,那么为此数据结构创建另一个heap就有意义,这么一来它们就可以被成簇集中在尽可能少的内存页上。new和delete的“placement版本”(见条款52)有可能完成这样的集簇行为。
- 为了获得非传统的行为。有时候你会希望operators new和delete做编译器附带版没做的某些事情。例如你可能会希望分配和归还共享内存内的区块,但唯一能够管理该内存的只有C API函数,那么写下一个定制版new和delete,你便得以为C API穿上一件C++外套。你也可以写一个自定的operator delete,在其中将所有归还的内存内容覆盖为0,籍此增加应用程序的数据安全性。
请记住
- 有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。