50:了解new和delete的合理替换时机

为什么会有人想要替换编译期提供的operator new或operator delete?

下面是三个最常见的理由:

1.用来检测运用上的错误。

若将“new所得内存”delete掉却不幸失败,会导致内存泄漏。若在“new所得内存”身上多次delete则会导致不明确行为。若operator new持有一串动态分配所得地址,而operator delete将地址从中移走,倒是很容易检测出上述错误用法。此外各式各样的编程错误可能导致数据"overrun"(写入点在分配区块尾端之后)或"underrun"(写入点在分配区块之前)。若我们自行定义一个operator new,便可超额分配内存,以额外空间(位于客户所得区块之前或后)放置特定的byte pattern(即签名)。operator delete便得以检查上述签名是否原封不动,若否则表示在分配区的某个生命时间点发生了overrun或underrun,这时候operator delete可以志记那个事实以及那个惹是生非的指针。

2.为了强化效能。

编译器所带的operator new和operator delete主要用于一般目的,它们不但可被长时间执行的程序(例如网页服务器)接受,也可被执行时间少于一秒的程序接受。它们必须处理一系列需求,包括大块内存、小块内存、大小混合型内存。它们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配和归还。它们必须考虑破碎问题,这最终会导致程序无法满足大区块内存要求,即使彼时有总量足够但分散为许多小区块的自由内存。

3.为了收集使用上的统计数据。

在一头栽进定制型new和定制型delete之前,理当先收集你的软件如何使用其动态内存。分配区块的大小分布如何?寿命分布如何?它们倾向于以FIFO(先进先出)次序或LIFO(后进先出)次序或随机次序来分配或归还?它们的运用型是否随时间改变,也就是说你的软件在不同的执行阶段有不同的分配/归还形态吗?任何时刻所使用的最大动态分配量是多少?自行定义opeator new和operator delete使我们得以轻松收集到这些信息。

观念上,写一个定制型operator new十分简单。例如,下面是个快速发展得出的初阶段global operator new,促进并协助检测”overrun"或"underrun"。其中还存在不少小错误:

static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;
//这段代码还有若干小错误
void* operator new(std::size_t size) throw(std::bad_alloc)
{
    using namespace std;
    //增加大小,使能够塞入两个signature
    size_t realSize = size + 2 * sizeof(int);
    //调用malloc取得内存
    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++规矩”的态度。

许多计算机体系结构要求特定的类型必须放在特定的内存地址上。例如它可能会要求指针的地址必须是4倍数或double的地址必须是8倍数。若没有奉行这个约束条件,可能导致运行期硬件异常。有些体系结构比较慈悲,没有那么霹雳,而是宣称若齐位条件获得满足,便提供较佳效率。例如Intel x86体系结构上的double可被对齐于任何byte边界,但若它是8-byte齐位,其访问速度会快许多。

本篇文章的主题是,了解何时可在“全局性的”或“class专属的”基础上合理替换缺省的new和delete。

答案如下:

1.为了检测运用错误(如前所述)。

2.为了收集动态分配内存的使用统计信息(如前所述)。

3.为了增加分配和归还的速度。

泛用型分配器往往比定制型分配器慢,特别是当定制型分配器专门针对某特定类型的对象而设计时。class专属分配器是“区块尺寸固定”的分配器实例,例如Boost提供的Pool程序库便是。

若你的程序是个单线程程序,但你的编译器所带的内存管理器具备线程安全,你或许可以写个不具线程安全的分配器而大幅改善速度。

4.为了降低缺省内存管理器带来的空间额外开销。

泛用型内存管理器往往不只比定制型慢,它们往往还使用更多内存,那是因为它们常常在每一个分配区块身上招引某些额外开销。针对小型对象而开发的分配器(例如Boost的Pool程序库)本质上消除了这样的额外开销。

5.为了弥补缺省分配器中的非最佳齐位。

一如先前所说,在x86体系结构上double的访问最快速,若它们都是8-byte齐位。但编译器自带的operator new并不保证对内存分配而得的double采取8-byte齐位。这种情况下,将缺省的operator new替换为一个8-byte齐位保证版,可导致程序效率大幅提升。

6.为了将相关对象成簇集中。

若你知道特定的某个数据结构往往被一起使用,而你又希望在处理这些数据时将“内存页错误”的频率降至最低,那么为此数据结构创建另一个heap就有意义,这么一来,它们就可以被成簇集中在尽可能少的内存页上。new和delete的“placement版本”有可能完成这样的集簇行为。

7.为了获得非传统的行为。

有时你会希望operator new和delete做编译器附带版没做的某些事情。例如,你可能会希望分配和归还共享内存的区块,但唯一能够管理该内存的只有C API函数,那么写下一个定制版new和delete(很可能是placement版本),你便得以为C API穿上一件C++外套。你也可以写一个自定的operator delete,在其中将所有归还内容覆盖为0,藉此增加应用程序的数据安全性。

总结

有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行测试、收集heap使用信息。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值