有啥理由重载全局新建和删除?

Posted

技术标签:

【中文标题】有啥理由重载全局新建和删除?【英文标题】:Any reason to overload global new and delete?有什么理由重载全局新建和删除? 【发布时间】:2010-11-12 06:10:14 【问题描述】:

除非您正在对操作系统或嵌入式系统的某些部分进行编程,否则有什么理由这样做吗?我可以想象,对于某些频繁创建和销毁的特定类,重载内存管理功能或引入对象池可能会降低开销,但是全局执行这些操作?

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

【问题讨论】:

【参考方案1】:

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

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

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

我们使用全局重载是因为可以方便地在其中挂起大量常见的调试功能,并根据我们从这些重载中收集的统计数据对整个应用进行全面改进。

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

看看一些用于 C/C++ 的分配器和调试系统,您会很快想到这些和其他想法:

valgrind electricfence dmalloc dlmalloc Application Verifier Insure++ BoundsChecker ...以及许多其他...(游戏开发行业是一个值得关注的好地方)

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

显然,如果您可以使用这些优秀工具中的任何一个,您会想要这样做,而不是自己动手。

在某些情况下,它更快、更容易、业务/法律麻烦更少、您的平台还没有可用的东西,或者只是更有启发性:深入研究并编写一个全局重载。

【讨论】:

哇,你实际上已经构建了类似于垃圾收集器的东西 @Andrei 但在恒定内存中快五倍!【参考方案2】:

重载 new 和 delete 的最常见原因只是检查 memory leaks 和内存使用统计信息。请注意,“内存泄漏”通常概括为内存错误。您可以检查诸如双重删除和缓冲区溢出之类的事情。

之后的用途通常是内存分配方案,例如garbage collection和pooling。

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

【讨论】:

【参考方案3】:

除了这里提到的其他重要用途(如内存标记)之外,它也是强制应用中所有分配进行固定块分配的唯一方法,这对性能和碎片有巨大影响。

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

因为固定块分配器比从堆中随意分配要快得多且更不容易产生碎片,这让您甚至可以强制从池中分配糟糕的 3d 方代码,而不是在整个地址空间中大便。

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

【讨论】:

nb Leander 在下面更深入地探讨了这一点。【参考方案4】:

UnrealEngine3 重载全局 new 和 delete 作为其核心内存管理系统的一部分。有多个分配器提供不同的功能(分析、性能等),它们需要所有分配都通过它。

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

【讨论】:

当然,游戏开发是一个非常特殊的领域。例如,针对特殊多核架构的应用程序等,必须在全球范围内重载新/删除。【参考方案5】:

一些实时系统会过载它们以避免它们在初始化后被使用..

【讨论】:

【参考方案6】:

重载 new 和 delete 可以为您的内存分配添加标签。我标记每个系统或控件或中间件的分配。我可以在运行时查看每个使用了多少。也许我想看看从 UI 中分离出来的解析器的使用情况,或者一个中间件真正使用了多少!

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

也许在调用 delete 之后,您可以用类似可识别的模式填充这个空间。 我相信 VisualStudio 在调试中会做类似的事情。不是用 0xCDCDCDCD 填充未初始化的内存吗?

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

【讨论】:

【参考方案7】:

当对 new 和 delete 的调用在您的环境中不起作用时,您需要重载它们。

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

【讨论】:

【参考方案8】:

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

在 linux 上,您可以使用自己的 malloc 版本代替系统版本,如下例所示:

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

在那篇文章中,他们试图收集性能统计信息。但是,如果您还覆盖 free,您也可能会检测到内存泄漏。

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

【讨论】:

我在这里问了这个问题。看起来有一种方法。 ***.com/questions/1210533/interposers-on-windows【参考方案9】:

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

正如您可能想象的那样,这存在许多问题,但它确实(大部分)有效,并且使团队免于审查一个相当大的现有应用程序中的每一个内存分配。

当然不是说这是一个很好的用途,但它可能是最有想象力的用途之一......

* 遗憾的是,与其说是实际安全,不如说是安全...

【讨论】:

那一个实际上是合理的。在某些(偏执的)系统中,您需要多次覆盖释放的内存:-) 当您拥有 MMU 和非平凡的内存使用模式(包括使用 realloc)时,这实际上可行吗? 简短回答 - 是的,据我所知。 Longer:MMU 将如何影响这一点?您通常不会将 realloc 与 new 和 delete 一起使用 - 那将如何工作?然而,公平地说,这并不是为了防止物理级别的攻击。对我们来说,软件不容易在内存中找到信息就足够了。换句话说,如果没有重载,我们可以搜索内存并找到我们无法找到的重载数据。所以......正如我所说 - 安全的外观比实际的安全。 在此处进一步跟进。如果您以这种方式考虑 - 您正在以非管理员用户身份运行应用程序。该应用程序有一些非常重要的数据,其他应用程序(例如信用卡)不应该使用这些数据。我能想到的唯一一种情况是,另一个应用程序可以可靠地访问分配给另一个进程的内存,这意味着您已经以某种方式受到了损害。 (如果一个进程在那里扫描分配给其他进程的内存以查找潜在的信用卡号,那么您已经丢失了)。【参考方案10】:

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

【讨论】:

【参考方案11】:

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

但在 99% 的情况下,它是作为一种调试功能来记录分配和释放内存的频率、地点和时间。

【讨论】:

谢谢。写入文件可能在调试阶段确实有用。在特定物理地址分配内存再次仅适用于嵌入式系统等,不适用于通用软件。【参考方案12】:

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

通常(至少在封闭平台上)默认堆操作缺乏控制和自省。对于许多应用程序来说,这无关紧要,但要让游戏在固定内存情况下稳定运行,增加的控制和自省都非常重要。

【讨论】:

【参考方案13】:

如果您的应用程序能够通过随机崩溃以外的其他方式响应内存不足的情况,这可能是一个不错的技巧。为此,您的new 可以是默认new 的简单代理,它可以捕获其失败、释放一些内容并重试。

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

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

【讨论】:

【参考方案14】:

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

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

【讨论】:

【参考方案15】:

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

这是一个相当不错的blog post about one way of doing this 和一些推理。

【讨论】:

【参考方案16】:

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

【讨论】:

以上是关于有啥理由重载全局新建和删除?的主要内容,如果未能解决你的问题,请参考以下文章

全局删除冲突重载

有啥理由不放弃“var”吗?

MinGW 中的全局重载运算符 new/delete

C++ 自定义全局新建/删除覆盖系统库

全局静态函数加上static和不加上有啥区别呢 尽量详细点 可加分哦

29.局部和全局重载new delete