提问人:Alok Save 提问时间:8/22/2011 最后编辑:CommunityAlok Save 更新时间:8/16/2014 访问量:19472
为什么要替换默认的 new 和 delete 运算符?
Why would one replace default new and delete operators?
问:
为什么要用自定义的 new 和 delete
运算符替换默认运算符 new
和 delete
?
这是极具启发性的 C++ 常见问题解答:
运算符重载中重载 new 和 delete 的延续。
本常见问题解答的后续条目是:
我应该如何编写符合 ISO C++ 标准的自定义 new
和 delete
运算符?
答:
出于多种原因,可能会尝试替换 和 运算符,即:new
delete
要检测使用错误,请执行以下操作:
有很多方式可以错误地使用并可能导致可怕的未定义行为和内存泄漏。
每个示例分别是:
在 ed 内存上使用多个内存和不调用使用 .
重载算子可以保留分配的地址列表,重载算子可以从列表中删除地址,这样就很容易检测到此类使用错误。new
delete
delete
new
delete
new
new
delete
同样,各种编程错误都可能导致数据溢出(写入超出已分配块的末尾)和欠载(在已分配的块开始之前写入)。
重载运算符可以过度分配块,并将已知的字节模式(“签名”)放在提供给客户端的内存之前和之后。重载运算符 delete 可以检查签名是否仍然完好无损。
因此,通过检查这些签名是否不完整,可以确定在分配的块生命周期内的某个时间发生了溢出或欠运行,并且操作员删除可以记录该事实以及违规指针的值,从而有助于提供良好的诊断信息。new
提高效率(速度和内存):
和运算符对每个人都很好,但对任何人都不是最佳。这种行为源于它们仅设计用于一般用途的事实。它们必须适应各种分配模式,从在程序持续时间内存在的几个块的动态分配到大量短期对象的持续分配和解除分配。最终,编译器附带的运算符和运算符采取了折衷策略。new
delete
new
delete
如果您对程序的动态内存使用模式有很好的了解,您通常会发现 operator new 和 operator delete 的自定义版本优于默认版本(性能更快,或需要更少内存高达 50%)。当然,除非你确定自己在做什么,否则这样做不是一个好主意(如果你不了解所涉及的复杂性,甚至不要尝试这个)。
要收集使用情况统计信息,请执行以下操作:
在考虑替换和提高效率之前,如#2所述,您应该收集有关应用程序/程序如何使用动态分配的信息。您可能希望收集以下信息:
分配块的分布、生存期的分布、分配顺序(FIFO 或 LIFO 或随机
)、了解一段时间内使用模式的变化、
使用的最大动态内存量等。new
delete
此外,有时您可能需要收集使用信息,例如:
计算类的动态对象数、
使用动态分配限制正在创建的对象数等。
总而言之,可以通过替换自定义 和 并在重载和 中添加诊断收集机制来收集此信息。new
delete
new
delete
为了补偿以下项中的次优内存对齐:new
许多计算机体系结构要求将特定类型的数据放在内存中特定类型的地址。例如,体系结构可能要求指针出现在 4 的倍数(即四字节对齐)的地址上,或者双精度必须出现在 8 的倍数(即八字节对齐)的地址上。如果不遵循此类约束,可能会导致运行时出现硬件异常。其他架构更宽容,并且可能允许它通过降低性能来工作。某些编译器附带的运算符不保证动态的 8 字节对齐
双打的分配。在这种情况下,用保证八字节对齐的运算符替换默认运算符可以大大提高程序性能,并且可以成为替换和运算符的一个很好的理由。new
new
new
delete
要将相关对象彼此靠近,请执行以下操作:
如果您知道特定的数据结构通常一起使用,并且希望在处理数据时最大程度地减少页面错误的频率,那么为数据结构创建一个单独的堆,以便将它们聚集在尽可能少的页面上是有意义的。自定义 Placement 版本的 和 可以使实现这种聚类成为可能。new
delete
要获得非常规行为:
有时,您希望运算符 new 和 delete 执行编译器提供的版本不提供的功能。
例如:您可以编写一个自定义运算符,用零覆盖已分配的内存,以提高应用程序数据的安全性。delete
评论
operator new
许多计算机体系结构要求将特定类型的数据放在内存中特定类型的地址。例如,体系结构可能要求指针出现在 4 的倍数(即四字节对齐)的地址上,或者双精度必须出现在 8 的倍数(即八字节对齐)的地址上。如果不遵循此类约束,可能会导致运行时出现硬件异常。其他架构更宽容,并且可能允许它通过降低性能来工作。
澄清一下:例如,如果架构要求数据是八字节对齐的,那么就没有什么可以优化的了。任何适当大小的动态分配(例如,、、、where)都保证正确对齐。如果实现没有做出这种保证,它就不符合要求。在这种情况下,改变做“正确的事情”将是“修复”实现的尝试,而不是优化。double
malloc(size)
operator new(size)
operator new[](size)
new char[size]
size >= sizeof(double)
operator new
另一方面,某些体系结构允许对一种或多种数据类型进行不同(或所有)类型的对齐,但根据这些相同类型的对齐方式提供不同的性能保证。然后,实现可能会返回内存(同样,假设请求大小适当),该内存不是最佳对齐的,并且仍然符合要求。这就是这个例子的意义所在。
首先,确实有许多不同的 and 运算符(实际上是一个任意数字)。new
delete
首先,有 、 和 。其次,对于任何类,有 、 和 。::operator new
::operator new[]
::operator delete
::operator delete[]
X
X::operator new
X::operator new[]
X::operator delete
X::operator delete[]
在这两者之间,重载特定于类的运算符比重载全局运算符更为常见——特定类的内存使用遵循足够特定的模式是相当常见的,因此您可以编写运算符来提供对默认值的实质性改进。通常,在全球范围内准确或具体地预测内存使用情况要困难得多。
可能还值得一提的是,尽管 和 彼此是分开的(对于任何 和 也是如此),但两者的要求之间没有区别。一个将用于分配单个对象,另一个用于分配对象数组,但每个对象仍然只接收所需的内存量,并且需要返回内存块的地址(至少)那么大。operator new
operator new[]
X::operator new
X::operator new[]
说到需求,可能值得回顾一下其他需求1:全局运算符必须是真正的全局 - 您不能将一个运算符放在命名空间中,也不能将一个运算符设为静态在特定的翻译单元中。换言之,只有两个级别可以发生重载:特定于类的重载或全局重载。不允许使用“命名空间 X 中的所有类”或“翻译单元 Y 中的所有分配”等中间点。特定于类的运算符是必需的,但实际上并不需要将它们声明为静态的,无论您是否显式声明它们,它们都将是静态的。正式地,全局运算符返回的内存对齐,以便它可以用于任何类型的对象。非正式地,在一个方面有一点回旋余地:如果你收到一个小块(例如,2个字节)的请求,你只需要为一个达到该大小的对象提供对齐的内存,因为试图在那里存储任何更大的东西都会导致未定义的行为。static
static
在介绍了这些初步内容之后,让我们回到最初的问题,即为什么要重载这些运算符。首先,我应该指出,全局运算符重载的原因往往与特定类运算符超载的原因大不相同。
由于它更常见,我将首先讨论特定于类的运算符。特定于类的内存管理的主要原因是性能。这通常有两种形式(或两种形式):提高速度或减少碎片。内存管理器只处理特定大小的块,因此可以返回任何可用块的地址,而不是花费任何时间检查块是否足够大,如果块太大,则将块一分为二等,从而提高了速度。碎片化以(大部分)相同的方式减少 - 例如,为 N 个对象预先分配一个足够大的块,恰好为 N 个对象提供所需的空间;分配一个对象的内存值将恰好为一个对象分配空间,而不是多一个字节。
导致全局内存管理运算符过载的原因要多得多。其中许多是面向调试或检测的,例如跟踪应用程序所需的总内存(例如,准备移植到嵌入式系统),或者通过显示分配和释放内存之间的不匹配来调试内存问题。另一种常见的策略是在每个请求块的边界之前和之后分配额外的内存,并将独特的模式写入这些区域。在执行结束时(可能还有其他时间),将检查这些区域,以查看代码是否在分配的边界之外编写。另一种方法是尝试通过至少自动分配或删除内存的某些方面来提高易用性,例如使用自动垃圾回收器。
非默认全局分配器也可用于提高性能。一个典型的情况是替换一个默认的分配器,该分配器通常很慢(例如,至少 4.x 左右的某些 MS VC++ 版本会为每个分配/删除操作调用系统和函数)。我在实践中看到的另一种可能性是在使用 SSE 操作时发生在 Intel 处理器上。它们对 128 位数据进行操作。虽然无论对齐方式如何,操作都会起作用,但当数据对齐到 128 位边界时,速度会提高。一些编译器(例如,MS VC++ 再次2)不一定强制对齐到更大的边界,因此即使使用默认分配器的代码可以工作,替换分配也可以为这些操作提供显着的速度改进。HeapAlloc
HeapFree
- 大多数要求包含在 C++ 标准的 §3.7.3 和 §18.4 中(或 C++0x 中的 §3.7.4 和 §18.6,至少从 N3291 开始)。
- 我不得不指出,我不打算挑剔Microsoft的编译器 - 我怀疑它有不寻常的此类问题,但我碰巧经常使用它,所以我倾向于非常了解它的问题。
与使用情况统计信息相关:按子系统编制预算。例如,在基于主机的游戏中,您可能希望为 3D 模型几何图形保留一些内存,一些用于纹理,一些用于声音,一些用于游戏脚本等。自定义分配器可以按子系统标记每个分配,并在超出单个预算时发出警告。
某些编译器附带的运算符 new 不保证动态分配双精度值的 8 字节对齐。
请引用。通常,默认的 new 运算符仅比 malloc 包装器稍微复杂一些,根据标准,malloc 包装器返回的内存与目标体系结构支持的任何数据类型适当对齐。
并不是说我没有充分的理由为自己的类重载和删除......你在这里谈到了几个合法的,但以上不是其中之一。
评论
我用它来分配特定共享内存领域的对象。(这与@Russell Borogove提到的类似。
几年前,我为CAVE开发了软件。这是一个多墙VR系统。它使用一台计算机来驱动每台投影仪;6 是最大值(4 个墙壁、地板和天花板),而 3 是更常见的(2 个墙壁和地板)。这些机器通过特殊的共享内存硬件进行通信。
为了支持它,我从我的普通(非 CAVE)场景类中派生出来,使用一个新的“new”,将场景信息直接放在共享内存领域中。然后,我将该指针传递给不同机器上的从属渲染器。
似乎值得在这里重复我从“任何理由重载全局新和删除?”的答案中的列表 - 请参阅该答案(或该问题的其他答案)以获取更详细的讨论、参考和其他原因。这些原因通常适用于本地运算符重载以及默认/全局重载,以及 C/// 重载或钩子。malloc
calloc
realloc
free
我们超载了我为许多人工作的全局 new 和 delete 运算符 原因:
- 汇集所有小额分配 -- 减少开销,减少碎片化,可以提高小额分配繁重应用的性能
- 构建具有已知生存期的分配 -- 忽略所有空闲,直到此时间段结束,然后释放所有空闲 一起(诚然,我们更多地使用本地操作员过载来做到这一点 比全球)
- 对齐调整 -- 到缓存线边界等
- alloc fill -- 帮助公开未初始化变量的用法
- 自由填充 -- 帮助公开以前删除的内存的使用情况
- 延迟自由 -- 提高自由填充的有效性,偶尔提高性能
- 哨兵或栅栏 -- 帮助暴露缓冲区溢出、欠载和偶尔出现的野生指针
- 重定向分配 -- 考虑 NUMA、特殊内存区域,甚至在内存中保持单独的系统(例如 嵌入式脚本语言或 DSL)
- 垃圾回收或清理 -- 同样适用于那些嵌入式脚本语言
- 堆验证 -- 您可以每 N 次分配/释放一次堆数据结构,以确保一切正常
- 会计,包括泄漏跟踪和使用情况快照/统计信息(堆栈、分配期限等)
评论