【问题标题】:Why don't we just keep all dynamic memory in arrays?为什么我们不把所有的动态内存都保存在数组中呢?
【发布时间】:2020-05-31 23:31:36
【问题描述】:

在 C/C++ 中,当您分配动态内存时,操作系统会为您提供一个无法预测的地址,指向某个遥远的内存区域。如果您分配了大量动态内存,并且需要在它们之间移动(例如,链表、树),那么您必须浪费时间等待 CPU 查询 RAM,因为很可能,您的内存块想要的记忆与你的记忆并不接近。

最近,对于我的一门大学课程中的一个项目,我必须实现一棵树,以便节点可以有任意数量的子节点,并且我必须能够在恒定时间内将节点合并为单个树。所以,我有一堆内部 Node 对象和一堆指向周围节点的指针。

因为这是 C++,所以它当然很快,但随着时间和内存的测试,它的性能还可以。

因此,我决定尝试实现指针替换,这实际上是静态向量的索引,以及在需要新对象时增长向量的函数,以及可以重复使用的“已删除”索引堆栈。这样一来,所有内容都将在内存中紧密相连,并且取消引用不太可能导致缓存未命中。另外,因为我知道树不会在千兆字节的数据规模上进行测试,所以我可以将这些索引限制为 32 位,从而显着降低树结构的内存开销。

结果令人惊叹——在大多数情况下,我的课程的员工实施时间和记忆力只有三分之一。

我的问题是,如果缓存的好处如此极端,我们为什么要直接使用new 和delete 呢?为什么不把所有东西都放在向量中,每种类型一个呢?

【问题讨论】:

  • 当然,如果您的程序可以提前准确预测每个对象需要多少,无论程序在运行时最终会做什么。
  • @SamVarshavchik 我的意思是动态分配的数组
  • 向量存在一些限制,例如在向量的开头或中间插入元素,时间复杂度为 O(n )。这是您需要选择插入和删除元素的频率与搜索元素的频率之间的权衡。如果你不想使用 new/delete,你可以使用 std::map。
  • 这实际上是静态向量的索引 -- 听起来这在没有适当同步的多线程程序中是个问题。
  • 这就是我们有分配器的原因。此外,当性能满足您的需求时,这些性能提升带来的好处将被优化成本抵消。

标签: c++ caching optimization memory heap-memory


【解决方案1】:

我们为什么不将所有动态内存都保存在数组中?

公平地说,我们确实将大部分动态内存保留在数组中。

但有时数组无法达到与基于节点的结构相同的渐近复杂性,在这种情况下,不使用数组可能是有益的。

我们为什么要直接使用 new 和 delete 呢?

很少需要直接使用new和delete。

【讨论】:

  • 我想我最后一次使用new 大约是在 11 年前。事后看来,这可能是一个错误。
【解决方案2】:

当标准分配器“足够好”完成您需要做的事情时,您可以使用它们。以 10 毫秒运行的程序与以 100 毫秒运行的程序之间的差异可能是 10 倍,但实际上这种差异毫无意义。两者都“立即”运行。

这并不是说您永远不应该使用自己的分配策略。通常,您应该使用与您的用例和要求相匹配的分配策略。您所描述的内容听起来很像 object pooling,这是一种众所周知且经常使用的模式,在您拥有大量相同类型的对象的情况下。


请记住,标准内存分配器必须支持所有可能的用例。 由于您的专用分配器知道您的使用模式,因此您可以针对您的特定用例从中挤出更多性能,但它不太可能比重度分配器更快在所有可能的用例中调整标准分配器。

new/malloc 的大多数标准库实现使用的是池分配器,它在某种程度上是您实现的对象池的泛化。第一次请求一些内存时,它们会从操作系统分配一大块内存,然后根据请求分发该池的部分,跟踪哪些部分正在使用,哪些部分可供以后(重新)使用。当池用完时,它们会从操作系统获得另一块内存并开始分发其中的一部分。

与您实施的最大不同之处在于,当您分配一个新池时,会将所有内容从旧池移到新池中。在一般情况下,对于大量数据,复制可能会成为一个瓶颈,它会克服您从本地获得的任何好处,如果您的数据大于缓存行,那么它是否都是连续的并不重要。

您的方法还要求分配器的每个用户都知道它的实现,因为每次重新分配都会使对旧池的任何指针或引用无效。这意味着您已经用易于维护换取了性能。这对于您永远不会再看的学校项目可能无关紧要,但对于实际应用程序而言,持续维护成本是一个非常重要的考虑因素。

【讨论】:

    【解决方案3】:

    补充迈尔斯的非常好的答案:

    当然,如果您根据特定的使用场景编写自己的内存分配机制,您可以做很多很棒的技巧。我们通常不会为此烦恼,因为标准机制已经足够好了,因为我们通常不希望编写和维护大量中间件代码,并且因为我们不想混淆可能需要查看的其他人在不熟悉我们的自定义、古怪、神秘的完成工作的机制的情况下阅读我们的代码。

    简而言之,我们已经到了计算机速度如此之快以至于易于开发和可维护性比原始性能更重要的地步。

    话虽如此,在性能确实至关重要的极端应用程序中,人们确实倾向于像您一样为内存分配等事情推出自己的机制,以节省每个可以节省的时钟周期。

    此外,还有一些方法可以在幕后实现您自己的内存分配机制,这样您的应用程序级代码就不需要知道该机制是否到位,并且它看起来与以“正常”或“标准”的方式做事。

    您知道new 和delete 运算符可以在C++ 中重载吗?看看吧,它会为你打开一个全新的世界。因此,您可以编写池内存分配器,使应用程序级代码对new 和delete 的使用看起来完全正常(或接近完全正常),并且您可以拦截这种用法在幕后并将其重定向到您的自定义内存分配器。我过去做过这些事情,至少可以说很有趣。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-08-16
      • 2014-12-05
      • 1970-01-01
      • 2018-12-21
      • 1970-01-01
      • 1970-01-01
      • 2011-12-22
      相关资源
      最近更新 更多