首页 文章

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

提问于
浏览
64

Why should would one replace the default operator new and delete with a custom new and delete operators?

这是在非常有启发性的C FAQ中继续Overloading new and delete
Operator overloading.

本FAQ的后续条目是:
How should I write ISO C++ standard conformant custom new and delete operators?

注意:答案是基于Scott Meyers的“更有效的C”的经验教训 . (注意:这是Stack Overflow的C FAQ的一个条目 . 如果你想批评在这个表单中提供常见问题解答的想法,那么发布所有这些的meta上的帖子就是这样做的地方 . 这个问题在C聊天室中受到监控,其中FAQ的想法首先出现在那里,所以你的答案很可能被那些提出这个想法的人阅读 . )

7 回答

  • 3

    有人可能会出于多种原因尝试替换 newdelete 运算符,即:

    检测使用错误:

    有许多方法可能会错误地使用 newdelete ,这可能会导致未定义的行为和内存泄漏的可怕动物 . 每个的例子是:
    new ed内存上使用多个 delete 而在使用 new 分配的内存上不调用 delete .
    重载运算符 new 可以保留已分配地址的列表,重载运算符 delete 可以从列表中删除地址,然后很容易检测到这样的使用错误 .

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


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

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

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


    收集使用情况统计信息:

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

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

    全部,可以通过替换自定义 newdelete 并在重载的 newdelete 中添加诊断收集机制来收集此信息 .


    补偿新的次优内存对齐:

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


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

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


    获得非常规行为:

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

  • 11

    首先,实际上有许多不同的 newdelete 运算符(实际上是一个任意数字) .

    首先,有 ::operator new::operator new[]::operator delete::operator delete[] . 第二,对于任何类 X ,有 X::operator newX::operator new[]X::operator deleteX::operator delete[] .

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

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

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

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

    既然's more common, I'将首先谈谈特定于类的运算符 . 特定于类的内存管理的主要原因是性能 . 这通常有两种形式(或两种形式):提高速度或减少碎片 . 由于内存管理器只处理特定大小的块,所以它可以返回任何空闲块的地址,而不是花时间检查块是否足够大,如果它块分成两个块,则速度会提高 . 's too large, etc. Fragmentation is reduced in (mostly) the same way -- for example, pre-allocating a block large enough for N objects gives exactly the space necessary for N objects; allocating one object'的内存将精确分配一个对象的空间,而不是单个字节 .

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

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


    • 大多数要求都包含在C标准的§3.7.3和§18.4中(或者C 0x中的§3.7.4和§18.6,至少从N3291开始) .

    • 我觉得有必要指出我没有't intend to pick on Microsoft'的编译器 - 我怀疑它有不寻常的数量这样的问题,但我碰巧使用它很多,所以我倾向于非常清楚它的问题 .

  • 3

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

    澄清一下:如果架构要求 double 数据是八字节对齐的,那么就没有什么可以优化的 . 任何类型的适当大小的动态分配(例如 malloc(size)operator new(size)operator new[](size)new char[size] ,其中 size >= sizeof(double) )都保证正确对齐 . 如果实现没有做出这种保证,那就不符合要求 . 在这种情况下,更改 operator new 以执行'the right thing'将尝试执行'fixing',而不是优化 .

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

  • 3

    带有一些编译器的运算符new不保证动态分配双精度的八字节对齐 .

    请引用 . 通常,默认的new运算符仅比malloc包装器稍微复杂一些,按标准,它返回的内存适合于目标体系结构支持的任何数据类型 .

    并不是说我没有充分的理由为自己的课程重载新的和删除...而你在这里已经触及了几个合法的,但上面不是其中之一 .

  • 6

    与使用情况统计相关:按子系统进行预算 . 例如,在基于控制台的游戏中,您可能希望为3D模型几何保留一些内存,一些用于纹理,一些用于声音,一些用于游戏脚本等 . 自定义分配器可以按子系统标记每个分配并发出超出个人预算时发出警告 .

  • 66

    我用它在特定的共享内存领域分配对象 . (这与@Russell Borogove提到的相似 . )

    多年前我为CAVE开发了软件 . 这是一个多墙VR系统 . 它用一台电脑驱动每台投影机; 6是最大值(4个墙壁,地板和天花板),而3个更常见(2个墙壁和地板) . 机器通过特殊的共享内存进行通信硬件 .

    为了支持它,我从我的普通(非CAVE)场景类派生出来,使用一个新的“new”,它将场景信息直接放在共享内存领域 . 然后我将指针传递给不同机器上的从属渲染器 .

  • 4

    似乎值得重复my answer from "Any reason to overload global new and delete?"这里的列表 - 请参阅答案(或确实是other answers to that question)以获得更详细的讨论,参考和其他原因 . 这些原因通常适用于本地运算符重载以及默认/全局重载,也适用于C malloc / calloc / realloc / free 重载或挂钩 .

    我们工作的全局new和delete运算符超载我的原因有很多:汇集所有小的分配 - 减少开销,减少碎片,可以提高构建具有已知生命周期的分配的小型alloc-heavy应用程序的性能 - 忽略所有释放直到这个时期的最后,然后将所有这些一起释放(不可否认我们用本地运算符重载比全局更多)对齐调整 - 到高速缓存行边界等,分配填充 - 帮助暴露未初始化变量的使用免费填充 - - 帮助揭示以前删除的内存的使用延迟免费 - 增加免费填充的效率,偶尔增加性能标记或fenceposts - 帮助暴露缓冲区溢出,欠载和偶尔的野指针重定向分配 - 占NUMA,特殊的内存区域,甚至在内存中分离独立的系统(例如嵌入式脚本语言或DSL)垃圾收集或清理 - 对于那些嵌入式脚本语言堆验证再次有用 - 你可以遍历堆数据结构,每N个分配/释放,以确保一切看起来都好,包括泄漏跟踪和使用快照/统计(堆栈,分配年龄等)

相关问题