有没有理由超载全局new和删除?

时间:2009-07-20 09:11:32

标签: c++ memory-management overloading

除非您正在编程操作系统或嵌入式系统的某些部分,否则有任何理由这样做?我可以想象,对于某些特定的类经常重载内存管理函数或引入一个对象池而创建和销毁的类可能会降低开销,但是在全局范围内做这些事情?

加成
我刚刚在重载的删除函数中发现了一个错误 - 内存并不总是被释放。这是一个不那么重要的内存关键应用程序。此外,禁用这些重载仅会使性能降低约0.5%。

16 个答案:

答案 0 :(得分:76)

我们工作的全局new和delete运算符超载的原因有很多:

  • 汇集所有小额分配 - 降低开销,减少碎片,可以提高小型重型应用的性能
  • 框架具有已知生命周期的分配 - 在此期间结束之前忽略所有释放,然后将所有这些释放放在一起(诚然,我们使用本地运算符重载而不是全局执行此操作)< / LI>
  • 对齐调整 - 到缓存行边界等
  • 分配填充 - 帮助公开未初始化变量的使用
  • 免费填充 - 帮助公开以前删除的内存的使用情况
  • 延迟免费 - 提高免费填充的效果,偶尔提高性能
  • 哨兵 fenceposts - 帮助公开缓冲区溢出,欠载和偶尔的狂野指针
  • 重定向分配 - 考虑NUMA,特殊内存区域,甚至将单独的系统保留在内存中(例如嵌入式脚本语言或DSL)
  • 垃圾收集或清理 - 对嵌入式脚本语言再次有用
  • 堆验证 - 每N次分配/释放可以遍历堆数据结构,以确保一切正常
  • 会计,包括泄漏跟踪使用情况快照/统计信息(堆栈,分配年龄等)

新/删除记帐的想法非常灵活和强大:例如,您可以在发生分配时记录活动线程的整个callstack,并汇总有关该记录的统计信息。如果由于某种原因没有空间将其保存在本地,您可以通过网络发送堆栈信息。您可以在这里收集的信息类型仅受您的想象力(当然还有表现)的限制。

我们使用全局重载,因为在那里挂起许多常见的调试功能很方便,并且根据我们从相同的重载中收集的统计信息,在整个应用程序中进行彻底的改进。

我们仍然会为个别类型使用自定义分配器;在许多情况下,通过为例如提供自定义分配器可以获得加速或功能。 STL数据结构的单一使用点远远超过了可以从全局过载中获得的一般加速。

看一下C / C ++的一些分配器和调试系统,你会很快想出这些和其他想法:

(一本陈旧但开创性的书籍Writing Solid Code,其中讨论了您可能希望在C中提供自定义分配器的许多原因,其中大多数仍然非常相关。)

显然,如果你可以使用这些精美的工具,你可以这样做,而不是自己动手。

在某些情况下,它更快,更容易,更少的商业/法律麻烦,您的平台无法提供任何东西,或者只是更具指导性:挖掘并编写全局超载。

答案 1 :(得分:26)

重载new和delete的最常见原因只是检查memory leaks和内存使用情况统计信息。请注意,“内存泄漏”通常会归结为内存错误。您可以检查双重删除和缓冲区溢出等内容。

之后的用途通常是内存分配方案,例如garbage collectionpooling

所有其他案例都只是特定的事情,在其他答案中提到(记录到磁盘,内核使用)。

答案 2 :(得分:15)

除了此处提到的其他重要用途,如内存标记,它也是强制应用程序中的所有分配都通过固定块分配的唯一方法,这对性能和碎片有很大影响。

例如,您可能有一系列具有固定块大小的内存池。覆盖全局new允许您将所有61字节分配指向具有64字节块的池,所有768-1024字节分配到1024b块池,所有那些都高于2048字节块游泳池,以及任何大于8kb的一般衣衫褴褛的堆。

因为固定块分配器比从堆中无条件地分配更快且更不容易碎片化,所以这可以强制你从池中分配蹩脚的3d派对代码,而不是整个地址空间。

这通常在对时间和空间至关重要的系统中完成,例如游戏。 280Z28,Meeh和Dan Olson描述了原因。

答案 3 :(得分:10)

UnrealEngine3将全局new和delete重载为其核心内存管理系统的一部分。有多个分配器提供不同的功能(分析,性能等),并且需要所有分配来完成它。

编辑:对于我自己的代码,我只会做它作为最后的手段。我的意思是,我几乎肯定不会使用它。但是我的个人项目显然要小得多/非常不同。

答案 4 :(得分:6)

一些实时系统会使它们超载以避免在init之后使用它们。

答案 5 :(得分:4)

超载新&amp; delete可以在内存分配中添加标记。我标记每个系统或控件或中间件的分配。我可以在运行时查看每个使用的数量。也许我想看看与UI分离的解析器的用法或者一块中间件真正使用了多少!

您还可以使用它在所分配的内存周围放置保护带。如果/当您的应用程序崩溃时,您可以查看该地址。如果您将内容视为“0xABCDABCD”(或您选择的任何内容),则表示您正在访问您不拥有的内存。

也许在调用删除后,您可以使用类似的可识别模式填充此空间。 我相信VisualStudio在调试中做了类似的事情。是不是用0xCDCDCDCD填充了未初始化的内存?

最后,如果您有碎片问题,可以使用它重定向到块分配器?我不确定这经常是一个问题。

答案 6 :(得分:3)

如果对您的环境中的new和delete调用不起作用,则需要重载它们。

例如,在内核编程中,默认的new和delete不起作用,因为它们依赖于用户模式库来分配内存。

答案 7 :(得分:2)

从实际的角度来看,在系统库级别覆盖malloc可能更好,因为operator new可能会调用它。

在linux上,您可以将自己的malloc版本替换为系统版本,如下例所示:

http://developers.sun.com/solaris/articles/lib_interposers.html

在那篇文章中,他们试图收集性能统计数据。但是如果你也覆盖了free,你也可以检测到内存泄漏。

由于您在使用LD_PRELOAD的共享库中执行此操作,因此您甚至无需重新编译应用程序。

答案 8 :(得分:2)

我已经看到它在一个系统中完成,因为'安全'*原因需要写入它在解除分配时使用的所有内存。方法是在每个内存块的开头分配一个额外的几个字节,它将包含整个块的大小,然后在删除时用零覆盖。

这有很多问题,你可以想象但它确实有效(大多数情况下)并且使团队无法在一个相当大的现有应用程序中审查每一个内存分配。

当然不是说这是一个很好的用途,但它可能是那里比较富有想象力的人之一...

*遗憾的是,实际的安全性并不像安全的出现那么多......

答案 9 :(得分:2)

用C ++编写的Photoshop插件应覆盖operator new,以便通过Photoshop获取内存。

答案 10 :(得分:2)

我已经完成了内存映射文件,因此写入内存的数据也会自动保存到磁盘中 如果你有内存映射的IO设备,它也用于返回特定物理地址的内存,或者有时你需要分配一块连续的内存。

但99%的时间它作为调试功能来记录分配和释放内存的频率,地点,时间。

答案 11 :(得分:2)

游戏从系统中分配一大块内存然后通过重载的new和delete提供自定义分配器实际上很常见。一个重要原因是控制台具有固定的内存大小,使泄漏和碎片都成为大问题。

通常(至少在封闭平台上)默认堆操作缺乏控制和缺乏内省。对于许多应用而言,这并不重要,但是对于在固定内存情况下稳定运行的游戏,增加的控制和内省都非常重要。

答案 12 :(得分:2)

对于您的应用程序来说,能够通过其他方式响应低内存条件而不是随机崩溃,这可能是一个很好的技巧。为此,new可以是默认new的简单代理,可以捕获其失败,释放一些内容并再次尝试。

最简单的技术是在启动时为此目的保留一个空白的内存块。您可能还有一些可以使用的缓存 - 这个想法是一样的。

当第一次分配失败开始时,您仍然有时间警告您的用户有关内存不足的情况(“我将能够存活一段时间,但您可能希望保存您的工作并关闭其他一些应用程序“),将状态保存到磁盘,切换到生存模式,或者在您的上下文中有意义的任何其他内容。

答案 13 :(得分:0)

最常见的用例可能是泄漏检查。

另一个用例是当您对环境中的内存分配有特定要求时,您使用的标准库不满足这些要求,例如,您需要保证在多线程环境中内存分配是无锁的

答案 14 :(得分:0)

正如许多人已经说过的,这通常在性能关键应用程序中完成,或者能够控制内存对齐或跟踪内存。游戏经常使用自定义内存管理器,尤其是在定位特定平台/控制台时。

这是一个非常好的blog post about one way of doing this和一些推理。

答案 15 :(得分:0)

重载的new运算符还使程序员能够从其程序中榨取一些额外的性能。例如,在一个类中,为了加快新节点的分配速度,维护了一个已删除节点的列表,以便在分配新节点时可以重用它们的内存。在这种情况下,重载的delete运算符会将节点添加到列表中删除的节点和重载的new运算符将从列表中分配内存,而不是从堆中分配内存以加速内存分配。当删除的节点列表为空时,可以使用堆中的内存。