1. 自定义new和delete的三个常见原因

我们先回顾一下基本原理。为什么人们一开始就想去替换编译器提供的operator new和operator delete版本?有三个最常见的原因:

  • 为了检测内存使用错误。不能成功delete new出来的内存会造成内存泄漏。在new出来的内存上使用多于一次的delete会产生未定义行为。如果operator new持有一份内存分配的列表,并且operator delete从列表中移除地址,那么就很容易侦测出这种使用错误。类似的,不同种类的编程错误能够导致数据右越界(overrun)(越过分配内存块的结尾写数据)或者左越界(underrun)(在分配内存块的开始之前写数据)。自定义的operator new能够分配额外的内存块,所以在客户申请内存前后就有空间存放已知的字节模式(“签名signatures”)。Operato delete能够检查签名是否发生了变化,如果变了,那么在分配内存块的生命周期中,越界(overrun orunderrun)就有可能会发生,operator delete会记录这个事实,并且将违规指针的值记录下来。
  • 为了提高效率。编译器提供的operator new和operator delete的版本是供大众使用的。它们必须能被长时间运行的程序所用(例如 web server),也能被执行时间小于1秒的程序所使用。它们必须要处理对大内存块,小内存块以及大小混合内存块的请求。它们必须要适应不同的内存分配模式,从为持续运行的程序提供内存块的动态分配到为大量短暂存在对象提供的常量大小的内存块分配和释放。它们必须考虑内存碎片问题,如果不做内存碎片的检查,最后有可能发生内存充足却因为分布在不同的小内存块中而导致内存请求失败的问题。

考虑以上在内存管理上的不同要求,编译器版本的operator new和operator delete为你提供一个大众化内存分配策略就不足为奇了。它们能够为每个人都工作  的很好,但是对于这些人来说都不是最优的。如果你对程序的动态内存运用模式有一个很好的理解,你就会发现使用自定义版本的operator new和operator delete会胜过默认版本。“胜过”的意思就是它们运行的更快——有时速度提升是数量级的,它们使用的内存会更少——最高能减少50%的内存。对于一些应用来说,能够很容易的替换默认operator new和operator delete版本,却能够收获很大的性能提升。

  • 为了收集内存使用的统计信息。在沿着自定义new和delete这条小路前进之前,对你的软件是如何使用动态分配内存的相关信息进行收集是很精明的。内存分配块的大小是如何分布的?内存块的生命周期是如何分布的?内存的分配和释放是使用FIFO(先进先出)的顺序,还是使用LIFO(后进先出)的顺序?或者有时候更加趋近于随机的顺序?内存使用的模式是不时地发生变化的么?例如,你的软件在不同的执行阶段是不是有不同的内存分配和释放模式?一次能够使用的动态分配内存的最大容量是多少?自定义版本的operator new和operator delete使得收集这些信息变得容易。

2. 自定义operator new中的对齐问题

从概念上来说,实现一个自定义operator new是非常容易的。例如,我们快速的实现一个全局operator new,它能够很容易的检测内存越界。它也有很多小的错误,但是我们一会再去为它们担心。

 1 static const int signature = 0xDEADBEEF;
 2
 3 typedef unsigned char Byte;
 4 // this code has several flaws — see below
 5 void* operator new(std::size_t size) throw(std::bad_alloc)
 6 {
 7
 8 using namespace std;
 9 size_t realSize = size + 2 * sizeof(int);             // increase size of request so 2
10 // signatures will also fit inside
11
12 void *pMem = malloc(realSize);   // call malloc to get the actual
13
14
15
16 if (!pMem) throw bad_alloc();                                                          // memory
17
18 // write signature into first and last parts of the memory
19
20 *(static_cast<int*>(pMem)) = signature;
21
22 *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) =
23
24 signature;
25
26 // return a pointer to the memory just past the first signature
27
28 return static_cast<Byte*>(pMem) + sizeof(int);
29
30 }      

这个operator new的大多数毛病是因为它不符合C++惯例。例如,Item 51中解释了所有的operator new都应该包含一个反复调用new-handling函数的循环,但是这个函数里没有。然而,因为在Item51中会有解释,在这里我们将其忽略。我现在想关注一个更加微妙的问题:对齐(alignment

对于许多计算机架构来说,在内存中替换特定类型的数据时,需要在特定种类的地址上进行。例如,一种架构可能需要指针定义的开始地址为4的整数倍(也就是4字节对齐的)或者定义double的开始地址必须为8的整数倍(也就是8字节对齐的)。不遵守这个约束条件在运行时就会导致硬件异常。其他架构可能更加宽松,也即是如果满足对齐会有更好的性能。例如,在英特尔X86架构中double可以被对齐在任何字节边界上,但是如果它们是8字节对齐的,访问它们的速度会大大加快。

Operator new和对齐(alignment)是相关的,因为C++需要所有operator new返回的指针都能够被恰当的对齐,malloc工作在同样的需求下,所以让operator new返回从malloc得到的指针是安全的。然而,在上面的operator new中,我们没有返回从malloc得到的指针,我们返回的是从malloc得到的指针加上int大小的偏移量。这就在安全上没有保证了!如果客户通过调用operator new来为double获取足够的内存(或者如果我们实现了operator new[],为double数组申请内存),并且我们工作在int为4字节大小但是double需要8字节对齐的机器上,我们可能返回一个没有恰当的对齐的指针。这可能会导致程序崩溃。或者它只会导致程序运行更加缓慢。不管哪种结果,都不是我们想要的。

3. 通常情况你无需自定义new和delete

因为像对齐(alignment)这样的细节问题的存在,程序员在专心完成其他任务的时候将这些细节问题忽略会导致各种问题的抛出,这就能够将专业级别的内存管理器区分出来。实现一个能够工作的内存管理器是非常容易的。实现一个工作良好的就非常难了。作为通用规则,我建议你不要尝试,除非有必要。

3.1 使用默认版本和商业产品

在许多情况下,你不必这么做。在一些编译器的内存管理函数中有控制调试和记录日志功能的开关。快速瞥一眼你的编译器文档可能就能消除你自己来实现New和delete的想法。在许多平台中,商业产品能够替换编译器自带的内存管理函数。它们的增强的功能和改善的性能能够使你受益,你所需要做的就是重新链接(前提是你必须买下这个产品。)

3.2 使用开源内存管理器

另外一个选择是开源的内存管理器。在许多平台上都能找到这样的管理器,所以你可以下载和尝试。其中一个开源的内存分配器是来自Boost的Pool库(Item 55)。这个Pool库提供的内存分配器对自定义内存管理很有帮助:也就是在有大量的小对象需要分配的时候。许多C++书籍中,包含本书的早期版本,展示出了高性能小对象内存分配器的源码,但他们通常都会忽略一些细节,像可移植性,对于对齐的考虑,线程安全等等。真正的库提供的源码都是更加健壮的。即使你自己决定去实现你自己的new和delete,看一下这些开源的版本能够让你对容易忽略的细节有了深刻洞察力,而这些细节就将“基本工作”和“真正工作”区分开来。(鉴于对齐是这样一个细节,因此注意一下TR1是很有价值的,其中包含了对特定类型对齐的支持。)

4. 使用自定义版本new和delete的意义总结

这个条款的论题是让你知道在什么情况下对默认版本的new和delete进行替换是有意义的,无论是在全局范围内替换还是在类的范围内替换。我们现在做一个总结。

  • 检测内存使用错误。
  • 收集使用动态分配内存的统计信息。
  • 提高内存分配和释放的速度。为大众提供的分配器通常情况下比自定义版本要慢的多,特别是在自定义版本是专门为特定类型对象所设计的情况下。类特定的分配器是固定大小分配器的一个实例应用,例如在Boost的Pool库中提供的分配器。如果你的应用是单线程的,但是你的编译器默认版本是线程安全的,你可以通过实现线程不安全的分配器来获得可观的速度提升。当然,在下决定要提升operator new和operator delete的速度之前,研究一下你的程序来确定这些函数真的是瓶颈所在。
  • 减少默认内存管理的空间开销。大众内存管理器通常情况下不仅慢,而且使用更多的内存。因为它们会为每个内存分配块引入一些额外的开销。为小对象创建的分配器从根本上消除了这些开销。
  • 能够补偿在默认分配器中的次优对齐。正如我先前提到的,在X86架构的机器上访问double,在8字节对齐的情况下速度是最快的。但是一些编译器中的operator new不能够保证对于动态分配的double是8字节对齐的。在这种情况中,用能够保证8字节对齐的版本替换默认版本可以很大程度的提高性能。
  • 将相关对象集中起来。如果你知道一些特定的数据结构通常情况下会被放在一起被使用,当在这些数据上进行工作时你想让页错误出现的频率最小化,为这些数据结构创建一个单独的堆就有意义了,这样它们就能够聚集在尽可能少的页中。替换new和delete的默认版本可以达到这种聚集。
  • 可以获得非常规的行为。有时候你想让operator new和delete能够做一些编译器版本不能做的事。例如,你可能想在共享内存中进行内存分配和释放,但是你只有一个C API来进行内存管理。实现自定义版本的new 和delete(可能是placement 版本——见Item 52)允许你为C API穿上C++的外衣。另外一个例子,你可以自己实现一个operator delete来为释放的内存填充数据0以达到增强应用数据安全性的目的。

5. 本条款总结

有许多正当的理由来自定义new 和delete,包括提高性能,调试堆应用错误和收集堆使用信息。

作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

读书笔记 effective c++ Item 50 了解何时替换new和delete 是有意义的相关推荐

  1. 读书笔记 effective c++ Item 16 成对使用new和delete时要用相同的形式

    1. 一个错误释放内存的例子 下面的场景会有什么错? 1 std::string *stringArray = new std::string[100]; 2 3 ... 4 5 delete str ...

  2. 读书笔记 effective c++ Item 47 使用traits class表示类型信息

    STL主要由为容器,迭代器和算法创建的模板组成,但是也有一些功能模板.其中之一叫做advance.Advance将一个指定的迭代器移动指定的距离: 1 template<typename Ite ...

  3. 读书笔记 effective c++ Item 49 理解new-handler的行为

    1. new-handler介绍 当操作符new不能满足内存分配请求的时候,它就会抛出异常.很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做.你仍然会看到这种旧行为,但是我会把关于它的讨 ...

  4. 读书笔记 effective c++ Item 18 使接口容易被正确使用,不容易被误用

    1. 什么样的接口才是好的接口 C++中充斥着接口:函数接口,类接口,模板接口.每个接口都是客户同你的代码进行交互的一种方法.假设你正在面对的是一些"讲道理"的人员,这些客户尝试把 ...

  5. 读书笔记 effective c++ Item 41 理解隐式接口和编译期多态

    1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), 1 class Widget { 2 public: 3 Widget(); 4 vi ...

  6. 读书笔记 effective c++ Item 34 区分接口继承和实现继承

    看上去最为简单的(public)继承的概念由两个单独部分组成:函数接口的继承和函数模板继承.这两种继承之间的区别同本书介绍部分讨论的函数声明和函数定义之间的区别完全对应. 1. 类函数的三种实现 作为 ...

  7. 读书笔记 effective c++ Item 30 理解内联的里里外外 (大师入场啦)

    正文 最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热 ...

  8. 读书笔记 effective c++ Item 5 了解c++默认生成并调用的函数

    1 编译器会默认生成哪些函数  什么时候空类不再是一个空类?答案是用c++处理的空类.如果你自己不声明,编译器会为你声明它们自己版本的拷贝构造函数,拷贝赋值运算符和析构函数,如果你一个构造函数都没有声 ...

  9. [读书笔记]Effective C++ - Scott Meyers

    [读书笔记]Effective C++ - Scott Meyers 条款01:视C++为一个语言联邦 C++四个次语言: 1. C Part-of-C++,没有模板.异常.重载. 2. Object ...

最新文章

  1. pyaudio与优美的声音
  2. 编程开发之--单例模式(6)单元测试
  3. Fiori Elements value help的工作原理
  4. asp.net中条件查询+分页
  5. 第三方登录android代码,Android Learning:微信第三方登录(示例代码)
  6. php table转json,html table表数据转Json格式示例代码分析
  7. 矩形窗函数的频谱图_读书笔记6-单频矩形脉冲信号
  8. IntelliJ IDEA2017 java连接mysql数据库并查询数据
  9. 【汇编优化】之MIPS架构优化
  10. iphone--使用NSUserDefaults存储数据
  11. windows 2012 抓明文密码方法
  12. 测试手机屏幕颜色软件,【AVW分享】一款你可以拥有的手机屏幕测试app
  13. 软件测试书籍有哪些_软件测试书籍推荐
  14. ATX电源工作原理的学习
  15. IDEA 出现编译错误 Multi-catches are not supported a this language level 解决方法
  16. MT7628路由器工作原理,MT7628处理器相关参数介绍
  17. 计算机论文结束语致谢,致谢范文(15篇) 毕业论文结尾致谢范文
  18. 网上看到的一个好文章,自勉
  19. 青少年CTF_misc部分题解
  20. Spring——》谈谈你对Spring框架的理解

热门文章

  1. Linux下实现多网卡绑定
  2. 消息总线(Spring Cloud Bus)
  3. Python3 定时访问网页
  4. 前台特效(6) 折叠栏目(动画效果)
  5. 获取app当前可用的剩余内存
  6. 中小企业信息化--网页设计模拟题1
  7. [JDBC技术]3.JDBC数据库连接池实例
  8. JavaScript的方法和技巧
  9. 互联网协议 — D-IP(确定性的)网络架构
  10. emwin生成c文件格式的汉字库,GB2312编码,模拟器可用