为什么要替换默认的new和delete运算符?

Alo*_*ave 64 c++ operator-overloading c++-faq new-operator delete-operator

为什么一个会替换默认的操作newdelete使用自定义newdelete运营商?

这是继续重载新的和删除在非常有启发性的C++ FAQ:
运算符重载.

本FAQ的后续条目是:
我应该如何编写符合ISO C++标准的自定义newdelete运算符?

注意:答案基于Scott Meyers的"更有效的C++"课程.
(注意:这是Stack Overflow的C++常见问题解答的一个条目.如果你想批评在这种形式下提供常见问题解答的想法,那么发布所有这些的元数据的发布将是这样做的地方.这个问题在C++聊天室中受到监控,其中FAQ的想法一开始就出现了,所以你的答案很可能被那些提出想法的人阅读.)

Alo*_*ave 66

人们可能会出于多种原因尝试更换newdelete运营商,即:

检测使用错误:

有许多方法可能会错误地使用newdelete可能导致未定义的行为内存泄漏的可怕野兽.每个各自的实例:
使用一个以上的deletenew编存储器&不调用delete使用分配的内存new.
重载运算符new可以保留已分配地址的列表,并且重载运算符delete可以从列表中删除地址,然后很容易检测到这样的使用错误.

类似地,各种编程错误可能导致数据溢出(写入超出分配块的末尾)和欠载(在分配块开始之前写入).
重载运算符new可以在内存可用于客户端之前和之后过度分配块并放置已知字节模式("签名").重载的运算符删除可以检查签名是否仍然完好无损.因此,通过检查这些签名是否不完整,可以确定在分配的块的生命周期中某个时间发生了溢出或欠载,并且操作员删除可以记录该事实以及违规指针的值,从而帮助提供良好的诊断信息.


提高效率(速度和记忆):

newdelete为大家运营商合作得相当好,但最佳的人.这种行为源于它们仅为通用目的而设计的事实.它们必须适应分配模式,范围从在程序持续时间内存在的几个块的动态分配到大量短期对象的常量分配和释放.最终,运行编译器的运营商new和运营商delete采用中间路线策略.

如果您对程序的动态内存使用模式有很好的理解,通常可以发现operator new和operator delete的自定义版本比默认版本更高(性能更快,或者需要更少的内存,最高可达50%).当然,除非你确定自己在做什么,否则这样做并不是一个好主意(如果你不理解所涉及的复杂性,甚至不要试试这个).


收集使用统计信息:

在考虑如#2中提到的替换newdelete提高效率之前,您应该收集有关您的应用程序/程序如何使用动态分配的信息.您可能希望收集以下信息:
分配块的
分布,生命周期的分配,分配
顺序(FIFO或LIFO或随机),
了解一段时间内的使用模式更改,使用的最大动态内存量等.

此外,有时您可能需要收集使用信息,例如:
计算类的动态对象
的数量,使用动态分配限制正在创建的对象的数量等.

总之,这些信息可以通过替换自定义newdelete在重载new和中添加诊断收集机制来收集delete.


为了补偿次优的内存对齐new:

许多计算机体系结构要求将特定类型的数据放置在特定种类地址的存储器中.例如,架构可能要求指针出现在四倍的地址处(即,四字节对齐),或者双倍必须出现在八倍的地址处(即,八字节对齐).不遵守此类约束可能会导致运行时出现硬件异常.其他体系结构更宽容,并且可能允许它在降低性能的情况下工作.new随某些编译器一起提供的运算符不能保证双字节动态分配的八字节对齐.在这种情况下,将默认运算符替换为new保证八字节对齐的运算符可以大大提高程序性能,并且可以成为替换newdelete运算符的一个很好的理由.


要将相关对象聚集在一起:

如果您知道特定的数据结构通常一起使用,并且您希望在处理数据时最大限度地减少页面错误的频率,那么为数据结构创建一个单独的堆就可以了,因此它们可以在很少的情况下聚集在一起页面尽可能.自定义Placement版本newdelete可以实现此类聚类.


获得非常规行为:

有时您希望运算符new和delete执行编译器提供的版本不提供的操作.
例如:您可以编写一个自定义运算符delete,用零覆盖释放的内存,以提高应用程序数据的安全性.

  • 我并不反对常见问题解答,但如果您打算提供常见问题,请注意*正确*,并能够支持您的声明.*带有一些编译器的运算符new不能保证动态分配双精度的八字节对齐.*哪个?关于`operator new`的C++标准§18.4.1.1/ 1声明它是*由new-expression(5.3.4)调用的分配函数(3.7.3.1),用于分配适当对齐的大小字节字节以表示任何对象那个大小.* (15认同)
  • 另一个是关于效率,而一些文章声称手工分配器的性能可能优于默认分配器,还有其他研究声称不然.告诉人们他们将创建比你的系统提供的更快的分配功能,好吧,我们大多数人都不会比现有的更好,并且可能会引入错误. (7认同)
  • 这些原因中没有多少对于替换(全局)默认运算符有效,但可能使用运算符的某些特定于类的重载.假设我们一般都可以做得比实施好一点,我不知道...... (4认同)
  • @DavidRodríguez - dribeas:感谢您的评论,我想出了基于我的学习的答案,我甚至不会声称它是核心的最佳或正确的,这就是C++ Lounge中人们的理由,Luc Danton具体是审查相同(现在),并将出现一个审查点列表,我们将在这里编辑或作为单独的答案添加. (2认同)

Jer*_*fin 12

首先,真的有许多不同的newdelete运营商(任意数,真的).

首先,有::operator new,::operator new[],::operator delete::operator delete[].其次,对于任何一类X,有X::operator new,X::operator new[],X::operator deleteX::operator delete[].

在这些之间,重载特定于类的运算符比使用全局运算符更常见 - 特定类的内存使用遵循特定的足够模式是相当常见的,您可以编写运算符来提供对默认值的实质​​性改进.通常,在全球范围内准确或特别地预测内存使用情况要困难得多.

可能还值得一提的是,虽然operator new并且operator new[]彼此是分开的(同样适用于任何X::operator newX::operator new[]),但两者的要求之间没有区别.将调用一个对象来分配一个对象,另一个用于分配对象数组,但每个对象仍然只需要一定量的内存,并且需要返回一个内存块(至少)那么大的内存地址.

说到需求,可能值得回顾其他要求1:全局运算符必须是真正的全局 - 您可能不会将其置于命名空间内在特定的转换单元中使其静态.换句话说,只有两个级别可以发生重载:类特定的重载或全局重载.不允许在诸如"命名空间X中的所有类"或"翻译单元Y中的所有分配"之间的中间点.特定于类的运算符必须是static- 但实际上并不需要将它们声明为静态 - 无论是否明确声明它们,它们都将是静态的static.正式地说,全局运算符多次返回内存对齐,以便它可以用于任何类型的对象.非正式地,在一个方面有一点摆动空间:如果你得到一个小块的请求(例如,2个字节),你只需要提供一个对齐那个大小的对象的内存,因为试图在那里存储更大的东西无论如何都会导致不确定的行为.

在完成了这些预备之后,让我们回到原来的问题,为什么你想要超载这些运算符.首先,我应该指出,全局运营商超载的原因往往与特定类运营商超载的原因大不相同.

由于它更常见,我将首先讨论特定于类的运算符.特定于类的内存管理的主要原因是性能.这通常有两种形式(或两种形式):提高速度或减少碎片.由于内存管理器处理特定大小的块,因此它可以返回任何空闲块的地址,而不是花时间检查块是否足够大,如果它是块,则将块分成两块,这样可以提高速度. (大部分)以相同的方式减少碎片 - 例如,为N个对象预先分配足够大的块,可以精确地提供N个对象所需的空间; 分配一个对象的内存值将为一个对象准确分配空间,而不是单个字节.

超载全局内存管理运营商的原因有很多种.其中许多都面向调试或检测,例如跟踪应用程序所需的总内存(例如,准备移植到嵌入式系统),或通过显示分配和释放内存之间的不匹配来调试内存问题.另一种常见策略是在每个请求块的边界之前和之后分配额外的内存,并将独特的模式写入这些区域.在执行结束时(也可能在其他时间),检查这些区域以查看代码是否已在分配的边界之外写入.另一种方法是尝试通过自动化存储器分配或删除的至少一些方面来改进易用性,例如使用自动垃圾收集器.

非默认全局分配器可用于提高性能.典型的情况是替换一般速度很慢的默认分配器(例如,至少某些版本的MS VC++在4.x附近会调用系统HeapAllocHeapFree每个分配/删除操作起作用).我在实践中看到的另一种可能性是在使用SSE操作时在Intel处理器上发生.它们以128位数据运行.虽然操作无论对齐如何都可以工作,但当数据与128位边界对齐时,速度会提高.一些编译器(例如,MS VC++再次2)不一定强制对齐到更大的边界,因此即使使用默认分配器的代码可以工作,替换分配也可以为这些操作提供显着的速度提升.


  1. 大多数要求都包含在C++标准的§3.7.3和§18.4中(或者C++ 0x中的§3.7.4和§18.6,至少从N3291开始).
  2. 我不得不指出,我不打算选择微软的编译器 - 我怀疑它有不寻常的数量这样的问题,但我碰巧使用它很多,所以我倾向于非常清楚它的问题.


Luc*_*ton 6

许多计算机体系结构要求将特定类型的数据放置在特定种类地址的存储器中.例如,架构可能要求指针出现在四倍的地址处(即,四字节对齐),或者双倍必须出现在八倍的地址处(即,八字节对齐).不遵守此类约束可能会导致运行时出现硬件异常.其他架构更宽容,并且可能允许它在降低性能的情况下工作.

为了澄清:如果一个结构要求的实例double数据是8字节对齐,那么就没有什么优化.任何种类的适当大小的动态分配的(例如malloc(size),operator new(size),operator new[](size),new char[size]其中size >= sizeof(double))是保证适当对准.如果实现没有做出这种保证,那就不符合要求.operator new在这种情况下改变做"正确的事"将是"修复"实施,而不是优化.

另一方面,一些体系结构允许一种或多种数据类型的不同(或所有)对齐类型,但根据这些相同类型的对齐提供不同的性能保证.然后,实现可以返回存储器(再次,假设具有适当大小的请求),该存储器被次优地对齐,并且仍然是符合的.这就是这个例子的意思.


lea*_*der 6

似乎值得从“任何原因使全局新建和删除超载?”的回答中重复该列表在这里 -有关更详细的讨论、参考资料和其他原因,请参阅该答案(或该问题的其他答案)。这些原因通常适用于当地的运算符重载以及默认/全球的,同时也到C malloc/ calloc/ realloc/free过载或钩为好。

出于多种原因,我们重载了我工作的全局 new 和 delete 运算符:

  • 汇集所有小分配——减少开销,减少碎片,可以提高小分配重应用的性能
  • 框架具有已知生命周期的分配 - 忽略所有释放直到此期间结束,然后将所有释放一起释放(诚然,我们使用本地运算符重载而不是全局运算符更多地执行此操作)
  • 对齐调整——缓存线边界等
  • alloc fill -- 帮助暴露未初始化变量的使用
  • 自由填充——帮助暴露以前删除的内存的使用情况
  • 延迟免费——增加免费填充的有效性,偶尔提高性能
  • 哨兵围栏——帮助暴露缓冲区溢出、不足以及偶尔出现的野指针
  • 重定向分配——考虑 NUMA、特殊内存区域,甚至在内存中保持独立的系统(例如嵌入式脚本语言或 DSL)
  • 垃圾收集或清理——同样对那些嵌入式脚本语言很有用
  • 堆验证——您可以每 N 次分配/释放检查堆数据结构,以确保一切正常
  • 会计,包括泄漏跟踪使用快照/统计(堆栈、分配年龄等)