读书笔记 effective c++ Item 50 了解何时替换new和delete 是有意义的
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,它能够很容易的检测内存越界。它也有很多小的错误,但是我们一会再去为它们担心。
static const int signature = 0xDEADBEEF; typedef unsigned char Byte;
// this code has several flaws — see below
void* operator new(std::size_t size) throw(std::bad_alloc)
{ using namespace std;
size_t realSize = size + * sizeof(int); // increase size of request so 2
// signatures will also fit inside void *pMem = malloc(realSize); // call malloc to get the actual if (!pMem) throw bad_alloc(); // memory // write signature into first and last parts of the memory *(static_cast<int*>(pMem)) = signature; *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) = signature; // return a pointer to the memory just past the first signature return static_cast<Byte*>(pMem) + sizeof(int); }
这个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,包括提高性能,调试堆应用错误和收集堆使用信息。
读书笔记 effective c++ Item 50 了解何时替换new和delete 是有意义的的更多相关文章
- 读书笔记 effective c++ Item 16 成对使用new和delete时要用相同的形式
1. 一个错误释放内存的例子 下面的场景会有什么错? std::]; ... delete stringArray 一切看上去都是有序的.new匹配了一个delete.但有一些地方确实是错了.程序的行 ...
- 读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库
1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C+ ...
- 读书笔记 effective c++ Item 39 明智而谨慎的使用private继承
1. private 继承介绍 Item 32表明C++把public继承当作”is-a”关系来对待.考虑一个继承体系,一个类Student public 继承自类Person,如果一个函数的成功调用 ...
- 读书笔记 effective c++ Item 51 实现new和delete的时候要遵守约定
Item 50中解释了在什么情况下你可能想实现自己版本的operator new和operator delete,但是没有解释当你实现的时候需要遵守的约定.遵守这些规则并不是很困难,但是它们其中有一些 ...
- 读书笔记 effective c++ Item 27 尽量少使用转型(casting)
C++设计的规则是用来保证使类型相关的错误不再可能出现.理论上来说,如果你的程序能够很干净的通过编译,它就不会尝试在任何对象上执行任何不安全或无意义的操作.这个保证很有价值,不要轻易放弃它. 不幸的是 ...
- 读书笔记 effective c++ Item 55 让你自己熟悉Boost
你正在寻找一个高质量的,开源的,与平台和编译器无关的程序库的集合?看一下Boost吧.想加入一个由雄心勃勃的,充满天赋的正致力于最高水平的程序库设计和实现工作的C++程序员们组成的团体么?看一下Boo ...
- 读书笔记 effective c++ Item 47 使用traits class表示类型信息
STL主要由为容器,迭代器和算法创建的模板组成,但是也有一些功能模板.其中之一叫做advance.Advance将一个指定的迭代器移动指定的距离: template<typename IterT ...
- 读书笔记 effective c++ Item 28 不要返回指向对象内部数据(internals)的句柄(handles)
假设你正在操作一个Rectangle类.每个矩形可以通过左上角的点和右下角的点来表示.为了保证一个Rectangle对象尽可能小,你可能决定不把定义矩形范围的点存储在Rectangle类中,而是把它放 ...
- 读书笔记 effective c++ Item 1 将c++视为一个语言联邦
Item 1 将c++视为一个语言联邦 如今的c++已经是一个多重泛型变成语言.支持过程化,面向对象,函数式,泛型和元编程的组合.这种强大使得c++无可匹敌,却也带来了一些问题.所有“合适的”规则看上 ...
随机推荐
- Couchbase 中的分布式储存
Couchbase 是一个具有高性能.可扩展性和可 用性强的数据库引擎.它可以让开发人员通过 NoSQL 的键值存储(二进制或者JSON)或者使用 N1QL 的形式对数据进行操作(N1QL 是非常类似 ...
- 知识管理(KM) - 数据流
快速链接: 人力资源知识体系索引 本章主要列出知识管理(KM)中涉及到的所有表. 步骤 操作 相关表 说明 1 知识管理资料 基础资料,见附表1 2 知识主题(107301) KMBlg:主题 K ...
- React+webpack开发环境的搭建
首先创建项目,确保该项目已经安装了webpack和webpack-dev-server具体安装方法请参考上章所述. 在上一章说过babel是一个javascript编辑器,在react项目中使用bab ...
- SVNManager配置
1.svn与apache的安装 yum install -y subversion httpd httpd.conf添加如下内容: LoadModule dav_svn_module module ...
- swift -- 基础
swift -- 基础 1.常量和变量 常量: let 变量: var 2.声明常量和变量 常量的声明: let let a = 1 //末尾可以不加分号,等号两边的空格必须对应(同 ...
- SEO-友情链接注意事项
为什么要专门给友链一个区域呢?由此就可以想象到友情链接对一个网站有多重要前期,网站没有权重的时候,跟别人换友链,人家基本是不会换的因为你网站没权重,加了友链他也获取不到权重,对网站没有多少好处一般我们 ...
- Metadata Service 一个最简单的应用 - 每天5分钟玩转 OpenStack(164)
实现 instance 定制化,cloud-init(或 cloudbase-init)只是故事的一半,metadata service 则是故事的的另一半.两者的分工是:metadata servi ...
- (15)IO流之File
File类用封装了一个文件夹或者文件的所有属性. File类的构造方法: File(String pathname) 指定文件或者文件夹的路径创建一个File文件 File(File parent, ...
- QQ互联 redirect uri is illegal(100010)的解决办法,很简单
我的地址栏内容是:http://openapi.qzone.qq.com/oauth/show?which=ConfirmPage&display=pc&response_type=c ...
- js中的call()、apply()和bind()方法的区别
call(thisObj,param1,param2....)方法:调用一个对象的方法,用另外的对象去替换当前对象. 下面给出一个例子: function add(a,b){ return a+b; ...